feat: implement custom smart context functionality with hooks, IPC handlers, and utilities
Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled
Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled
- Added custom hooks for managing smart context metadata and snippets. - Implemented IPC handlers for retrieving context, upserting snippets, and updating summaries. - Created utility functions for reading and writing smart context data to the filesystem. - Established a structured custom code organization under src/custom/ for better maintainability. - Included a comprehensive update guide to assist with future updates and custom feature integration.
This commit is contained in:
10
dyad-backup-20251218-085122/commit-history.txt
Normal file
10
dyad-backup-20251218-085122/commit-history.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
705608a Add Dyad Update Management Guide and custom hooks for smart context
|
||||||
|
73fc42b Add custom code structure and update scripts
|
||||||
|
5325549 Bump to v0.29 beta (#1895)
|
||||||
|
e0f123b Bump react (internal) (#1893)
|
||||||
|
91e8d0c Add Opus 4.5 and clean-up naming & remove legacy models for anthropic (#1892)
|
||||||
|
6235f7b Summarize chat trigger (#1890)
|
||||||
|
90c5805 adding a button for fixing all errors (#1785)
|
||||||
|
40aeed1 default to deep context (#1891)
|
||||||
|
538745d Detect external changes with deep context (#1888)
|
||||||
|
3701886 Fallback to balanced smart context for mentioned apps because not sup… (#1886)
|
||||||
60
dyad-backup-20251218-085122/custom/hooks/useSmartContext.ts
Normal file
60
dyad-backup-20251218-085122/custom/hooks/useSmartContext.ts
Normal file
@@ -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<SmartContextMeta, Error>({
|
||||||
|
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<SmartContextRetrieveResult, Error>({
|
||||||
|
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<number, Error, Array<Pick<SmartContextSnippet, "text" | "source">>>({
|
||||||
|
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<SmartContextMeta, Error, { summary: string }>({
|
||||||
|
mutationFn: async ({ summary }) => {
|
||||||
|
const ipc = IpcClient.getInstance();
|
||||||
|
return ipc.updateSmartContextRollingSummary(chatId, summary);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ["smart-context", chatId, "meta"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
18
dyad-backup-20251218-085122/custom/index.ts
Normal file
18
dyad-backup-20251218-085122/custom/index.ts
Normal file
@@ -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';
|
||||||
@@ -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<SmartContextMeta> => {
|
||||||
|
return readMeta(chatId);
|
||||||
|
});
|
||||||
|
|
||||||
|
handle(
|
||||||
|
"sc:upsert-snippets",
|
||||||
|
async (_event, params: UpsertSnippetsParams): Promise<number> => {
|
||||||
|
const count = await appendSnippets(params.chatId, params.snippets);
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
handle(
|
||||||
|
"sc:update-rolling-summary",
|
||||||
|
async (_event, params: { chatId: number; summary: string }): Promise<SmartContextMeta> => {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
212
dyad-backup-20251218-085122/custom/utils/smart_context_store.ts
Normal file
212
dyad-backup-20251218-085122/custom/utils/smart_context_store.ts
Normal file
@@ -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<void> {
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readMeta(chatId: number): Promise<SmartContextMeta> {
|
||||||
|
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<void> {
|
||||||
|
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<SmartContextMeta> {
|
||||||
|
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<SmartContextSnippet, "id" | "ts" | "tokens">[],
|
||||||
|
): Promise<number> {
|
||||||
|
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<SmartContextSnippet[]> {
|
||||||
|
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<RetrieveContextResult> {
|
||||||
|
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<void> {
|
||||||
|
// Placeholder for future embedding/vector index rebuild.
|
||||||
|
return;
|
||||||
|
}
|
||||||
617
dyad-backup-20251218-085122/last-changes.diff
Normal file
617
dyad-backup-20251218-085122/last-changes.diff
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
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<SmartContextMeta, Error>({
|
||||||
|
+ 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<SmartContextRetrieveResult, Error>({
|
||||||
|
+ 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<number, Error, Array<Pick<SmartContextSnippet, "text" | "source">>>({
|
||||||
|
+ 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<SmartContextMeta, Error, { summary: string }>({
|
||||||
|
+ 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<SmartContextMeta> => {
|
||||||
|
+ return readMeta(chatId);
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ handle(
|
||||||
|
+ "sc:upsert-snippets",
|
||||||
|
+ async (_event, params: UpsertSnippetsParams): Promise<number> => {
|
||||||
|
+ const count = await appendSnippets(params.chatId, params.snippets);
|
||||||
|
+ return count;
|
||||||
|
+ },
|
||||||
|
+ );
|
||||||
|
+
|
||||||
|
+ handle(
|
||||||
|
+ "sc:update-rolling-summary",
|
||||||
|
+ async (_event, params: { chatId: number; summary: string }): Promise<SmartContextMeta> => {
|
||||||
|
+ 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<void> {
|
||||||
|
+ await fs.mkdir(dir, { recursive: true });
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+export async function readMeta(chatId: number): Promise<SmartContextMeta> {
|
||||||
|
+ 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<void> {
|
||||||
|
+ 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<SmartContextMeta> {
|
||||||
|
+ 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<SmartContextSnippet, "id" | "ts" | "tokens">[],
|
||||||
|
+): Promise<number> {
|
||||||
|
+ 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<SmartContextSnippet[]> {
|
||||||
|
+ 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<RetrieveContextResult> {
|
||||||
|
+ 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<void> {
|
||||||
|
+ // 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
|
||||||
2700
package-lock.json
generated
2700
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -70,18 +70,18 @@
|
|||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vitest/ui": "^3.1.1",
|
"@vitest/ui": "^3.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"drizzle-kit": "^0.30.6",
|
"drizzle-kit": "^0.31.8",
|
||||||
"electron": "38.2.2",
|
"electron": "38.2.2",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"happy-dom": "^17.4.4",
|
"happy-dom": "^20.0.11",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^15.5.2",
|
"lint-staged": "^15.5.2",
|
||||||
"oxlint": "^1.8.0",
|
"oxlint": "^1.8.0",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^5.4.17",
|
"vite": "^7.3.0",
|
||||||
"vitest": "^3.1.1"
|
"vitest": "^3.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
60
src/custom/hooks/useSmartContext.ts
Normal file
60
src/custom/hooks/useSmartContext.ts
Normal file
@@ -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<SmartContextMeta, Error>({
|
||||||
|
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<SmartContextRetrieveResult, Error>({
|
||||||
|
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<number, Error, Array<Pick<SmartContextSnippet, "text" | "source">>>({
|
||||||
|
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<SmartContextMeta, Error, { summary: string }>({
|
||||||
|
mutationFn: async ({ summary }) => {
|
||||||
|
const ipc = IpcClient.getInstance();
|
||||||
|
return ipc.updateSmartContextRollingSummary(chatId, summary);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ["smart-context", chatId, "meta"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
18
src/custom/index.ts
Normal file
18
src/custom/index.ts
Normal file
@@ -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';
|
||||||
65
src/custom/ipc/smart_context_handlers.ts
Normal file
65
src/custom/ipc/smart_context_handlers.ts
Normal file
@@ -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<SmartContextMeta> => {
|
||||||
|
return readMeta(chatId);
|
||||||
|
});
|
||||||
|
|
||||||
|
handle(
|
||||||
|
"sc:upsert-snippets",
|
||||||
|
async (_event, params: UpsertSnippetsParams): Promise<number> => {
|
||||||
|
const count = await appendSnippets(params.chatId, params.snippets);
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
handle(
|
||||||
|
"sc:update-rolling-summary",
|
||||||
|
async (_event, params: { chatId: number; summary: string }): Promise<SmartContextMeta> => {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
212
src/custom/utils/smart_context_store.ts
Normal file
212
src/custom/utils/smart_context_store.ts
Normal file
@@ -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<void> {
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readMeta(chatId: number): Promise<SmartContextMeta> {
|
||||||
|
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<void> {
|
||||||
|
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<SmartContextMeta> {
|
||||||
|
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<SmartContextSnippet, "id" | "ts" | "tokens">[],
|
||||||
|
): Promise<number> {
|
||||||
|
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<SmartContextSnippet[]> {
|
||||||
|
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<RetrieveContextResult> {
|
||||||
|
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<void> {
|
||||||
|
// Placeholder for future embedding/vector index rebuild.
|
||||||
|
return;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user