5 commits 14 files changed 3 contributors Commits on Aug 26, 2025 Remove rate limit simulation from fake LLM server (#1) @pesachnps @cursoragent 3 people authored on Aug 26 Add payload size limits and truncation for chat context (#2) @pesachnps @cursoragent 3 people authored on Aug 26 Update README with detailed installation, build, and development inst… @pesachnps @cursoragent 3 people authored on Aug 26 Update GitHub repository link in PromoMessage component (#4) @pesachnps @cursoragent 3 people authored on Aug 26 Add smart context retrieval for enhanced chat memory and context (#5) @pesachnps @cursoragent 3 people authored on Aug 26 Showing with 666 additions and 66 deletions. 81 changes: 72 additions & 9 deletions81 README.md Original file line number Diff line number Diff line change @@ -2,28 +2,91 @@ Dyad is a local, open-source AI app builder. It's fast, private, and fully under your control β€” like Lovable, v0, or Bolt, but running right on your machine. [![Image](https://github.com/user-attachments/assets/f6c83dfc-6ffd-4d32-93dd-4b9c46d17790)](http://dyad.sh/) More info at: [http://dyad.sh/](http://dyad.sh/) ![Image](https://github.com/user-attachments/assets/f6c83dfc-6ffd-4d32-93dd-4b9c46d17790) ## πŸš€ Features - ⚑️ **Local**: Fast, private and no lock-in. - πŸ›  **Bring your own keys**: Use your own AI API keys β€” no vendor lock-in. - πŸ–₯️ **Cross-platform**: Easy to run on Mac or Windows. ## πŸ“¦ Download ## 🧰 Prerequisites - Node.js >= 20 - npm (comes with Node.js) - Git You can verify your versions: ```bash node -v npm -v ``` ## πŸ—οΈ Install (from source) ```bash git clone https://github.com/dyad-sh/dyad.git cd dyad npm install ``` ## ▢️ Run locally (development) - Start the app with the default configuration: ```bash npm start ``` - Optionally, point the app to a locally running engine (on http://localhost:8080/v1): ```bash npm run dev:engine ``` No sign-up required. Just download and go. ### Environment variables (optional) ### [πŸ‘‰ Download for your platform](https://www.dyad.sh/#download) - `DYAD_ENGINE_URL`: URL of the Dyad engine (defaults to built-in configuration). - `DYAD_GATEWAY_URL`: URL of a compatible gateway if you prefer to route requests. Example: ```bash DYAD_ENGINE_URL=http://localhost:8080/v1 npm start ``` ## πŸ“¦ Build installers (make) Create platform-specific distributables: ```bash npm run make ``` Outputs are written to the `out/` directory. ## πŸ§ͺ Tests and linting ```bash # Unit tests npm test # Lint npm run lint # Prettier check npm run prettier:check ``` ## 🀝 Community Join our growing community of AI app builders on **Reddit**: [r/dyadbuilders](https://www.reddit.com/r/dyadbuilders/) - share your projects and get help from the community! Join our growing community of AI app builders on **Reddit**: [r/dyadbuilders](https://www.reddit.com/r/dyadbuilders/) β€” share your projects and get help from the community! ## πŸ› οΈ Contributing **Dyad** is open-source (Apache 2.0 licensed). If you're interested in contributing to Dyad, please read our [contributing](./CONTRIBUTING.md) doc. ## πŸ“„ License If you're interested in contributing to dyad, please read our [contributing](./CONTRIBUTING.md) doc. MIT License β€” see [LICENSE](./LICENSE). 10 changes: 5 additions & 5 deletions10 package-lock.json Some generated files are not rendered by default. Learn more about how customized files appear on GitHub. 40 changes: 36 additions & 4 deletions40 src/components/HelpDialog.tsx Original file line number Diff line number Diff line change @@ -155,11 +155,43 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"} setIsUploading(true); try { // Prepare data for upload // Prepare data for upload with pruning to avoid oversized payloads const MAX_LOG_CHARS = 150_000; const MAX_CODEBASE_CHARS = 200_000; const MAX_MESSAGES = 200; // recent messages const MAX_MESSAGE_CHARS = 8_000; // per message const logs = chatLogsData.debugInfo?.logs || ""; const prunedLogs = logs.length > MAX_LOG_CHARS ? logs.slice(-MAX_LOG_CHARS) : logs; const codebase = chatLogsData.codebase || ""; const prunedCodebase = codebase.length > MAX_CODEBASE_CHARS ? codebase.slice(0, MAX_CODEBASE_CHARS) + `\n\n[Truncated codebase snippet: original length ${codebase.length}]` : codebase; const allMessages = chatLogsData.chat?.messages || []; const recentMessages = allMessages.slice(-MAX_MESSAGES).map((m) => ({ ...m, content: typeof m.content === "string" && m.content.length > MAX_MESSAGE_CHARS ? m.content.slice(0, MAX_MESSAGE_CHARS) + `\n\n[Truncated message: original length ${m.content.length}]` : m.content, })); const chatLogsJson = { systemInfo: chatLogsData.debugInfo, chat: chatLogsData.chat, codebaseSnippet: chatLogsData.codebase, systemInfo: { ...chatLogsData.debugInfo, logs: prunedLogs, }, chat: { ...chatLogsData.chat, messages: recentMessages, }, codebaseSnippet: prunedCodebase, }; // Get signed URL 2 changes: 1 addition & 1 deletion2 src/components/chat/PromoMessage.tsx Original file line number Diff line number Diff line change @@ -192,7 +192,7 @@ export const GITHUB_TIP: MessageConfig = { { type: "link", content: "GitHub", url: "https://github.com/dyad-sh/dyad", url: "https://github.com/pesachnps/dyad-without-the-limits", }, ], }; 61 changes: 61 additions & 0 deletions61 src/hooks/useSmartContext.ts Original file line number Diff line number Diff line change @@ -0,0 +1,61 @@ 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"] }); }, }); } 137 changes: 125 additions & 12 deletions137 src/ipc/handlers/chat_stream_handlers.ts Original file line number Diff line number Diff line change @@ -38,7 +38,7 @@ import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; import { readFile, writeFile, unlink } from "fs/promises"; import { getMaxTokens, getTemperature } from "../utils/token_utils"; import { getMaxTokens, getTemperature, getContextWindow } from "../utils/token_utils"; import { MAX_CHAT_TURNS_IN_CONTEXT } from "@/constants/settings_constants"; import { validateChatContext } from "../utils/context_paths_utils"; import { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"; @@ -64,11 +64,31 @@ import { parseAppMentions } from "@/shared/parse_mention_apps"; import { prompts as promptsTable } from "../../db/schema"; import { inArray } from "drizzle-orm"; import { replacePromptReference } from "../utils/replacePromptReference"; import { appendSnippets, retrieveContext as retrieveSmartContext, updateRollingSummary as updateSmartContextRollingSummary, } from "../utils/smart_context_store"; type AsyncIterableStream = AsyncIterable & ReadableStream; const logger = log.scope("chat_stream_handlers"); // Request size safeguards const MAX_CODEBASE_CHARS = 200_000; // ~200k chars for codebase context const MAX_OTHER_APPS_CODEBASE_CHARS = 200_000; // cap referenced apps section const MAX_TEXT_ATTACHMENT_CHARS = 50_000; // per text attachment const MAX_IMAGE_BYTES = 3 * 1024 * 1024; // 3MB per image function truncateWithNotice(input: string, limit: number, label: string): string { if (input.length <= limit) return input; const head = input.slice(0, limit); return ( head + `\n\n[Truncated ${label}: original length ${input.length.toLocaleString()} > limit ${limit.toLocaleString()}]` ); } // Track active streams for cancellation const activeStreams = new Map(); @@ -437,6 +457,12 @@ ${componentSnippet} ); } const cappedOtherAppsCodebaseInfo = truncateWithNotice( otherAppsCodebaseInfo, MAX_OTHER_APPS_CODEBASE_CHARS, "referenced apps codebase context", ); logger.log(`Extracted codebase information from ${appPath}`); logger.log( "codebaseInfo: length", @@ -450,6 +476,13 @@ ${componentSnippet} files, ); // Apply size caps to codebase snippets prior to prompt construction const cappedCodebaseInfo = truncateWithNotice( codebaseInfo, MAX_CODEBASE_CHARS, "codebase context", ); // Prepare message history for the AI const messageHistory = updatedChat.messages.map((message) => ({ role: message.role as "user" | "assistant" | "system", @@ -576,36 +609,72 @@ This conversation includes one or more image attachments. When the user uploads `; } const codebasePrefix = isEngineEnabled ? // No codebase prefix if engine is set, we will take of it there. [] const codebasePrefix = settings.enableProSmartFilesContextMode ? ([] as const) : ([ { role: "user", content: createCodebasePrompt(codebaseInfo), content: createCodebasePrompt(cappedCodebaseInfo), }, { role: "assistant", content: "OK, got it. I'm ready to help", content: "Thanks for the codebase. I will use it to answer your questions.", }, ] as const); const otherCodebasePrefix = otherAppsCodebaseInfo const otherCodebasePrefix = cappedOtherAppsCodebaseInfo ? ([ { role: "user", content: createOtherAppsCodebasePrompt(otherAppsCodebaseInfo), content: createOtherAppsCodebasePrompt(cappedOtherAppsCodebaseInfo), }, { role: "assistant", content: "OK.", content: "Got it. I will use the referenced apps' codebases for read-only context.", }, ] as const) : ([] as const); // Smart Context: retrieve rolling summary + top snippets within a budget const contextWindow = await getContextWindow(); // Reserve budget: 20% system/instructions, 20% output, 15% summary, rest for snippets const availableForContext = Math.floor(contextWindow * 0.45); const retrieval = await retrieveSmartContext( req.chatId, req.prompt, Math.max(0, availableForContext), ); const summaryPrefix: ModelMessage[] = retrieval.rollingSummary ? ([ { role: "user", content: `Rolling summary:\n${retrieval.rollingSummary}`, }, { role: "assistant", content: "Okay." }, ] as const) : []; : ([] as const); const snippetsText = retrieval.snippets .map((s) => `- ${s.text}`) .join("\n\n"); const snippetsPrefix: ModelMessage[] = snippetsText ? ([ { role: "user", content: `Relevant context snippets (compressed):\n${snippetsText}`, }, { role: "assistant", content: "Noted." }, ] as const) : ([] as const); let chatMessages: ModelMessage[] = [ ...codebasePrefix, ...otherCodebasePrefix, ...summaryPrefix, ...snippetsPrefix, ...limitedMessageHistory.map((msg) => ({ role: msg.role as "user" | "assistant" | "system", // Why remove thinking tags? @@ -868,6 +937,12 @@ ${problemReport.problems chatContext, virtualFileSystem, }); const cappedCodebaseInfoForContinuation = truncateWithNotice( codebaseInfo, MAX_CODEBASE_CHARS, "codebase context", ); const { modelClient } = await getModelClient( settings.selectedModel, settings, @@ -886,7 +961,7 @@ ${problemReport.problems ) { return { role: "user", content: createCodebasePrompt(codebaseInfo), content: createCodebasePrompt(cappedCodebaseInfoForContinuation), } as const; } return msg; @@ -1055,6 +1130,21 @@ ${problemReport.problems } } // Append Smart Context snippets from this turn and update rolling summary title if present try { // Add user prompt and assistant response as snippets for future retrieval await appendSnippets(req.chatId, [ { text: userPrompt, source: { type: "message" } }, { text: fullResponse, source: { type: "message" } }, ]); const summaryMatch = fullResponse.match(/(.*?)<\/dyad-chat-summary>/); if (summaryMatch && summaryMatch[1]) { await updateSmartContextRollingSummary(req.chatId, summaryMatch[1]); } } catch (e) { logger.warn("smart-context post-write failed", e); } // Return the chat ID for backwards compatibility return req.chatId; } catch (error) { @@ -1135,6 +1225,12 @@ async function replaceTextAttachmentWithContent( // Read the full content const fullContent = await readFile(filePath, "utf-8"); const contentToInclude = truncateWithNotice( fullContent, MAX_TEXT_ATTACHMENT_CHARS, `attachment ${fileName}`, ); // Replace the placeholder tag with the full content const escapedPath = filePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const tagPattern = new RegExp( @@ -1144,7 +1240,7 @@ async function replaceTextAttachmentWithContent( const replacedText = text.replace( tagPattern, `Full content of ${fileName}:\n\`\`\`\n${fullContent}\n\`\`\``, `Full content of ${fileName}:\n\`\`\`\n${contentToInclude}\n\`\`\``, ); logger.log( @@ -1193,10 +1289,17 @@ async function prepareMessageWithAttachments( }); // Add image parts for any image attachments const omittedImages: string[] = []; for (const filePath of attachmentPaths) { const ext = path.extname(filePath).toLowerCase(); if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].includes(ext)) { try { // Check size before reading const stats = await fs.promises.stat(filePath); if (stats.size > MAX_IMAGE_BYTES) { omittedImages.push(path.basename(filePath)); continue; } // Read the file as a buffer const imageBuffer = await readFile(filePath); @@ -1213,6 +1316,16 @@ async function prepareMessageWithAttachments( } } if (omittedImages.length > 0) { contentParts.push({ type: "text", text: `Note: ${omittedImages.length} image(s) omitted due to size limits (>${Math.floor( MAX_IMAGE_BYTES / (1024 * 1024), )}MB): ` + omittedImages.join(", "), }); } // Return the message with the content array return { role: "user", 66 changes: 66 additions & 0 deletions66 src/ipc/handlers/smart_context_handlers.ts Original file line number Diff line number Diff line change @@ -0,0 +1,66 @@ 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; }); } 38 changes: 38 additions & 0 deletions38 src/ipc/ipc_client.ts Original file line number Diff line number Diff line change @@ -65,6 +65,12 @@ import type { import type { Template } from "../shared/templates"; import type { AppChatContext, ProposalResult } from "@/lib/schemas"; import { showError } from "@/lib/toast"; import type { SmartContextMeta, SmartContextSnippet, SmartContextRetrieveResult, RetrieveSmartContextParams, } from "./ipc_types"; export interface ChatStreamCallbacks { onUpdate: (messages: Message[]) => void; @@ -240,6 +246,38 @@ export class IpcClient { return IpcClient.instance; } // --- Smart Context --- public async getSmartContextMeta(chatId: number): Promise { return this.ipcRenderer.invoke("sc:get-meta", chatId); } public async upsertSmartContextSnippets( chatId: number, snippets: Array>, ): Promise { return this.ipcRenderer.invoke("sc:upsert-snippets", { chatId, snippets }); } public async updateSmartContextRollingSummary( chatId: number, summary: string, ): Promise { return this.ipcRenderer.invoke("sc:update-rolling-summary", { chatId, summary, }); } public async retrieveSmartContext( params: RetrieveSmartContextParams, ): Promise { return this.ipcRenderer.invoke("sc:retrieve-context", params); } public async rebuildSmartContextIndex(chatId: number): Promise { await this.ipcRenderer.invoke("sc:rebuild-index", chatId); } public async restartDyad(): Promise { await this.ipcRenderer.invoke("restart-dyad"); } 2 changes: 2 additions & 0 deletions2 src/ipc/ipc_host.ts Original file line number Diff line number Diff line change @@ -30,6 +30,7 @@ import { registerTemplateHandlers } from "./handlers/template_handlers"; import { registerPortalHandlers } from "./handlers/portal_handlers"; import { registerPromptHandlers } from "./handlers/prompt_handlers"; import { registerHelpBotHandlers } from "./handlers/help_bot_handlers"; import { registerSmartContextHandlers } from "./handlers/smart_context_handlers"; export function registerIpcHandlers() { // Register all IPC handlers by category @@ -65,4 +66,5 @@ export function registerIpcHandlers() { registerPortalHandlers(); registerPromptHandlers(); registerHelpBotHandlers(); registerSmartContextHandlers(); } 40 changes: 40 additions & 0 deletions40 src/ipc/ipc_types.ts Original file line number Diff line number Diff line change @@ -354,6 +354,46 @@ export interface UploadFileToCodebaseResult { filePath: string; } // --- Smart Context Types --- 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; tokens?: number; } export interface SmartContextMetaConfig { maxSnippets?: number; } export interface SmartContextMeta { entityId: string; updatedAt: number; rollingSummary?: string; summaryTokens?: number; config?: SmartContextMetaConfig; } export interface RetrieveSmartContextParams { chatId: number; query: string; budgetTokens: number; } export interface SmartContextRetrieveResult { rollingSummary?: string; usedTokens: number; snippets: SmartContextSnippet[]; } // --- Prompts --- export interface PromptDto { id: number; 213 changes: 213 additions & 0 deletions213 src/ipc/utils/smart_context_store.ts Original file line number Diff line number Diff line change @@ -0,0 +1,213 @@ 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; } 6 changes: 6 additions & 0 deletions6 src/preload.ts Original file line number Diff line number Diff line change @@ -106,6 +106,12 @@ const validInvokeChannels = [ "restart-dyad", "get-templates", "portal:migrate-create", // Smart Context "sc:get-meta", "sc:upsert-snippets", "sc:update-rolling-summary", "sc:retrieve-context", "sc:rebuild-index", // Help bot "help:chat:start", "help:chat:cancel", 24 changes: 0 additions & 24 deletions24 testing/fake-llm-server/README.md Original file line number Diff line number Diff line change @@ -7,7 +7,6 @@ A simple server that mimics the OpenAI streaming chat completions API for testin - Implements a basic version of the OpenAI chat completions API - Supports both streaming and non-streaming responses - Always responds with "hello world" message - Simulates a 429 rate limit error when the last message is "[429]" - Configurable through environment variables ## Installation @@ -83,29 +82,6 @@ For non-streaming requests, you'll get a standard JSON response: For streaming requests, you'll receive a series of server-sent events (SSE), each containing a chunk of the response. ### Simulating Rate Limit Errors To test how your application handles rate limiting, send a message with content exactly equal to `[429]`: ```json { "messages": [{ "role": "user", "content": "[429]" }], "model": "any-model" } ``` This will return a 429 status code with the following response: ```json { "error": { "message": "Too many requests. Please try again later.", "type": "rate_limit_error", "param": null, "code": "rate_limit_exceeded" } } ``` ## Configuration 12 changes: 1 addition & 11 deletions12 testing/fake-llm-server/chatCompletionHandler.ts Original file line number Diff line number Diff line change @@ -10,18 +10,8 @@ export const createChatCompletionHandler = const { stream = false, messages = [] } = req.body; console.log("* Received messages", messages); // Check if the last message contains "[429]" to simulate rate limiting // Disabled rate limit simulation const lastMessage = messages[messages.length - 1]; if (lastMessage && lastMessage.content === "[429]") { return res.status(429).json({ error: { message: "Too many requests. Please try again later.", type: "rate_limit_error", param: null, code: "rate_limit_exceeded", }, }); } let messageContent = CANNED_MESSAGE; Footer