From 705608ae466edf812db541465bfafc14394ad50c Mon Sep 17 00:00:00 2001 From: Kunthawat Greethong Date: Fri, 5 Dec 2025 20:01:06 +0700 Subject: [PATCH] Add Dyad Update Management Guide and custom hooks for smart context --- UPDATE_GUIDE.md | 198 ++++++++++++++++ .../commit-history.txt | 7 + .../custom/hooks/useSmartContext.ts | 60 +++++ dyad-backup-20251205-195655/custom/index.ts | 18 ++ .../custom/ipc/smart_context_handlers.ts | 65 ++++++ .../custom/utils/smart_context_store.ts | 212 ++++++++++++++++++ dyad-backup-20251205-195655/last-changes.diff | 13 ++ 7 files changed, 573 insertions(+) create mode 100644 UPDATE_GUIDE.md create mode 100644 dyad-backup-20251205-195655/commit-history.txt create mode 100644 dyad-backup-20251205-195655/custom/hooks/useSmartContext.ts create mode 100644 dyad-backup-20251205-195655/custom/index.ts create mode 100644 dyad-backup-20251205-195655/custom/ipc/smart_context_handlers.ts create mode 100644 dyad-backup-20251205-195655/custom/utils/smart_context_store.ts create mode 100644 dyad-backup-20251205-195655/last-changes.diff diff --git a/UPDATE_GUIDE.md b/UPDATE_GUIDE.md new file mode 100644 index 0000000..224eb9b --- /dev/null +++ b/UPDATE_GUIDE.md @@ -0,0 +1,198 @@ +# Dyad Update Management Guide + +This guide explains how to update your forked Dyad application while preserving your custom modifications. + +## 🎯 Overview + +Your setup uses a **selective update strategy** that: +- Keeps your custom code separate from the main codebase +- Automatically preserves custom modifications during updates +- Provides backup and rollback capabilities +- Minimizes merge conflicts + +## 📁 Custom Code Structure + +Your custom modifications are organized in `src/custom/`: + +``` +src/custom/ +├── index.ts # Main entry point for custom features +├── hooks/ +│ └── useSmartContext.ts # Custom smart context hook +├── ipc/ +│ └── smart_context_handlers.ts # Custom IPC handlers +└── utils/ + └── smart_context_store.ts # Custom utilities +``` + +## 🚀 Update Process + +### Method 1: Automated Update (Recommended) + +Use the provided update script: + +```bash +./update-dyad-v2.sh +``` + +**What the script does:** +1. Creates a timestamped backup +2. Backs up your custom code +3. Fetches latest changes from upstream +4. Resets to the latest upstream version +5. Restores your custom code +6. Pushes updates to your fork + +### Method 2: Manual Update + +If you prefer manual control: + +```bash +# 1. Create backup +cp -r src/custom/ dyad-backup-$(date +%Y%m%d-%H%M%S)/ + +# 2. Fetch latest changes +git fetch upstream + +# 3. Reset to latest upstream +git reset --hard upstream/main + +# 4. Restore custom code +cp -r dyad-backup-*/src/custom/ src/ + +# 5. Commit and push +git add src/custom/ +git commit -m "Restore custom code after update" +git push origin main +``` + +## 🔄 Update Workflow + +### Before Updating +1. **Test current state** - Ensure your app works properly +2. **Commit any changes** - Don't have uncommitted work +3. **Check custom code** - Note any modifications that might need updates + +### After Updating +1. **Run npm install** - Update dependencies if needed +2. **Test the application** - Ensure everything works +3. **Check custom integrations** - Verify custom features still work +4. **Update custom code** if needed - Adapt to any API changes + +## 🛠️ Adding New Custom Features + +When adding new custom features: + +1. **Place in src/custom/** - Keep custom code organized +2. **Update imports** - Use relative imports within custom directory +3. **Document changes** - Note what each custom feature does +4. **Test integration** - Ensure custom features work with main app + +Example: +```typescript +// src/custom/components/MyCustomComponent.tsx +import { useSmartContext } from '../hooks/useSmartContext'; + +export const MyCustomComponent = () => { + // Your custom logic +}; +``` + +## 🚨 Troubleshooting + +### Merge Conflicts +If you encounter merge conflicts during manual updates: + +```bash +# Abort the merge +git merge --abort + +# Use the automated script instead +./update-dyad-v2.sh +``` + +### Custom Code Not Working +After an update, if custom features don't work: + +1. **Check API changes** - The upstream may have changed interfaces +2. **Update imports** - File paths might have changed +3. **Review breaking changes** - Check upstream release notes +4. **Test incrementally** - Isolate the problematic code + +### Backup Restoration +If you need to restore from backup: + +```bash +# Find your backup directory +ls dyad-backup-* + +# Restore specific files +cp -r dyad-backup-YYYYMMDD-HHMMSS/src/custom/ src/ + +# Or restore entire project (last resort) +rm -rf * .gitignore +cp -r dyad-backup-YYYYMMDD-HHMMSS/* . +cp dyad-backup-YYYYMMDD-HHMMSS/.gitignore . +``` + +## 📋 Best Practices + +### Regular Maintenance +- **Update frequently** - Smaller updates are easier to manage +- **Test after each update** - Catch issues early +- **Keep custom code minimal** - Only customize what's necessary +- **Document customizations** - Future you will thank you + +### Code Organization +- **Separate concerns** - Keep UI, logic, and utilities separate +- **Use TypeScript** - Catch integration issues early +- **Follow existing patterns** - Match the upstream code style +- **Avoid modifying core files** - Use extension patterns when possible + +### Backup Strategy +- **Multiple backups** - Keep several backup versions +- **Offsite backup** - Consider cloud storage for critical backups +- **Test backups** - Ensure you can restore from backup +- **Label clearly** - Use descriptive backup names + +## 🔧 Advanced Configuration + +### Custom Update Script +You can modify `update-dyad-v2.sh` to: +- Skip certain files from backup +- Add custom post-update steps +- Include additional validation +- Send notifications on completion + +### Selective File Restoration +To restore only specific custom files: + +```bash +# Restore specific directory +cp -r dyad-backup-*/src/custom/hooks/ src/custom/ + +# Restore specific file +cp dyad-backup-*/src/custom/index.ts src/custom/ +``` + +## 📞 Getting Help + +If you encounter issues: + +1. **Check this guide first** - Most common issues are covered +2. **Review the script output** - Error messages are informative +3. **Test with a clean state** - Start fresh if needed +4. **Document the issue** - Note what you were trying to do + +## 🎉 Success Indicators + +You'll know the update was successful when: +- ✅ Script completes without errors +- ✅ Custom code is present in `src/custom/` +- ✅ Application starts and runs normally +- ✅ Custom features work as expected +- ✅ No merge conflicts in git status + +--- + +**Remember**: The goal is to make updates painless and predictable. When in doubt, use the automated script and keep good backups! diff --git a/dyad-backup-20251205-195655/commit-history.txt b/dyad-backup-20251205-195655/commit-history.txt new file mode 100644 index 0000000..1d95c10 --- /dev/null +++ b/dyad-backup-20251205-195655/commit-history.txt @@ -0,0 +1,7 @@ +52a977b backup: auto-commit before update - Fri Dec 5 15:16:35 +07 2025 +8a1cecb backup: auto-commit before update - Fri Dec 5 15:12:17 +07 2025 +e6de49c fix: update .gitignore to exclude 'out/' directory +6d74721 Add user settings configuration for GPT-5 Codex model with Azure provider +d22227b feat: implement fuzzy search and replace functionality with Levenshtein distance +11986a0 Add project files +3b43cb5 Add blank diff --git a/dyad-backup-20251205-195655/custom/hooks/useSmartContext.ts b/dyad-backup-20251205-195655/custom/hooks/useSmartContext.ts new file mode 100644 index 0000000..85042ae --- /dev/null +++ b/dyad-backup-20251205-195655/custom/hooks/useSmartContext.ts @@ -0,0 +1,60 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { IpcClient } from "@/ipc/ipc_client"; +import type { + SmartContextMeta, + SmartContextSnippet, + SmartContextRetrieveResult, +} from "@/ipc/ipc_types"; + +export function useSmartContextMeta(chatId: number) { + return useQuery({ + queryKey: ["smart-context", chatId, "meta"], + queryFn: async () => { + const ipc = IpcClient.getInstance(); + return ipc.getSmartContextMeta(chatId); + }, + enabled: !!chatId, + }); +} + +export function useRetrieveSmartContext( + chatId: number, + query: string, + budgetTokens: number, +) { + return useQuery({ + queryKey: ["smart-context", chatId, "retrieve", query, budgetTokens], + queryFn: async () => { + const ipc = IpcClient.getInstance(); + return ipc.retrieveSmartContext({ chatId, query, budgetTokens }); + }, + enabled: !!chatId && !!query && budgetTokens > 0, + meta: { showErrorToast: true }, + }); +} + +export function useUpsertSmartContextSnippets(chatId: number) { + const qc = useQueryClient(); + return useMutation>>({ + mutationFn: async (snippets) => { + const ipc = IpcClient.getInstance(); + return ipc.upsertSmartContextSnippets(chatId, snippets); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["smart-context", chatId] }); + }, + }); +} + +export function useUpdateRollingSummary(chatId: number) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ summary }) => { + const ipc = IpcClient.getInstance(); + return ipc.updateSmartContextRollingSummary(chatId, summary); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["smart-context", chatId, "meta"] }); + }, + }); +} \ No newline at end of file diff --git a/dyad-backup-20251205-195655/custom/index.ts b/dyad-backup-20251205-195655/custom/index.ts new file mode 100644 index 0000000..63ec1c0 --- /dev/null +++ b/dyad-backup-20251205-195655/custom/index.ts @@ -0,0 +1,18 @@ +// Custom modules for moreminimore-vibe +// This file exports all custom functionality to make imports easier + +// Custom hooks +export { useSmartContextMeta, useRetrieveSmartContext, useUpsertSmartContextSnippets, useUpdateRollingSummary } from './hooks/useSmartContext'; + +// Custom IPC handlers (these will need to be imported and registered in the main process) +export { registerSmartContextHandlers } from './ipc/smart_context_handlers'; + +// Custom utilities +export * from './utils/smart_context_store'; + +// Re-export types that might be needed +export type { + SmartContextMeta, + SmartContextSnippet, + SmartContextRetrieveResult, +} from '../ipc/ipc_types'; diff --git a/dyad-backup-20251205-195655/custom/ipc/smart_context_handlers.ts b/dyad-backup-20251205-195655/custom/ipc/smart_context_handlers.ts new file mode 100644 index 0000000..9309bcb --- /dev/null +++ b/dyad-backup-20251205-195655/custom/ipc/smart_context_handlers.ts @@ -0,0 +1,65 @@ +import log from "electron-log"; +import { createLoggedHandler } from "./safe_handle"; +import { + appendSnippets, + readMeta, + retrieveContext, + updateRollingSummary, + rebuildIndex, + type SmartContextSnippet, + type SmartContextMeta, +} from "../utils/smart_context_store"; + +const logger = log.scope("smart_context_handlers"); +const handle = createLoggedHandler(logger); + +export interface UpsertSnippetsParams { + chatId: number; + snippets: Array<{ + text: string; + source: + | { type: "message"; messageIndex?: number } + | { type: "code"; filePath: string } + | { type: "attachment"; name: string; mime?: string } + | { type: "other"; label?: string }; + }>; +} + +export interface RetrieveContextParams { + chatId: number; + query: string; + budgetTokens: number; +} + +export function registerSmartContextHandlers() { + handle("sc:get-meta", async (_event, chatId: number): Promise => { + return readMeta(chatId); + }); + + handle( + "sc:upsert-snippets", + async (_event, params: UpsertSnippetsParams): Promise => { + const count = await appendSnippets(params.chatId, params.snippets); + return count; + }, + ); + + handle( + "sc:update-rolling-summary", + async (_event, params: { chatId: number; summary: string }): Promise => { + return updateRollingSummary(params.chatId, params.summary); + }, + ); + + handle( + "sc:retrieve-context", + async (_event, params: RetrieveContextParams) => { + return retrieveContext(params.chatId, params.query, params.budgetTokens); + }, + ); + + handle("sc:rebuild-index", async (_event, chatId: number) => { + await rebuildIndex(chatId); + return { ok: true } as const; + }); +} diff --git a/dyad-backup-20251205-195655/custom/utils/smart_context_store.ts b/dyad-backup-20251205-195655/custom/utils/smart_context_store.ts new file mode 100644 index 0000000..560a538 --- /dev/null +++ b/dyad-backup-20251205-195655/custom/utils/smart_context_store.ts @@ -0,0 +1,212 @@ +import path from "node:path"; +import { promises as fs } from "node:fs"; +import { randomUUID } from "node:crypto"; +import { getUserDataPath } from "../../paths/paths"; +import { estimateTokens } from "./token_utils"; + +export type SmartContextSource = + | { type: "message"; messageIndex?: number } + | { type: "code"; filePath: string } + | { type: "attachment"; name: string; mime?: string } + | { type: "other"; label?: string }; + +export interface SmartContextSnippet { + id: string; + text: string; + score?: number; + source: SmartContextSource; + ts: number; // epoch ms + tokens?: number; +} + +export interface SmartContextMetaConfig { + maxSnippets?: number; +} + +export interface SmartContextMeta { + entityId: string; // e.g., chatId as string + updatedAt: number; + rollingSummary?: string; + summaryTokens?: number; + config?: SmartContextMetaConfig; +} + +function getThreadDir(chatId: number): string { + const base = path.join(getUserDataPath(), "smart-context", "threads"); + return path.join(base, String(chatId)); +} + +function getMetaPath(chatId: number): string { + return path.join(getThreadDir(chatId), "meta.json"); +} + +function getSnippetsPath(chatId: number): string { + return path.join(getThreadDir(chatId), "snippets.jsonl"); +} + +async function ensureDir(dir: string): Promise { + await fs.mkdir(dir, { recursive: true }); +} + +export async function readMeta(chatId: number): Promise { + const dir = getThreadDir(chatId); + await ensureDir(dir); + const metaPath = getMetaPath(chatId); + try { + const raw = await fs.readFile(metaPath, "utf8"); + const meta = JSON.parse(raw) as SmartContextMeta; + return meta; + } catch { + const fresh: SmartContextMeta = { + entityId: String(chatId), + updatedAt: Date.now(), + rollingSummary: "", + summaryTokens: 0, + config: { maxSnippets: 400 }, + }; + await fs.writeFile(metaPath, JSON.stringify(fresh, null, 2), "utf8"); + return fresh; + } +} + +export async function writeMeta( + chatId: number, + meta: SmartContextMeta, +): Promise { + const dir = getThreadDir(chatId); + await ensureDir(dir); + const metaPath = getMetaPath(chatId); + const updated: SmartContextMeta = { + ...meta, + entityId: String(chatId), + updatedAt: Date.now(), + }; + await fs.writeFile(metaPath, JSON.stringify(updated, null, 2), "utf8"); +} + +export async function updateRollingSummary( + chatId: number, + summary: string, +): Promise { + const meta = await readMeta(chatId); + const summaryTokens = estimateTokens(summary || ""); + const next: SmartContextMeta = { + ...meta, + rollingSummary: summary, + summaryTokens, + }; + await writeMeta(chatId, next); + return next; +} + +export async function appendSnippets( + chatId: number, + snippets: Omit[], +): Promise { + const dir = getThreadDir(chatId); + await ensureDir(dir); + const snippetsPath = getSnippetsPath(chatId); + const withDefaults: SmartContextSnippet[] = snippets.map((s) => ({ + id: randomUUID(), + ts: Date.now(), + tokens: estimateTokens(s.text), + ...s, + })); + const lines = withDefaults.map((obj) => JSON.stringify(obj)).join("\n"); + await fs.appendFile(snippetsPath, (lines ? lines + "\n" : ""), "utf8"); + + // prune if exceeded max + const meta = await readMeta(chatId); + const maxSnippets = meta.config?.maxSnippets ?? 400; + try { + const file = await fs.readFile(snippetsPath, "utf8"); + const allLines = file.split("\n").filter(Boolean); + if (allLines.length > maxSnippets) { + const toKeep = allLines.slice(allLines.length - maxSnippets); + await fs.writeFile(snippetsPath, toKeep.join("\n") + "\n", "utf8"); + return toKeep.length; + } + return allLines.length; + } catch { + return withDefaults.length; + } +} + +export async function readAllSnippets(chatId: number): Promise { + try { + const raw = await fs.readFile(getSnippetsPath(chatId), "utf8"); + return raw + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as SmartContextSnippet); + } catch { + return []; + } +} + +function normalize(value: number, min: number, max: number): number { + if (max === min) return 0; + return (value - min) / (max - min); +} + +function keywordScore(text: string, query: string): number { + const toTokens = (s: string) => + s + .toLowerCase() + .replace(/[^a-z0-9_\- ]+/g, " ") + .split(/\s+/) + .filter(Boolean); + const qTokens = new Set(toTokens(query)); + const tTokens = toTokens(text); + if (qTokens.size === 0 || tTokens.length === 0) return 0; + let hits = 0; + for (const tok of tTokens) if (qTokens.has(tok)) hits++; + return hits / tTokens.length; // simple overlap ratio +} + +export interface RetrieveContextResult { + rollingSummary?: string; + usedTokens: number; + snippets: SmartContextSnippet[]; +} + +export async function retrieveContext( + chatId: number, + query: string, + budgetTokens: number, +): Promise { + const meta = await readMeta(chatId); + const snippets = await readAllSnippets(chatId); + const now = Date.now(); + let minTs = now; + let maxTs = 0; + for (const s of snippets) { + if (s.ts < minTs) minTs = s.ts; + if (s.ts > maxTs) maxTs = s.ts; + } + const scored = snippets.map((s) => { + const recency = normalize(s.ts, minTs, maxTs); + const kw = keywordScore(s.text, query); + const base = 0.6 * kw + 0.4 * recency; + const score = base; + return { ...s, score } as SmartContextSnippet; + }); + scored.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)); + + const picked: SmartContextSnippet[] = []; + let usedTokens = 0; + for (const s of scored) { + const t = s.tokens ?? estimateTokens(s.text); + if (usedTokens + t > budgetTokens) break; + picked.push(s); + usedTokens += t; + } + + const rollingSummary = meta.rollingSummary || ""; + return { rollingSummary, usedTokens, snippets: picked }; +} + +export async function rebuildIndex(_chatId: number): Promise { + // Placeholder for future embedding/vector index rebuild. + return; +} \ No newline at end of file diff --git a/dyad-backup-20251205-195655/last-changes.diff b/dyad-backup-20251205-195655/last-changes.diff new file mode 100644 index 0000000..85869aa --- /dev/null +++ b/dyad-backup-20251205-195655/last-changes.diff @@ -0,0 +1,13 @@ +diff --git a/update-dyad.sh b/update-dyad.sh +index 2763b94..4d7da4c 100755 +--- a/update-dyad.sh ++++ b/update-dyad.sh +@@ -78,7 +78,7 @@ git log --oneline HEAD..upstream/main --reverse + + # Attempt to merge + print_status "Merging upstream changes into your branch..." +-if git merge upstream/main -m "merge: update from upstream - $(date)"; then ++if git merge upstream/main -m "merge: update from upstream - $(date)" --allow-unrelated-histories; then + print_success "✅ Update completed successfully!" + + # Check for any conflicts that need manual resolution