From 73fc42bf4f8ac8f75a5437212aa047a8ad7e1737 Mon Sep 17 00:00:00 2001 From: Kunthawat Greethong Date: Fri, 5 Dec 2025 19:57:45 +0700 Subject: [PATCH] Add custom code structure and update scripts - Organized custom modifications in src/custom/ - Added update-dyad-v2.sh for selective updates - Preserved custom smart context functionality - Created backup system for safe updates --- src/custom/hooks/useSmartContext.ts | 60 +++++++ src/custom/index.ts | 18 ++ src/custom/ipc/smart_context_handlers.ts | 65 +++++++ src/custom/utils/smart_context_store.ts | 212 +++++++++++++++++++++++ update-dyad-v2.sh | 135 +++++++++++++++ 5 files changed, 490 insertions(+) create mode 100644 src/custom/hooks/useSmartContext.ts create mode 100644 src/custom/index.ts create mode 100644 src/custom/ipc/smart_context_handlers.ts create mode 100644 src/custom/utils/smart_context_store.ts create mode 100755 update-dyad-v2.sh diff --git a/src/custom/hooks/useSmartContext.ts b/src/custom/hooks/useSmartContext.ts new file mode 100644 index 0000000..85042ae --- /dev/null +++ b/src/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/src/custom/index.ts b/src/custom/index.ts new file mode 100644 index 0000000..63ec1c0 --- /dev/null +++ b/src/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/src/custom/ipc/smart_context_handlers.ts b/src/custom/ipc/smart_context_handlers.ts new file mode 100644 index 0000000..9309bcb --- /dev/null +++ b/src/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/src/custom/utils/smart_context_store.ts b/src/custom/utils/smart_context_store.ts new file mode 100644 index 0000000..560a538 --- /dev/null +++ b/src/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/update-dyad-v2.sh b/update-dyad-v2.sh new file mode 100755 index 0000000..4085bb8 --- /dev/null +++ b/update-dyad-v2.sh @@ -0,0 +1,135 @@ +#!/bin/bash + +# Dyad Update Script v2 - Selective Update Approach +# This script updates your forked Dyad app by backing up custom code, +# resetting to upstream, then restoring custom code. + +set -e # Exit on any error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if we're in a git repository +if ! git rev-parse --git-head > /dev/null 2>&1; then + print_error "Not in a git repository. Please run this script from the project root." + exit 1 +fi + +print_status "Starting Dyad selective update process..." +print_status "Current branch: $(git branch --show-current)" + +# Create a temporary backup directory +BACKUP_DIR="dyad-backup-$(date +%Y%m%d-%H%M%S)" +print_status "Creating backup in: $BACKUP_DIR" + +mkdir -p "$BACKUP_DIR" + +# Backup custom code structure +print_status "Backing up your custom code..." + +# Backup the custom directory if it exists +if [ -d "src/custom" ]; then + cp -r src/custom "$BACKUP_DIR/" + print_success "Backed up src/custom/" +fi + +# Backup any other custom files you might have added +# Add any additional files/directories you want to preserve here + +# Backup current state as a reference +print_status "Creating reference backup..." +git log --oneline -10 > "$BACKUP_DIR/commit-history.txt" +git diff HEAD~1 > "$BACKUP_DIR/last-changes.diff" 2>/dev/null || true + +# Fetch latest changes from upstream +print_status "Fetching latest changes from upstream..." +if git fetch upstream; then + print_success "Successfully fetched changes from upstream." +else + print_error "Failed to fetch from upstream. Check your internet connection." + exit 1 +fi + +# Get the upstream commit info +UPSTREAM_COMMIT=$(git rev-parse upstream/main) +print_status "Upstream commit: $UPSTREAM_COMMIT" + +# Reset to upstream +print_status "Resetting to upstream/main..." +if git reset --hard upstream/main; then + print_success "Successfully reset to upstream/main." +else + print_error "Failed to reset to upstream/main." + exit 1 +fi + +# Restore custom code +print_status "Restoring your custom code..." + +if [ -d "$BACKUP_DIR/custom" ]; then + cp -r "$BACKUP_DIR/custom" src/ + print_success "Restored src/custom/" +fi + +# Update package.json to include any custom dependencies if needed +print_status "Checking for custom dependencies..." + +# Add custom files to git +print_status "Adding custom files to git..." +git add src/custom/ UPDATE_GUIDE.md update-dyad*.sh 2>/dev/null || true + +# Create a commit for the update +print_status "Creating commit for the update..." +git commit -m "feat: update to upstream $UPSTREAM_COMMIT and restore custom code + +- Updated to latest upstream version +- Restored custom code in src/custom/ +- Preserved custom modifications + +Backup saved in: $BACKUP_DIR" || { + print_warning "No changes to commit (custom code already matches upstream)" +} + +# Push to origin +print_status "Pushing to your fork..." +if git push origin main --force-with-lease; then + print_success "Successfully pushed to origin." +else + print_warning "Push failed. Push manually with: git push origin main --force-with-lease" +fi + +print_success "🎉 Update completed successfully!" +print_status "Summary:" +print_status "- Updated to latest upstream version" +print_status "- Your custom code has been preserved in src/custom/" +print_status "- Backup saved in: $BACKUP_DIR" +print_status "" +print_status "Next steps:" +print_status "1. Test the application to ensure everything works" +print_status "2. Run 'npm install' to update dependencies if needed" +print_status "3. Check if any of your custom code needs updates for the new version" +print_status "" +print_status "If you need to restore from backup:" +print_status "1. Copy files from $BACKUP_DIR back to your project" +print_status "2. Commit and push the changes"