diff --git a/README.md b/README.md index 67d23ee..f937d87 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ OpenGUI is for people who like coding agents but want stronger workflow than ter - **Prompt queue** that auto-dispatches when assistant becomes idle - **Model, backend, and agent selection** directly from chat workflow - **Slash commands** from prompt box -- **Syntax highlighting + math rendering** with Shiki and KaTeX +- **Syntax highlighting** with Shiki - **Dark/light theme** with system-aware toggle - **Desktop, web, and Docker deployment options** - **Cross-platform builds** for Linux, macOS, and Windows diff --git a/bun.lock b/bun.lock index 6f074f2..d479769 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,6 @@ "embla-carousel-react": "^8.6.0", "i18next": "^25.6.3", "input-otp": "^1.4.2", - "katex": "^0.16.45", "lucide-react": "^1.14.0", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", @@ -40,9 +39,7 @@ "react-markdown": "^10.1.0", "react-resizable-panels": "^4.11.0", "recharts": "3.8.1", - "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "vaul": "^1.1.2", @@ -840,7 +837,6 @@ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "3.0.3" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "3.0.3" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -1284,7 +1280,6 @@ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "2.0.1", "safe-buffer": "5.2.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], "koffi": ["koffi@2.16.2", "", {}, "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA=="], @@ -1350,7 +1345,6 @@ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "4.0.4", "devlop": "1.1.0", "mdast-util-from-markdown": "2.0.3", "mdast-util-to-markdown": "2.1.2" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "3.0.4", "@types/mdast": "4.0.4", "devlop": "1.1.0", "longest-streak": "3.1.0", "mdast-util-from-markdown": "2.0.3", "mdast-util-to-markdown": "2.1.2", "unist-util-remove-position": "5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "1.0.5", "@types/hast": "3.0.4", "@types/mdast": "4.0.4", "devlop": "1.1.0", "mdast-util-from-markdown": "2.0.3", "mdast-util-to-markdown": "2.1.2" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], @@ -1388,7 +1382,6 @@ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "1.1.0", "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "0.16.8", "devlop": "1.1.0", "katex": "0.16.45", "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], @@ -1578,11 +1571,9 @@ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "3.0.4", "@types/katex": "0.16.8", "hast-util-from-html-isomorphic": "2.0.0", "hast-util-to-text": "4.0.2", "katex": "0.16.45", "unist-util-visit-parents": "6.0.2", "vfile": "6.0.3" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "4.0.4", "mdast-util-gfm": "3.1.0", "micromark-extension-gfm": "3.0.0", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "4.0.4", "mdast-util-math": "3.0.0", "micromark-extension-math": "3.1.0", "unified": "11.0.5" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "4.0.4", "mdast-util-from-markdown": "2.0.3", "micromark-util-types": "2.0.2", "unified": "11.0.5" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], diff --git a/opencode-bridge.ts b/opencode-bridge.ts index 325434d..7fb72ae 100644 --- a/opencode-bridge.ts +++ b/opencode-bridge.ts @@ -1514,7 +1514,7 @@ export function setupOpenCodeBridge(ipcMain, _getWindows) { }; } } - console.log( + console.info( `[opencode-bridge] Resolved binary: ${binary ?? "(not found)"} (platform: ${process.platform})`, ); if (!binary) { @@ -1528,7 +1528,7 @@ export function setupOpenCodeBridge(ipcMain, _getWindows) { // Spawn detached so the server survives app close. // Use piped stdio so we can capture logs on startup failure. const serverArgs = ["serve", "--port", String(LOCAL_SERVER_PORT)]; - console.log( + console.info( `[opencode-bridge] Spawning: ${binary} ${serverArgs.join(" ")} (platform: ${process.platform})`, ); @@ -1570,7 +1570,7 @@ export function setupOpenCodeBridge(ipcMain, _getWindows) { }); // Wait for the server to become healthy - console.log( + console.info( `[opencode-bridge] Waiting for server to become healthy (timeout: ${STARTUP_TIMEOUT / 1000}s)...`, ); try { @@ -1599,7 +1599,7 @@ export function setupOpenCodeBridge(ipcMain, _getWindows) { child.stderr.removeAllListeners("data"); child.stderr.destroy(); } - console.log("[opencode-bridge] Server became healthy after launcher exited."); + console.info("[opencode-bridge] Server became healthy after launcher exited."); return { success: true, data: { alreadyRunning: false } }; } @@ -1641,7 +1641,7 @@ export function setupOpenCodeBridge(ipcMain, _getWindows) { child.stderr.destroy(); } - console.log("[opencode-bridge] Server is healthy."); + console.info("[opencode-bridge] Server is healthy."); return { success: true, data: { alreadyRunning: false } }; } catch (err) { return { success: false, error: err.message ?? String(err) }; diff --git a/package.json b/package.json index a6769c1..a86a2a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opengui", - "version": "0.5.8", + "version": "0.5.9", "private": false, "description": "OpenGUI - Desktop and web command center for coding agents", "homepage": "https://github.com/akemmanuel/OpenGUI", @@ -29,7 +29,7 @@ "@hookform/resolvers": "^5.2.2", "@mariozechner/jiti": "^2.6.5", "@openai/codex-sdk": "^0.130.0", - "@opencode-ai/sdk": "^1.14.46", + "@opencode-ai/sdk": "^1.14.48", "@tanstack/react-virtual": "^3.13.24", "@wooorm/starry-night": "^3.9.0", "class-variance-authority": "^0.7.1", @@ -39,9 +39,8 @@ "electron-updater": "^6.8.3", "embla-carousel-react": "^8.6.0", "hast-util-to-jsx-runtime": "^2.3.6", - "i18next": "^26.0.10", + "i18next": "^26.1.0", "input-otp": "^1.4.2", - "katex": "^0.16.45", "lucide-react": "^1.14.0", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", @@ -53,9 +52,7 @@ "react-markdown": "^10.1.0", "react-resizable-panels": "^4.11.0", "recharts": "3.8.1", - "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0", "sonner": "^2.0.7", "streamdown": "^2.5.0", "tailwind-merge": "^3.6.0", @@ -71,11 +68,12 @@ "@vitejs/plugin-react": "^6.0.1", "bun-plugin-tailwind": "^0.1.2", "electron": "^42.0.1", + "esbuild": "^0.28.0", "oxlint": "^1.63.0", "oxlint-tsgolint": "^0.22.1", "tailwindcss": "^4.3.0", "tw-animate-css": "^1.4.0", - "vite": "^8.0.11", + "vite": "^8.0.12", "vite-plus": "^0.1.20" }, "packageManager": "pnpm@10.33.4", diff --git a/pi-bridge.ts b/pi-bridge.ts index 2011a0e..59b1c6f 100644 --- a/pi-bridge.ts +++ b/pi-bridge.ts @@ -2,13 +2,12 @@ import { execFile as execFileCallback, spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import { createServer as createNetServer } from "node:net"; -import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { existsSync } from "node:fs"; -import { mkdir, readFile, realpath, unlink, writeFile } from "node:fs/promises"; +import { mkdir, readFile, unlink, writeFile } from "node:fs/promises"; import { getSupportedThinkingLevels } from "@earendil-works/pi-ai"; import { SessionManager, @@ -52,26 +51,7 @@ const PI_DAEMON_HEALTH_TIMEOUT = 2_000; // Bump when daemon import/runtime behavior changes. Existing healthy daemon gets reused // across app restarts; failed lazy ESM imports inside pi-ai stay poisoned in-process. const PI_DAEMON_VERSION = "2026-05-08-pi-message-id-reconcile-v1"; -const FRESH_PI_MODELS_TTL_MS = 60_000; -const MAX_FRESH_PI_MODELS_CACHE_ENTRIES = 64; const __dirname = dirname(fileURLToPath(import.meta.url)); -const freshPiModelsCache = new Map(); - -function setFreshPiModelsCache(key, value) { - const now = Date.now(); - for (const [cacheKey, cached] of freshPiModelsCache.entries()) { - if (!cached || now - cached.time >= FRESH_PI_MODELS_TTL_MS) { - freshPiModelsCache.delete(cacheKey); - } - } - if (freshPiModelsCache.has(key)) freshPiModelsCache.delete(key); - freshPiModelsCache.set(key, value); - while (freshPiModelsCache.size > MAX_FRESH_PI_MODELS_CACHE_ENTRIES) { - const oldestKey = freshPiModelsCache.keys().next().value; - if (oldestKey === undefined) break; - freshPiModelsCache.delete(oldestKey); - } -} function normalizeDir(directory) { if (typeof directory !== "string") return ""; @@ -313,199 +293,6 @@ function normalizePiModel(model) { }; } -function parsePiTokenCount(value) { - const text = String(value || "") - .trim() - .toUpperCase(); - if (!text) return 0; - const match = text.match(/^(\d+(?:\.\d+)?)([KM])?$/); - if (!match) return Number.parseInt(text.replace(/\D/g, ""), 10) || 0; - const amount = Number.parseFloat(match[1]); - const unit = match[2]; - if (unit === "M") return Math.round(amount * 1_000_000); - if (unit === "K") return Math.round(amount * 1_000); - return Math.round(amount); -} - -function inferApiForProvider(provider, template) { - if (template?.api) return template.api; - if (provider.includes("anthropic")) return "anthropic-messages"; - if (provider.includes("google") || provider.includes("gemini")) return "google-generative-ai"; - if (provider.includes("codex")) return "openai-codex-responses"; - if (provider.includes("azure")) return "azure-openai-responses"; - if (provider.includes("openai")) return "openai-responses"; - return "openai-completions"; -} - -function inferBaseUrlForProvider(provider, template) { - if (template?.baseUrl) return template.baseUrl; - if (provider.includes("anthropic")) return "https://api.anthropic.com"; - if (provider.includes("google") || provider.includes("gemini")) - return "https://generativelanguage.googleapis.com/v1beta"; - if (provider.includes("codex")) return "https://chatgpt.com/backend-api"; - if (provider.includes("openai")) return "https://api.openai.com"; - return ""; -} - -function parsePiListModelsTable(text, referenceModels = []) { - const referencesByProvider = new Map(); - for (const model of referenceModels) { - if (!referencesByProvider.has(model.provider)) referencesByProvider.set(model.provider, model); - } - const models = []; - for (const rawLine of String(text || "").split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line || line.startsWith("provider") || line.startsWith("─") || line.startsWith("-")) - continue; - const parts = line.split(/\s{2,}/).filter(Boolean); - if (parts.length < 6) continue; - const [provider, id, context, maxOut, thinking, images] = parts; - if (!provider || !id || provider === "provider") continue; - const template = referencesByProvider.get(provider); - models.push({ - id, - name: id, - api: inferApiForProvider(provider, template), - provider, - baseUrl: inferBaseUrlForProvider(provider, template), - reasoning: thinking === "yes", - input: images === "yes" ? ["text", "image"] : ["text"], - cost: template?.cost || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: parsePiTokenCount(context), - maxTokens: parsePiTokenCount(maxOut), - headers: template?.headers || {}, - compat: template?.compat, - }); - } - return models; -} - -function mergeModels(primaryModels, discoveredModels) { - const byKey = new Map(); - for (const model of primaryModels) byKey.set(`${model.provider}/${model.id}`, model); - for (const model of discoveredModels) { - const key = `${model.provider}/${model.id}`; - const existing = byKey.get(key); - byKey.set( - key, - existing - ? { - ...existing, - ...model, - thinkingLevelMap: model.thinkingLevelMap ?? existing.thinkingLevelMap, - compat: model.compat ?? existing.compat, - headers: - model.headers && Object.keys(model.headers).length > 0 - ? model.headers - : existing.headers, - } - : model, - ); - } - return Array.from(byKey.values()); -} - -function localPiCandidates() { - const candidates = []; - if (process.env.OPENGUI_PI_BINARY) candidates.push(process.env.OPENGUI_PI_BINARY); - candidates.push(join(homedir(), ".bun", "bin", process.platform === "win32" ? "pi.cmd" : "pi")); - candidates.push(join(homedir(), ".pi", "bin", process.platform === "win32" ? "pi.cmd" : "pi")); - candidates.push("pi"); - return [...new Set(candidates)]; -} - -function localBunCandidates() { - const candidates = []; - if (process.env.OPENGUI_BUN_BINARY) candidates.push(process.env.OPENGUI_BUN_BINARY); - if (process.env.BUN) candidates.push(process.env.BUN); - candidates.push(join(homedir(), ".bun", "bin", process.platform === "win32" ? "bun.exe" : "bun")); - candidates.push("bun"); - return [...new Set(candidates)]; -} - -async function resolvePiListModelsCommand(binary) { - const args = ["--list-models"]; - if (!binary.includes("/") && !binary.includes("\\")) { - return { command: binary, args }; - } - let resolved = binary; - try { - resolved = await realpath(binary); - } catch { - return { command: binary, args }; - } - if (!resolved.endsWith(".js") && !resolved.endsWith(".mjs") && !resolved.endsWith(".cjs")) { - return { command: binary, args }; - } - for (const bun of localBunCandidates()) { - try { - return { command: bun, args: [resolved, ...args] }; - } catch { - // Try next Bun candidate. - } - break; - } - return { command: binary, args }; -} - -async function discoverLocalPiModels(cwd, referenceModels = []) { - const cacheKey = `${cwd || process.cwd()}:${referenceModels.length}`; - const cached = freshPiModelsCache.get(cacheKey); - if (cached && Date.now() - cached.time < FRESH_PI_MODELS_TTL_MS) return cached.models; - let lastError = null; - for (const binary of localPiCandidates()) { - try { - const pathParts = [ - join(homedir(), ".bun", "bin"), - "/opt/homebrew/bin", - "/usr/local/bin", - "/usr/bin", - "/bin", - process.env.PATH || "", - ].filter(Boolean); - const resolvedCommand = await resolvePiListModelsCommand(binary); - if (process.env.OPENGUI_PI_DEBUG) { - console.warn( - "Pi model discovery candidate:", - binary, - "->", - resolvedCommand.command, - resolvedCommand.args.join(" "), - ); - } - const { stdout, stderr } = await execFile(resolvedCommand.command, resolvedCommand.args, { - cwd: cwd || process.cwd(), - env: { - ...process.env, - PATH: [...new Set(pathParts.join(":").split(":"))].join(":"), - PI_SKIP_VERSION_CHECK: "1", - PI_OFFLINE: "1", - }, - maxBuffer: 8 * 1024 * 1024, - timeout: 15_000, - }); - const models = parsePiListModelsTable(`${stdout}\n${stderr}`, referenceModels); - if (process.env.OPENGUI_PI_DEBUG) { - console.warn("Pi model discovery result:", binary, models.length); - } - if (models.length > 0) { - setFreshPiModelsCache(cacheKey, { time: Date.now(), models }); - return models; - } - } catch (error) { - if (process.env.OPENGUI_PI_DEBUG) { - console.warn("Pi model discovery failed:", binary, error); - } - lastError = error; - } - } - if (lastError && process.env.OPENGUI_PI_DEBUG) { - console.warn("Failed to auto-discover local Pi models:", lastError); - } - setFreshPiModelsCache(cacheKey, { time: Date.now(), models: [] }); - return []; -} - function buildProvidersData(models) { const providers = new Map(); const defaults = {}; @@ -1898,7 +1685,6 @@ export class PiBridgeManager { return sessions.find((session) => session.id === sessionId) || null; } - // legacy project-level session switch removed resolveRealMessageId(project, sessionId, messageId) { const state = this.getSyntheticState(project, sessionId); return state.syntheticToReal.get(messageId) || messageId; @@ -2084,12 +1870,7 @@ export class PiBridgeManager { if (!selectedModel?.providerID || !selectedModel?.modelID) return; session.modelRegistry.refresh?.(); const availableModels = session.modelRegistry.getAvailable(); - const knownModels = session.modelRegistry.getAll(); - const discoveredModels = await discoverLocalPiModels( - session.sessionManager.getCwd?.(), - knownModels, - ); - const model = mergeModels(availableModels, discoveredModels).find( + const model = availableModels.find( (item) => item.provider === selectedModel.providerID && item.id === selectedModel.modelID, ); if (!model) { @@ -2308,9 +2089,7 @@ export class PiBridgeManager { } runtime.services.modelRegistry.refresh?.(); const availableModels = runtime.services.modelRegistry.getAvailable(); - const knownModels = runtime.services.modelRegistry.getAll(); - const discoveredModels = await discoverLocalPiModels(project.directory, knownModels); - return buildProvidersData(mergeModels(availableModels, discoveredModels)); + return buildProvidersData(availableModels); } const models = []; for (const project of this.projects.values()) { @@ -2319,13 +2098,7 @@ export class PiBridgeManager { if (!runtime) continue; runtime.services.modelRegistry.refresh?.(); const availableModels = runtime.services.modelRegistry.getAvailable(); - const knownModels = runtime.services.modelRegistry.getAll(); - models.push( - ...mergeModels( - availableModels, - await discoverLocalPiModels(project.directory, knownModels), - ), - ); + models.push(...availableModels); } return buildProvidersData(models); } @@ -2801,9 +2574,6 @@ class PiDaemonClient { export function setupPiBridge(ipcMain, getAllWindows, options = {}) { const manager = new PiDaemonClient(getAllWindows, options); - void manager.ensureDaemon().catch((error) => { - console.error("Failed to start Pi daemon:", error); - }); ipcMain.handle("pi:project:add", async (_event, config) => { try { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 500a3f1..f4af6f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^0.130.0 version: 0.130.0 '@opencode-ai/sdk': - specifier: ^1.14.46 - version: 1.14.46 + specifier: ^1.14.48 + version: 1.14.48 '@tanstack/react-virtual': specifier: ^3.13.24 version: 3.13.24(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -75,14 +75,11 @@ importers: specifier: ^2.3.6 version: 2.3.6 i18next: - specifier: ^26.0.10 - version: 26.0.10 + specifier: ^26.1.0 + version: 26.1.0 input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - katex: - specifier: ^0.16.45 - version: 0.16.45 lucide-react: specifier: ^1.14.0 version: 1.14.0(react@19.2.6) @@ -106,7 +103,7 @@ importers: version: 7.75.0(react@19.2.6) react-i18next: specifier: ^17.0.7 - version: 17.0.7(i18next@26.0.10)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 17.0.7(i18next@26.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.6) @@ -116,15 +113,9 @@ importers: recharts: specifier: 3.8.1 version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.5)(react@19.2.6)(redux@5.0.1) - rehype-katex: - specifier: ^7.0.1 - version: 7.0.1 remark-gfm: specifier: ^4.0.1 version: 4.0.1 - remark-math: - specifier: ^6.0.0 - version: 6.0.0 sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -146,7 +137,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.3.0 - version: 4.3.0(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4)) + version: 4.3.0(vite@8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0)) '@types/bun': specifier: ^1.3.13 version: 1.3.13 @@ -158,13 +149,16 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4)) + version: 6.0.1(vite@8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0)) bun-plugin-tailwind: specifier: ^0.1.2 version: 0.1.2(bun@1.3.13) electron: specifier: ^42.0.1 version: 42.0.1 + esbuild: + specifier: ^0.28.0 + version: 0.28.0 oxlint: specifier: ^1.63.0 version: 1.63.0(oxlint-tsgolint@0.22.1) @@ -178,11 +172,11 @@ importers: specifier: ^1.4.0 version: 1.4.0 vite: - specifier: ^8.0.11 - version: 8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4) + specifier: ^8.0.12 + version: 8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0) vite-plus: specifier: ^0.1.20 - version: 0.1.20(@types/node@25.6.2)(jiti@2.7.0)(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4))(yaml@2.8.4) + version: 0.1.20(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(vite@8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0) packages: @@ -443,20 +437,8 @@ packages: '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - '@chevrotain/cst-dts-gen@12.0.0': - resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} - - '@chevrotain/gast@12.0.0': - resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} - - '@chevrotain/regexp-to-ast@12.0.0': - resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} - - '@chevrotain/types@12.0.0': - resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} - - '@chevrotain/utils@12.0.0': - resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} @@ -514,6 +496,162 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -643,8 +781,8 @@ packages: resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} hasBin: true - '@mermaid-js/parser@1.1.0': - resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} + '@mermaid-js/parser@1.1.1': + resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} '@mistralai/mistralai@2.2.1': resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==} @@ -713,8 +851,8 @@ packages: cpu: [x64] os: [win32] - '@opencode-ai/sdk@1.14.46': - resolution: {integrity: sha512-7KOMuoCkNI+bLOw3GCg0nWZ5m7A/MzNsyLfTbZYmE/DIaUqkV2LNRULtrW6PHL1WtYVmJEFPws4dbw/4dVxjzA==} + '@opencode-ai/sdk@1.14.48': + resolution: {integrity: sha512-wKM86jCzV/ZApyWrdm3uP8XdWcS0LMbu3FV+OWz1ChiGGg1wiIWNGMJs5CY8/QX2/rUuZrd1Q1DqvdamZ0zLeg==} '@oven/bun-darwin-aarch64@1.3.13': resolution: {integrity: sha512-qAS6Hg8Q14ckfBuqJ2Zh7gBQSVSUHeibSq4OFqBTv6DzyJuxYlr0sdYQzmYmnbPxbqobekqUDTa/4XEaqRi7vg==} @@ -783,8 +921,8 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - '@oxc-project/types@0.128.0': - resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + '@oxc-project/types@0.129.0': + resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==} '@oxfmt/binding-android-arm-eabi@0.46.0': resolution: {integrity: sha512-b1doV4WRcJU+BESSlCvCjV+5CEr/T6h0frArAdV26Nir+gGNFNaylvDiiMPfF1pxeV0txZEs38ojzJaxBYg+ng==} @@ -1968,103 +2106,103 @@ packages: react-redux: optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==} + '@rolldown/binding-android-arm64@1.0.0': + resolution: {integrity: sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==} + '@rolldown/binding-darwin-arm64@1.0.0': + resolution: {integrity: sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.18': - resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==} + '@rolldown/binding-darwin-x64@1.0.0': + resolution: {integrity: sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.18': - resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==} + '@rolldown/binding-freebsd-x64@1.0.0': + resolution: {integrity: sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': - resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0': + resolution: {integrity: sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==} + '@rolldown/binding-linux-arm64-gnu@1.0.0': + resolution: {integrity: sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': - resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} + '@rolldown/binding-linux-arm64-musl@1.0.0': + resolution: {integrity: sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0': + resolution: {integrity: sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} + '@rolldown/binding-linux-s390x-gnu@1.0.0': + resolution: {integrity: sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} + '@rolldown/binding-linux-x64-gnu@1.0.0': + resolution: {integrity: sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': - resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} + '@rolldown/binding-linux-x64-musl@1.0.0': + resolution: {integrity: sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} + '@rolldown/binding-openharmony-arm64@1.0.0': + resolution: {integrity: sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': - resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==} + '@rolldown/binding-wasm32-wasi@1.0.0': + resolution: {integrity: sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': - resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0': + resolution: {integrity: sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': - resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==} + '@rolldown/binding-win32-x64-msvc@1.0.0': + resolution: {integrity: sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.18': - resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==} + '@rolldown/pluginutils@1.0.0': + resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==} '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} @@ -2499,9 +2637,6 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/katex@0.16.8': - resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -2844,15 +2979,6 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - chevrotain-allstar@0.4.3: - resolution: {integrity: sha512-2X4mkroolSMKqW+H22pyPMUVDqYZzPhephTmg/NODKb1IGYPHfxfhcW0EjS7wcPJNbze2i4vBWT7zT5FKF2lrQ==} - peerDependencies: - chevrotain: ^12.0.0 - - chevrotain@12.0.0: - resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} - engines: {node: '>=22.0.0'} - class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -3213,6 +3339,11 @@ packages: es-toolkit@1.46.1: resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3409,21 +3540,9 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} - hast-util-from-dom@5.0.1: - resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} - - hast-util-from-html-isomorphic@2.0.0: - resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} - - hast-util-from-html@2.0.3: - resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} - hast-util-from-parse5@8.0.3: resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} - hast-util-is-element@3.0.0: - resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -3439,9 +3558,6 @@ packages: hast-util-to-parse5@8.0.1: resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} - hast-util-to-text@4.0.2: - resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} - hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -3480,8 +3596,8 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - i18next@26.0.10: - resolution: {integrity: sha512-k3yGPAlWR2RdMYoVXJoDZDT87qeHIWKH7gVksdZMpRty7QX/D9QZeYGvN08KGbKHke9wn01eYT+EEsrqX/YTlw==} + i18next@26.1.0: + resolution: {integrity: sha512-dIU6td04DvQuIqVst5S9g0GviTmhZ0DYD4b9ociVGJmuCa5vZ2de/t+Enf4olvj87mF8Y2lwjNQBwC9QZsvzKQ==} peerDependencies: typescript: ^5 || ^6 peerDependenciesMeta: @@ -3608,10 +3724,6 @@ packages: koffi@2.16.2: resolution: {integrity: sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==} - langium@4.2.3: - resolution: {integrity: sha512-sOPIi4hISFnY7twwV97ca1TsxpBtXq0URu/LL1AvxwccPG/RIBBlKS7a/f/EL6w8lTNaS0EFs/F+IdSOaqYpng==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -3773,9 +3885,6 @@ packages: mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} - mdast-util-math@3.0.0: - resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} - mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -3805,8 +3914,8 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} - mermaid@11.14.0: - resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} + mermaid@11.15.0: + resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -3832,9 +3941,6 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - micromark-extension-math@3.1.0: - resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} - micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} @@ -4265,9 +4371,6 @@ packages: rehype-harden@1.1.8: resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} - rehype-katex@7.0.1: - resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} - rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} @@ -4277,9 +4380,6 @@ packages: remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} - remark-math@6.0.0: - resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} - remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -4314,8 +4414,8 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.0.0-rc.18: - resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==} + rolldown@1.0.0: + resolution: {integrity: sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4571,18 +4671,12 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unist-util-find-after@5.0.0: - resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} - unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - unist-util-remove-position@5.0.0: - resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} - unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -4625,10 +4719,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} - hasBin: true - uuid@14.0.0: resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true @@ -4660,8 +4750,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - vite@8.0.11: - resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} + vite@8.0.12: + resolution: {integrity: sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4707,32 +4797,12 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-textdocument@1.0.12: - resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - vscode-oniguruma@2.0.1: resolution: {integrity: sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==} vscode-textmate@9.3.2: resolution: {integrity: sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q==} - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -4772,8 +4842,8 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - yaml@2.8.4: - resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true @@ -5316,20 +5386,7 @@ snapshots: '@braintree/sanitize-url@7.1.2': {} - '@chevrotain/cst-dts-gen@12.0.0': - dependencies: - '@chevrotain/gast': 12.0.0 - '@chevrotain/types': 12.0.0 - - '@chevrotain/gast@12.0.0': - dependencies: - '@chevrotain/types': 12.0.0 - - '@chevrotain/regexp-to-ast@12.0.0': {} - - '@chevrotain/types@12.0.0': {} - - '@chevrotain/utils@12.0.0': {} + '@chevrotain/types@11.1.2': {} '@date-fns/tz@1.4.1': {} @@ -5415,7 +5472,7 @@ snapshots: typebox: 1.1.38 undici: 7.25.0 uuid: 14.0.0 - yaml: 2.8.4 + yaml: 2.9.0 optionalDependencies: '@mariozechner/clipboard': 0.3.5 transitivePeerDependencies: @@ -5466,6 +5523,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -5581,9 +5716,9 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mermaid-js/parser@1.1.0': + '@mermaid-js/parser@1.1.1': dependencies: - langium: 4.2.3 + '@chevrotain/types': 11.1.2 '@mistralai/mistralai@2.2.1': dependencies: @@ -5656,7 +5791,7 @@ snapshots: '@openai/codex@0.130.0-win32-x64': optional: true - '@opencode-ai/sdk@1.14.46': + '@opencode-ai/sdk@1.14.48': dependencies: cross-spawn: 7.0.6 @@ -5700,7 +5835,7 @@ snapshots: '@oxc-project/types@0.127.0': {} - '@oxc-project/types@0.128.0': {} + '@oxc-project/types@0.129.0': {} '@oxfmt/binding-android-arm-eabi@0.46.0': optional: true @@ -6709,56 +6844,56 @@ snapshots: react: 19.2.6 react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) - '@rolldown/binding-android-arm64@1.0.0-rc.18': + '@rolldown/binding-android-arm64@1.0.0': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + '@rolldown/binding-darwin-arm64@1.0.0': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.18': + '@rolldown/binding-darwin-x64@1.0.0': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + '@rolldown/binding-freebsd-x64@1.0.0': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-arm64-gnu@1.0.0': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + '@rolldown/binding-linux-arm64-musl@1.0.0': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-ppc64-gnu@1.0.0': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-s390x-gnu@1.0.0': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-x64-gnu@1.0.0': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + '@rolldown/binding-linux-x64-musl@1.0.0': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + '@rolldown/binding-openharmony-arm64@1.0.0': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + '@rolldown/binding-wasm32-wasi@1.0.0': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + '@rolldown/binding-win32-arm64-msvc@1.0.0': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + '@rolldown/binding-win32-x64-msvc@1.0.0': optional: true - '@rolldown/pluginutils@1.0.0-rc.18': {} + '@rolldown/pluginutils@1.0.0': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -7129,12 +7264,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - '@tailwindcss/vite@4.3.0(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4))': + '@tailwindcss/vite@4.3.0(vite@8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4) + vite: 8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0) '@tanstack/react-virtual@3.13.24(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -7304,8 +7439,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/katex@0.16.8': {} - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -7353,12 +7486,12 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vitejs/plugin-react@6.0.1(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4))': + '@vitejs/plugin-react@6.0.1(vite@8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4) + vite: 8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0) - '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4)': + '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0)': dependencies: '@oxc-project/runtime': 0.127.0 '@oxc-project/types': 0.127.0 @@ -7366,9 +7499,10 @@ snapshots: postcss: 8.5.14 optionalDependencies: '@types/node': 25.6.2 + esbuild: 0.28.0 fsevents: 2.3.3 jiti: 2.7.0 - yaml: 2.8.4 + yaml: 2.9.0 '@voidzero-dev/vite-plus-darwin-arm64@0.1.20': optional: true @@ -7388,11 +7522,11 @@ snapshots: '@voidzero-dev/vite-plus-linux-x64-musl@0.1.20': optional: true - '@voidzero-dev/vite-plus-test@0.1.20(@types/node@25.6.2)(jiti@2.7.0)(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4))(yaml@2.8.4)': + '@voidzero-dev/vite-plus-test@0.1.20(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(vite@8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0)': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@voidzero-dev/vite-plus-core': 0.1.20(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4) + '@voidzero-dev/vite-plus-core': 0.1.20(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0) es-module-lexer: 1.7.0 obug: 2.1.1 pixelmatch: 7.2.0 @@ -7402,7 +7536,7 @@ snapshots: tinybench: 2.9.0 tinyexec: 1.1.2 tinyglobby: 0.2.16 - vite: 8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4) + vite: 8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0) ws: 8.20.0 optionalDependencies: '@types/node': 25.6.2 @@ -7573,19 +7707,6 @@ snapshots: character-reference-invalid@2.0.1: {} - chevrotain-allstar@0.4.3(chevrotain@12.0.0): - dependencies: - chevrotain: 12.0.0 - lodash-es: 4.18.1 - - chevrotain@12.0.0: - dependencies: - '@chevrotain/cst-dts-gen': 12.0.0 - '@chevrotain/gast': 12.0.0 - '@chevrotain/regexp-to-ast': 12.0.0 - '@chevrotain/types': 12.0.0 - '@chevrotain/utils': 12.0.0 - class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -7964,6 +8085,35 @@ snapshots: es-toolkit@1.46.1: {} + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -8199,28 +8349,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-from-dom@5.0.1: - dependencies: - '@types/hast': 3.0.4 - hastscript: 9.0.1 - web-namespaces: 2.0.1 - - hast-util-from-html-isomorphic@2.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-from-dom: 5.0.1 - hast-util-from-html: 2.0.3 - unist-util-remove-position: 5.0.0 - - hast-util-from-html@2.0.3: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - hast-util-from-parse5: 8.0.3 - parse5: 7.3.0 - vfile: 6.0.3 - vfile-message: 4.0.3 - hast-util-from-parse5@8.0.3: dependencies: '@types/hast': 3.0.4 @@ -8232,10 +8360,6 @@ snapshots: vfile-location: 5.0.3 web-namespaces: 2.0.1 - hast-util-is-element@3.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -8292,13 +8416,6 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 - hast-util-to-text@4.0.2: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - hast-util-is-element: 3.0.0 - unist-util-find-after: 5.0.0 - hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -8349,7 +8466,7 @@ snapshots: transitivePeerDependencies: - supports-color - i18next@26.0.10: {} + i18next@26.1.0: {} iconv-lite@0.6.3: dependencies: @@ -8452,15 +8569,6 @@ snapshots: koffi@2.16.2: optional: true - langium@4.2.3: - dependencies: - '@chevrotain/regexp-to-ast': 12.0.0 - chevrotain: 12.0.0 - chevrotain-allstar: 0.4.3(chevrotain@12.0.0) - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - layout-base@1.0.2: {} layout-base@2.0.1: {} @@ -8629,18 +8737,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-math@3.0.0: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - longest-streak: 3.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - unist-util-remove-position: 5.0.0 - transitivePeerDependencies: - - supports-color - mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -8717,11 +8813,11 @@ snapshots: merge-descriptors@2.0.0: {} - mermaid@11.14.0: + mermaid@11.15.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.3 - '@mermaid-js/parser': 1.1.0 + '@mermaid-js/parser': 1.1.1 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.3 @@ -8732,14 +8828,14 @@ snapshots: dagre-d3-es: 7.0.14 dayjs: 1.11.20 dompurify: 3.4.2 + es-toolkit: 1.46.1 katex: 0.16.45 khroma: 2.1.0 - lodash-es: 4.18.1 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.4.0 ts-dedent: 2.2.0 - uuid: 11.1.1 + uuid: 14.0.0 micromark-core-commonmark@2.0.3: dependencies: @@ -8818,16 +8914,6 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-math@3.1.0: - dependencies: - '@types/katex': 0.16.8 - devlop: 1.1.0 - katex: 0.16.45 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 @@ -9322,11 +9408,11 @@ snapshots: dependencies: react: 19.2.6 - react-i18next@17.0.7(i18next@26.0.10)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-i18next@17.0.7(i18next@26.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@babel/runtime': 7.29.2 html-parse-stringify: 3.0.1 - i18next: 26.0.10 + i18next: 26.1.0 react: 19.2.6 use-sync-external-store: 1.6.0(react@19.2.6) optionalDependencies: @@ -9425,16 +9511,6 @@ snapshots: dependencies: unist-util-visit: 5.1.0 - rehype-katex@7.0.1: - dependencies: - '@types/hast': 3.0.4 - '@types/katex': 0.16.8 - hast-util-from-html-isomorphic: 2.0.0 - hast-util-to-text: 4.0.2 - katex: 0.16.45 - unist-util-visit-parents: 6.0.2 - vfile: 6.0.3 - rehype-raw@7.0.0: dependencies: '@types/hast': 3.0.4 @@ -9457,15 +9533,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-math@6.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-math: 3.0.0 - micromark-extension-math: 3.1.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -9503,26 +9570,26 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.0.0-rc.18: + rolldown@1.0.0: dependencies: - '@oxc-project/types': 0.128.0 - '@rolldown/pluginutils': 1.0.0-rc.18 + '@oxc-project/types': 0.129.0 + '@rolldown/pluginutils': 1.0.0 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.18 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.18 - '@rolldown/binding-darwin-x64': 1.0.0-rc.18 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.18 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 + '@rolldown/binding-android-arm64': 1.0.0 + '@rolldown/binding-darwin-arm64': 1.0.0 + '@rolldown/binding-darwin-x64': 1.0.0 + '@rolldown/binding-freebsd-x64': 1.0.0 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0 + '@rolldown/binding-linux-arm64-gnu': 1.0.0 + '@rolldown/binding-linux-arm64-musl': 1.0.0 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0 + '@rolldown/binding-linux-s390x-gnu': 1.0.0 + '@rolldown/binding-linux-x64-gnu': 1.0.0 + '@rolldown/binding-linux-x64-musl': 1.0.0 + '@rolldown/binding-openharmony-arm64': 1.0.0 + '@rolldown/binding-wasm32-wasi': 1.0.0 + '@rolldown/binding-win32-arm64-msvc': 1.0.0 + '@rolldown/binding-win32-x64-msvc': 1.0.0 roughjs@4.6.6: dependencies: @@ -9663,7 +9730,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 marked: 17.0.6 - mermaid: 11.14.0 + mermaid: 11.15.0 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) rehype-harden: 1.1.8 @@ -9802,11 +9869,6 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unist-util-find-after@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -9815,11 +9877,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-remove-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-visit: 5.1.0 - unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -9858,8 +9915,6 @@ snapshots: dependencies: react: 19.2.6 - uuid@11.1.1: {} - uuid@14.0.0: {} vary@1.1.2: {} @@ -9905,11 +9960,11 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plus@0.1.20(@types/node@25.6.2)(jiti@2.7.0)(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4))(yaml@2.8.4): + vite-plus@0.1.20(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(vite@8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0): dependencies: '@oxc-project/types': 0.127.0 - '@voidzero-dev/vite-plus-core': 0.1.20(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4) - '@voidzero-dev/vite-plus-test': 0.1.20(@types/node@25.6.2)(jiti@2.7.0)(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4))(yaml@2.8.4) + '@voidzero-dev/vite-plus-core': 0.1.20(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0) + '@voidzero-dev/vite-plus-test': 0.1.20(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(vite@8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0) oxfmt: 0.46.0 oxlint: 1.61.0(oxlint-tsgolint@0.22.0) oxlint-tsgolint: 0.22.0 @@ -9952,42 +10007,26 @@ snapshots: - vite - yaml - vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)(yaml@2.8.4): + vite@8.0.12(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.14 - rolldown: 1.0.0-rc.18 + rolldown: 1.0.0 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.6.2 + esbuild: 0.28.0 fsevents: 2.3.3 jiti: 2.7.0 - yaml: 2.8.4 + yaml: 2.9.0 void-elements@3.1.0: {} - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-textdocument@1.0.12: {} - - vscode-languageserver-types@3.17.5: {} - - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 - vscode-oniguruma@2.0.1: {} vscode-textmate@9.3.2: {} - vscode-uri@3.1.0: {} - web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} @@ -10010,7 +10049,7 @@ snapshots: y18n@5.0.8: {} - yaml@2.8.4: {} + yaml@2.9.0: {} yargs-parser@20.2.9: {} diff --git a/preload.ts b/preload.ts index 6f11a3d..eea93d0 100644 --- a/preload.ts +++ b/preload.ts @@ -1,13 +1,33 @@ -// @ts-nocheck -import { contextBridge, ipcRenderer } from "electron"; - -const invoke = - (channel) => - (...args) => - ipcRenderer.invoke(channel, ...args); +import { contextBridge, ipcRenderer, type IpcRendererEvent } from "electron"; +import type { + ClaudeCodeBridge, + CodexBridge, + ElectronAPI, + InstallProgress, + OpenCodeBridge, + PiBridge, + SettingsBridgeChange, + AppUpdateState, +} from "./src/types/electron"; + +type Listener = (data: T) => void; + +type BridgeApiOptions = { + rendererReady?: boolean; + extraInvoke?: Record; +}; + +type DynamicBridgeApi = Record< + string, + ((...args: never[]) => Promise) | ((callback: Listener) => () => void) +>; + +function invoke Promise>(channel: string): T { + return ((...args: never[]) => ipcRenderer.invoke(channel, ...args)) as T; +} -function createBridgeApi(prefix, options = {}) { - const api = { +function createBridgeApi(prefix: string, options: BridgeApiOptions = {}): T { + const api: DynamicBridgeApi = { addProject: invoke(`${prefix}:project:add`), removeProject: invoke(`${prefix}:project:remove`), disconnect: invoke(`${prefix}:disconnect`), @@ -28,38 +48,42 @@ function createBridgeApi(prefix, options = {}) { sendCommand: invoke(`${prefix}:command:send`), summarizeSession: invoke(`${prefix}:session:summarize`), findFiles: invoke(`${prefix}:find:files`), - onEvent: (callback) => { + onEvent: (callback: Listener) => { if (options.rendererReady) ipcRenderer.send(`${prefix}:renderer-ready`); - const handler = (_event, data) => callback(data); + const handler = (_event: IpcRendererEvent, data: unknown) => callback(data); ipcRenderer.on(`${prefix}:bridge-event`, handler); - return () => ipcRenderer.removeListener(`${prefix}:bridge-event`, handler); + return () => { + ipcRenderer.removeListener(`${prefix}:bridge-event`, handler); + }; }, }; for (const [name, channel] of Object.entries(options.extraInvoke ?? {})) { - api[name] = invoke(`${prefix}:${String(channel)}`); + api[name] = invoke(`${prefix}:${channel}`); } - return api; + return api as T; } -contextBridge.exposeInMainWorld("electronAPI", { +const electronAPI: ElectronAPI = { settings: { getAllSync: () => ipcRenderer.sendSync("settings:get-all-sync"), - getSync: (key) => ipcRenderer.sendSync("settings:get-sync", key), - setSync: (key, value) => ipcRenderer.sendSync("settings:set-sync", key, value), - removeSync: (key) => ipcRenderer.sendSync("settings:remove-sync", key), - mergeSync: (entries) => ipcRenderer.sendSync("settings:merge-sync", entries), + getSync: (key: string) => ipcRenderer.sendSync("settings:get-sync", key), + setSync: (key: string, value: string) => ipcRenderer.sendSync("settings:set-sync", key, value), + removeSync: (key: string) => ipcRenderer.sendSync("settings:remove-sync", key), + mergeSync: (entries: Record) => + ipcRenderer.sendSync("settings:merge-sync", entries), set: invoke("settings:set"), remove: invoke("settings:remove"), - onDidChange: (callback) => { - const handler = (_event, change) => callback(change); + onDidChange: (callback: Listener) => { + const handler = (_event: IpcRendererEvent, change: SettingsBridgeChange) => callback(change); ipcRenderer.on("settings:changed", handler); - return () => ipcRenderer.removeListener("settings:changed", handler); + return () => { + ipcRenderer.removeListener("settings:changed", handler); + }; }, }, - // Window controls minimize: invoke("window:minimize"), maximize: invoke("window:maximize"), close: invoke("window:close"), @@ -68,76 +92,66 @@ contextBridge.exposeInMainWorld("electronAPI", { getSystemLocale: invoke("platform:locale"), detectBackends: invoke("platform:detectBackends"), isPackaged: invoke("app:isPackaged"), - onMaximizeChange: (callback) => { - const handler = (_event, isMaximized) => callback(isMaximized); + onMaximizeChange: (callback: Listener) => { + const handler = (_event: IpcRendererEvent, isMaximized: boolean) => callback(isMaximized); ipcRenderer.on("window:maximizeChanged", handler); - return () => ipcRenderer.removeListener("window:maximizeChanged", handler); + return () => { + ipcRenderer.removeListener("window:maximizeChanged", handler); + }; }, - // Directory picker openDirectory: invoke("dialog:openDirectory"), - - // Detach a project into its own window detachProject: invoke("window:detachProject"), - - // Get the detached project directory from the URL query param (empty if not detached) - getDetachedProject: () => { - const params = new URLSearchParams(window.location.search); - return params.get("detach") || null; - }, + getDetachedProject: () => new URLSearchParams(window.location.search).get("detach"), getDetachedProjects: invoke("window:getDetachedProjects"), - onDetachedProjectsChange: (callback) => { - const handler = (_event, detachedProjects) => callback(detachedProjects); + onDetachedProjectsChange: (callback: Listener) => { + const handler = (_event: IpcRendererEvent, detachedProjects: string[]) => + callback(detachedProjects); ipcRenderer.on("window:detachedProjectsChanged", handler); - return () => ipcRenderer.removeListener("window:detachedProjectsChanged", handler); + return () => { + ipcRenderer.removeListener("window:detachedProjectsChanged", handler); + }; }, - // Open a URL in the system browser openExternal: invoke("shell:openExternal"), - updates: { getState: invoke("updates:getState"), check: invoke("updates:check"), download: invoke("updates:download"), install: invoke("updates:install"), - onStateChanged: (callback) => { - const handler = (_event, nextState) => callback(nextState); + onStateChanged: (callback: Listener) => { + const handler = (_event: IpcRendererEvent, nextState: AppUpdateState) => callback(nextState); ipcRenderer.on("updates:state-changed", handler); - return () => ipcRenderer.removeListener("updates:state-changed", handler); + return () => { + ipcRenderer.removeListener("updates:state-changed", handler); + }; }, }, - // Open a directory in the system file browser openInFileBrowser: invoke("shell:openInFileBrowser"), - - // Open a terminal at a directory openInTerminal: invoke("shell:openInTerminal"), - - // Home directory (for path abbreviation) getHomeDir: invoke("platform:homeDir"), - - // Backend installer – runs allowlisted backend install and streams progress events installBackend: invoke("backend:install"), - onInstallProgress: (callback) => { - const handler = (_event, data) => callback(data); + onInstallProgress: (callback: Listener) => { + const handler = (_event: IpcRendererEvent, data: InstallProgress) => callback(data); ipcRenderer.on("backend:install-progress", handler); - return () => ipcRenderer.removeListener("backend:install-progress", handler); + return () => { + ipcRenderer.removeListener("backend:install-progress", handler); + }; }, - - // Skills install progress events - onSkillsInstallProgress: (callback) => { - const handler = (_event, data) => callback(data); + onSkillsInstallProgress: (callback: Listener) => { + const handler = (_event: IpcRendererEvent, data: InstallProgress) => callback(data); ipcRenderer.on("opencode:skills:install-progress", handler); - return () => ipcRenderer.removeListener("opencode:skills:install-progress", handler); + return () => { + ipcRenderer.removeListener("opencode:skills:install-progress", handler); + }; }, - // Worktree setup helpers worktree: { detectSetup: invoke("worktree:detect-setup"), runSetup: invoke("worktree:run-setup"), }, - // Git helpers git: { isRepo: invoke("git:is-repo"), listBranches: invoke("git:branch:list"), @@ -150,10 +164,10 @@ contextBridge.exposeInMainWorld("electronAPI", { getRemoteUrl: invoke("git:remote:url"), }, - claudeCode: createBridgeApi("claude-code", { rendererReady: true }), - pi: createBridgeApi("pi"), - codex: createBridgeApi("codex"), - opencode: createBridgeApi("opencode", { + claudeCode: createBridgeApi("claude-code", { rendererReady: true }), + pi: createBridgeApi("pi"), + codex: createBridgeApi("codex"), + opencode: createBridgeApi("opencode", { extraInvoke: { revertSession: "session:revert", unrevertSession: "session:unrevert", @@ -176,7 +190,6 @@ contextBridge.exposeInMainWorld("electronAPI", { startServer: "server:start", stopServer: "server:stop", getServerStatus: "server:status", - // Skills marketplace marketplaceList: "skills:marketplace:list", marketplaceSearch: "skills:marketplace:search", marketplaceDetail: "skills:marketplace:detail", @@ -189,4 +202,6 @@ contextBridge.exposeInMainWorld("electronAPI", { checkSkillsCli: "skills:check-cli", }, }), -}); +}; + +contextBridge.exposeInMainWorld("electronAPI", electronAPI); diff --git a/scripts/fetch-provider-icons.ts b/scripts/fetch-provider-icons.ts index 9d12e06..dcf8a8d 100644 --- a/scripts/fetch-provider-icons.ts +++ b/scripts/fetch-provider-icons.ts @@ -12,14 +12,14 @@ const ICONS_DIR = "src/components/provider-icons/svgs"; const OUTPUT_DIR = "src/components/provider-icons"; async function main() { - console.log(`Fetching provider list from ${MODELS_URL}/api.json ...`); + console.info(`Fetching provider list from ${MODELS_URL}/api.json ...`); const apiRes = await fetch(`${MODELS_URL}/api.json`); if (!apiRes.ok) { throw new Error(`Failed to fetch api.json: ${apiRes.status}`); } const api = (await apiRes.json()) as Record; const providerIds = Object.keys(api); - console.log(`Found ${providerIds.length} providers`); + console.info(`Found ${providerIds.length} providers`); // Ensure output directories exist await Bun.write(`${ICONS_DIR}/.gitkeep`, ""); @@ -58,16 +58,16 @@ async function main() { } } - console.log(`Downloaded ${succeeded.length} icons, ${failed.length} failed`); + console.info(`Downloaded ${succeeded.length} icons, ${failed.length} failed`); if (failed.length > 0) { - console.log(`Failed: ${failed.join(", ")}`); + console.info(`Failed: ${failed.join(", ")}`); } // Sort for deterministic output succeeded.sort(); // Generate sprite.svg - console.log("Generating sprite.svg ..."); + console.info("Generating sprite.svg ..."); const symbols: string[] = []; for (const id of succeeded) { @@ -93,10 +93,10 @@ ${symbols.join("\n")} `; await Bun.write(`${OUTPUT_DIR}/sprite.svg`, sprite); - console.log(`Wrote sprite.svg with ${symbols.length} symbols`); + console.info(`Wrote sprite.svg with ${symbols.length} symbols`); // Generate types.ts - console.log("Generating types.ts ..."); + console.info("Generating types.ts ..."); const typesContent = `/** * Auto-generated provider icon names. * Do not edit manually - run \`bun scripts/fetch-provider-icons.ts\` to regenerate. @@ -110,9 +110,9 @@ export type ProviderIconName = (typeof providerIconNames)[number]; `; await Bun.write(`${OUTPUT_DIR}/types.ts`, typesContent); - console.log(`Wrote types.ts with ${succeeded.length} icon names`); + console.info(`Wrote types.ts with ${succeeded.length} icon names`); - console.log("Done!"); + console.info("Done!"); } main().catch((err) => { diff --git a/server/web-server.ts b/server/web-server.ts index dde20dd..1d55340 100644 --- a/server/web-server.ts +++ b/server/web-server.ts @@ -405,4 +405,4 @@ const server = Bun.serve({ }, }); -console.log(`OpenGUI web running at ${server.url}`); +console.info(`OpenGUI web running at ${server.url}`); diff --git a/src/agents/backend.ts b/src/agents/backend.ts index 461aa56..3a68919 100644 --- a/src/agents/backend.ts +++ b/src/agents/backend.ts @@ -54,7 +54,7 @@ export interface AgentBackendCapabilities { localServer: boolean; } -export interface AgentBackendWorkspaceProfile { +interface AgentBackendWorkspaceProfile { kind: "remote-server" | "local-cli"; fields: { serverUrl: boolean; @@ -64,11 +64,11 @@ export interface AgentBackendWorkspaceProfile { }; } -export interface AgentSessionStatus { +interface AgentSessionStatus { type: string; } -export interface AgentMessagePage { +interface AgentMessagePage { messages: Array<{ info: Message; parts: Part[] }>; nextCursor: string | null; } @@ -124,7 +124,7 @@ export type AgentBackendEvent = | { type: "question.cleared"; sessionID: string } | { type: "session.error"; error: string; sessionID?: string }; -export interface AgentRuntimeBackend { +interface AgentRuntimeBackend { listSessions(target?: AgentBackendTarget): Promise; createSession(input?: { title?: string; @@ -194,13 +194,13 @@ export interface AgentRuntimeBackend { subscribe(listener: (event: AgentBackendEvent) => void): () => void; } -export interface AgentHostBackend { +interface AgentHostBackend { addProject(config: ConnectionConfig): Promise; removeProject(target: AgentBackendTarget): Promise; disconnect(): Promise; } -export interface AgentPlatformBackend { +interface AgentPlatformBackend { server?: { start(): Promise<{ alreadyRunning?: boolean }>; stop(): Promise<{ alreadyStopped?: boolean; pid?: number }>; diff --git a/src/agents/claude-code.ts b/src/agents/claude-code.ts index 71203bd..be013c2 100644 --- a/src/agents/claude-code.ts +++ b/src/agents/claude-code.ts @@ -16,7 +16,7 @@ import { type TaggedSession, } from "./shared"; -export interface ClaudeCodeBackendAdapter extends ClaudeCodeBridge, AgentBackendDescriptor { +interface ClaudeCodeBackendAdapter extends ClaudeCodeBridge, AgentBackendDescriptor { native: ClaudeCodeBridge; } diff --git a/src/agents/codex.ts b/src/agents/codex.ts index e0575ed..1ae7ff7 100644 --- a/src/agents/codex.ts +++ b/src/agents/codex.ts @@ -16,7 +16,7 @@ import { type TaggedSession, } from "./shared"; -export interface CodexBackendAdapter extends CodexBridge, AgentBackendDescriptor { +interface CodexBackendAdapter extends CodexBridge, AgentBackendDescriptor { native: CodexBridge; } diff --git a/src/agents/index.ts b/src/agents/index.ts index 867fe29..ff6e9d2 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -18,7 +18,7 @@ export const AGENT_BACKEND_LABELS: Record = { export { createBackendIdCodec as createAgentIdCodec } from "./shared"; -export const AGENT_ID_CODECS = Object.fromEntries( +const AGENT_ID_CODECS = Object.fromEntries( AGENT_BACKEND_IDS.map((backendId) => [backendId, createBackendIdCodec(backendId)]), ) as Record>; diff --git a/src/agents/opencode.ts b/src/agents/opencode.ts index f275ecd..d33759a 100644 --- a/src/agents/opencode.ts +++ b/src/agents/opencode.ts @@ -1,15 +1,9 @@ import type { Event as OpenCodeEvent, QuestionAnswer } from "@opencode-ai/sdk/v2/client"; -import type { - NativeBackendEvent, - NativeAgentBridge, - OpenCodeBridge, - SelectedModel, -} from "@/types/electron"; +import type { NativeBackendEvent, NativeAgentBridge, OpenCodeBridge } from "@/types/electron"; import type { AgentBackendCapabilities, AgentBackendDescriptor, AgentBackendEvent, - AgentBackendTarget, } from "./backend"; import { createBackendIdCodec, @@ -21,7 +15,7 @@ import { type TaggedSession, } from "./shared"; -export interface OpenCodeBackendAdapter extends OpenCodeBridge, AgentBackendDescriptor { +interface OpenCodeBackendAdapter extends OpenCodeBridge, AgentBackendDescriptor { native: OpenCodeBridge; } @@ -563,19 +557,3 @@ export function createOpenCodeBackend( return adapter; } - -export function toOpenCodeTarget(directory?: string, workspaceId?: string): AgentBackendTarget { - return { directory, workspaceId }; -} - -export function toOpenCodePromptOptions(input: { - model?: SelectedModel | null; - agent?: string | null; - variant?: string; -}) { - return { - model: input.model ?? undefined, - agent: input.agent ?? undefined, - variant: input.variant, - }; -} diff --git a/src/agents/pi.ts b/src/agents/pi.ts index 8fe78f5..1a98a30 100644 --- a/src/agents/pi.ts +++ b/src/agents/pi.ts @@ -16,7 +16,7 @@ import { type TaggedSession, } from "./shared"; -export interface PiBackendAdapter extends PiBridge, AgentBackendDescriptor { +interface PiBackendAdapter extends PiBridge, AgentBackendDescriptor { native: PiBridge; } diff --git a/src/agents/shared.ts b/src/agents/shared.ts index d3adb81..53d661c 100644 --- a/src/agents/shared.ts +++ b/src/agents/shared.ts @@ -4,12 +4,12 @@ import { normalizeProjectPath } from "@/lib/utils"; import type { AgentBackendEvent, AgentBackendTarget } from "./backend"; import type { AgentBackendId } from "./index"; -export type BridgeResult = +type BridgeResult = | { success: true; data?: T } | { success: false; error?: string } | { success: boolean; data?: T; error?: string }; -export type SessionTags = { +type SessionTags = { _projectDir?: string; _workspaceId?: string; _backendId?: AgentBackendId; @@ -79,9 +79,7 @@ export function normalizePartSessionId(backendId: AgentBackendId, part: Part): P : part; } -export function normalizeBridgeConnectionStatus( - event: NativeBackendEvent, -): AgentBackendEvent | null { +function normalizeBridgeConnectionStatus(event: NativeBackendEvent): AgentBackendEvent | null { if (event.type !== "connection:status") return null; return { type: "connection.status", @@ -134,7 +132,7 @@ export function normalizeTaggedBackendEvent( } } -export function normalizeBackendEventPayload( +function normalizeBackendEventPayload( backendId: AgentBackendId, payload: AgentBackendEvent, ): AgentBackendEvent { diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 0a11934..0cbb4ba 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -152,7 +152,6 @@ export function AppSidebar({ queuedPrompts, pendingQuestions, pendingPermissions, - temporarySessions, unreadSessionIds, sessionDrafts, sessionMeta, @@ -278,10 +277,7 @@ export function AppSidebar({ const rootSessions = sessions.filter((s) => { const sessionDir = normalizeProjectPath(s._projectDir ?? s.directory); return ( - !s.parentID && - openDirectorySet.has(sessionDir) && - !temporarySessions.has(s.id) && - !sessionMeta[s.id]?.movedToSessionId + !s.parentID && openDirectorySet.has(sessionDir) && !sessionMeta[s.id]?.movedToSessionId ); }); const rootOpenDirectories = openDirectories.filter((dir) => !worktreeDirs.has(dir)); @@ -323,7 +319,6 @@ export function AppSidebar({ }, [ sessions, connections, - temporarySessions, worktreeParents, worktreeDirs, detachedProject, @@ -338,12 +333,11 @@ export function AppSidebar({ sessions.filter( (session) => !session.parentID && - !temporarySessions.has(session.id) && !sessionMeta[session.id]?.movedToSessionId && isChatSession(session), ), ), - [sessions, temporarySessions, isChatSession, sessionMeta, sortSessionsForSidebar], + [sessions, isChatSession, sessionMeta, sortSessionsForSidebar], ); const filteredChatSessions = useMemo(() => { if (!hasActiveSearch) return chatSessions; diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index 6af90fa..2d213ab 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -12,9 +12,7 @@ import { } from "react"; import ReactMarkdown from "react-markdown"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; -import rehypeKatex from "rehype-katex"; import remarkGfm from "remark-gfm"; -import remarkMath from "remark-math"; import { cn, openExternalLink } from "@/lib/utils"; type StarryNight = Awaited>; @@ -163,17 +161,11 @@ const markdownComponents = { }; export const MarkdownRenderer = memo(function MarkdownRenderer({ content }: { content: string }) { - const remarkPlugins = useMemo(() => [remarkGfm, remarkMath], []); - const rehypePlugins = useMemo(() => [rehypeKatex], []); + const remarkPlugins = useMemo(() => [remarkGfm], []); return (
- + {content}
diff --git a/src/components/ModelSelector.tsx b/src/components/ModelSelector.tsx index 3d073d1..5390d68 100644 --- a/src/components/ModelSelector.tsx +++ b/src/components/ModelSelector.tsx @@ -17,7 +17,11 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { useAvailableBackendIds, useBackendCapabilities } from "@/hooks/use-agent-backend"; +import { + useAvailableBackendIds, + useBackendCapabilities, + useCurrentAgentBackendId, +} from "@/hooks/use-agent-backend"; import { useActions, useModelState, useSessionState } from "@/hooks/use-agent-state"; import { DEFAULT_MODEL_MAX_AGE_MONTHS, MAX_RECENT_MODELS, STORAGE_KEYS } from "@/lib/constants"; import { storageGet, storageParsed, storageSetJSON } from "@/lib/safe-storage"; @@ -111,18 +115,18 @@ export function ModelSelector() { const { providers, selectedModel } = useModelState(); const { sessions, activeSessionId, draftSessionBackendId } = useSessionState(); const availableBackendIds = useAvailableBackendIds(); + const preferredBackendId = useCurrentAgentBackendId(); const capabilities = useBackendCapabilities(); const activeSession = sessions.find((session) => session.id === activeSessionId) ?? null; const lockedBackendId = activeSession?._backendId ?? null; - const selectedBackendId = lockedBackendId ?? draftSessionBackendId; + const selectedBackendId = lockedBackendId ?? draftSessionBackendId ?? preferredBackendId; const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const inputRef = useRef(null); const [recentValues, setRecentValues] = useState([]); const [favoriteValues, setFavoriteValues] = useState>(new Set()); const [modelMaxAgeMonths, setModelMaxAgeMonths] = useState(() => getStoredModelMaxAgeMonths()); - - if (!capabilities?.models) return null; + const [storageHydrated, setStorageHydrated] = useState(false); useEffect(() => { if (typeof window === "undefined") return; @@ -134,6 +138,7 @@ export function ModelSelector() { if (Array.isArray(favArr)) { setFavoriteValues(new Set(favArr.filter((v): v is string => typeof v === "string"))); } + setStorageHydrated(true); }, []); useEffect(() => { @@ -150,14 +155,14 @@ export function ModelSelector() { }, []); useEffect(() => { - if (typeof window === "undefined") return; + if (typeof window === "undefined" || !storageHydrated) return; storageSetJSON(STORAGE_KEYS.RECENT_MODELS, recentValues.slice(0, MAX_RECENT_MODELS)); - }, [recentValues]); + }, [recentValues, storageHydrated]); useEffect(() => { - if (typeof window === "undefined") return; + if (typeof window === "undefined" || !storageHydrated) return; storageSetJSON(STORAGE_KEYS.FAVORITE_MODELS, [...favoriteValues]); - }, [favoriteValues]); + }, [favoriteValues, storageHydrated]); // Open via Ctrl+X M chord shortcut dispatched from App.tsx useEffect(() => { @@ -313,7 +318,7 @@ export function ModelSelector() { const hasResults = filteredGroups.some((group) => group.models.length > 0); - if (providers.length === 0) return null; + if (!capabilities?.models || providers.length === 0) return null; return ( diff --git a/src/components/message-list/VirtualMessageScroller.tsx b/src/components/message-list/VirtualMessageScroller.tsx index 5b7bea3..20f8335 100644 --- a/src/components/message-list/VirtualMessageScroller.tsx +++ b/src/components/message-list/VirtualMessageScroller.tsx @@ -1,15 +1,13 @@ import { useVirtualizer } from "@tanstack/react-virtual"; -import { ArrowDown } from "lucide-react"; import { type ReactNode, + type WheelEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, - useState, } from "react"; -import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import type { MessageEntry } from "@/hooks/use-agent-state"; import { NEAR_BOTTOM_PX } from "@/lib/constants"; @@ -56,7 +54,6 @@ export function VirtualMessageScroller({ const loadInFlightRef = useRef(false); const programmaticScrollRef = useRef(false); const pinnedToBottomRef = useRef(true); - const [showJumpToLatest, setShowJumpToLatest] = useState(false); const keys = useMemo(() => messages.map((message) => message.info.id), [messages]); const keyIndex = useMemo(() => { @@ -114,7 +111,6 @@ export function VirtualMessageScroller({ const scrollToLatest = useCallback(() => { if (messages.length === 0) return; pinnedToBottomRef.current = true; - setShowJumpToLatest(false); programmaticScrollRef.current = true; virtualizer.scrollToIndex(messages.length - 1, { align: "end" }); requestAnimationFrame(() => { @@ -126,14 +122,26 @@ export function VirtualMessageScroller({ }); }, [messages.length, virtualizer]); + const detachFromBottom = useCallback((force = false) => { + const el = scrollRef.current; + if (!el || (!force && isNearBottom(el))) return; + pinnedToBottomRef.current = false; + }, []); + const handleScroll = useCallback(() => { const el = scrollRef.current; if (!el || programmaticScrollRef.current) return; const nearBottom = isNearBottom(el); pinnedToBottomRef.current = nearBottom; - if (nearBottom) setShowJumpToLatest(false); }, []); + const handleWheel = useCallback( + (event: WheelEvent) => { + if (event.deltaY < 0) detachFromBottom(true); + }, + [detachFromBottom], + ); + useLayoutEffect(() => { restoreAnchor(); }, [restoreAnchor, messages.length]); @@ -147,7 +155,6 @@ export function VirtualMessageScroller({ scrollToLatest(); return; } - if (previousLastKey !== lastKey) setShowJumpToLatest(true); }, [keys, scrollToLatest]); useEffect(() => { @@ -175,6 +182,8 @@ export function VirtualMessageScroller({
detachFromBottom(true)} className="relative flex-1 overflow-auto px-4 py-4" >
@@ -206,18 +215,6 @@ export function VirtualMessageScroller({
{trailingContent}
- {showJumpToLatest && ( - - )} ); } diff --git a/src/components/message-list/tools/ToolPartView.tsx b/src/components/message-list/tools/ToolPartView.tsx index 11631d3..dc0a1d5 100644 --- a/src/components/message-list/tools/ToolPartView.tsx +++ b/src/components/message-list/tools/ToolPartView.tsx @@ -174,7 +174,16 @@ function ToolBody({ case "terminal": return (
- +
); case "apply-patch": @@ -299,11 +308,12 @@ export function ToolPartView({ taskContentRef={taskContentRef} /> )} - {presentation.error && ( -
- {presentation.error} -
- )} + {presentation.error && + !(presentation.tool.kind === "bash" && presentation.bashOutputText?.trim()) && ( +
+ {presentation.error} +
+ )} {hasSideContent && (
{presentation.sideContent.images.length > 0 && ( diff --git a/src/components/message-list/tools/applyPatch.ts b/src/components/message-list/tools/applyPatch.ts index 00a44f8..a1e2679 100644 --- a/src/components/message-list/tools/applyPatch.ts +++ b/src/components/message-list/tools/applyPatch.ts @@ -2,7 +2,7 @@ import type { ToolPart } from "@opencode-ai/sdk/v2/client"; import { parseUnifiedDiff, type DiffLine, type DiffResult } from "@/lib/diff"; import { getToolInput, isRecord, stringField, toFiniteNumber } from "./toolTypes"; -export type ApplyPatchChangeType = "add" | "delete" | "move" | "update"; +type ApplyPatchChangeType = "add" | "delete" | "move" | "update"; export interface ApplyPatchFileDiff { id: string; @@ -22,7 +22,7 @@ function computeApplyPatchDiff(file: Record): DiffResult | null return parseDiffText(file.diff); } -export function extractApplyPatchFiles(state: ToolPart["state"]): ApplyPatchFileDiff[] { +function extractApplyPatchFiles(state: ToolPart["state"]): ApplyPatchFileDiff[] { if (!("metadata" in state) || !isRecord(state.metadata)) return []; const rawFiles = state.metadata.files; if (!Array.isArray(rawFiles)) return []; diff --git a/src/components/message-list/tools/toolPresentation.ts b/src/components/message-list/tools/toolPresentation.ts index 77c5573..ade96f3 100644 --- a/src/components/message-list/tools/toolPresentation.ts +++ b/src/components/message-list/tools/toolPresentation.ts @@ -26,7 +26,7 @@ export type ToolBody = | { type: "task"; taskInfo: TaskInfo } | null; -export interface NormalizedTool { +interface NormalizedTool { rawName: string; kind: ToolKind; variant: ToolVariant; @@ -62,6 +62,10 @@ function getBashMetadataOutput(state: ToolPart["state"]): string | null { return typeof state.metadata.output === "string" ? state.metadata.output : null; } +function getErrorText(state: ToolPart["state"]): string | null { + return "error" in state && typeof state.error === "string" ? state.error : null; +} + function getToolTitle(part: ToolPart, kind: ToolKind, isRunning: boolean): string { const input = getToolInput(part.state); const title = @@ -125,12 +129,13 @@ export function getToolPresentation( const rawOutputText = getRawOutputText(state); const outputText = rawOutputText?.trim() || null; + const errorText = getErrorText(state); const bashMetadataOutput = kind === "bash" ? getBashMetadataOutput(state) : null; const bashOutputText = kind === "bash" ? isRunning ? (bashMetadataOutput ?? rawOutputText) - : (rawOutputText ?? bashMetadataOutput) + : (rawOutputText ?? bashMetadataOutput ?? errorText) : null; const editFiles = kind === "edit" ? extractEditFiles(state) : []; @@ -215,7 +220,7 @@ export function getToolPresentation( expandable: body !== null, body, sideContent: { todos, images }, - error: state.status === "error" && state.error ? state.error : null, + error: state.status === "error" && errorText ? errorText : null, taskInfo, bashOutputText, }; diff --git a/src/components/message-list/tools/toolTypes.ts b/src/components/message-list/tools/toolTypes.ts index 298056c..d842a19 100644 --- a/src/components/message-list/tools/toolTypes.ts +++ b/src/components/message-list/tools/toolTypes.ts @@ -27,7 +27,7 @@ export type ToolVariant = | "browser" | "fetch"; -export type ToolStatus = ToolPart["state"]["status"]; +type ToolStatus = ToolPart["state"]["status"]; const TOOL_ALIASES = { read: "read", diff --git a/src/hooks/agent-contexts.ts b/src/hooks/agent-contexts.ts index f0a5682..801e3b0 100644 --- a/src/hooks/agent-contexts.ts +++ b/src/hooks/agent-contexts.ts @@ -19,17 +19,11 @@ import type { } from "@/hooks/agent-state-types"; import type { ProjectMeta, - RecentProject, SessionColor, SessionMetaMap, WorktreeParentMap, } from "@/hooks/agent-state-persistence"; -import type { - ConnectionConfig, - ConnectionStatus, - SelectedModel, - Workspace, -} from "@/types/electron"; +import type { ConnectionStatus, SelectedModel, Workspace } from "@/types/electron"; export interface SessionContextValue { sessions: Session[]; @@ -42,13 +36,10 @@ export interface SessionContextValue { pendingQuestions: Record; draftSessionDirectory: string | null; draftSessionBackendId: AgentBackendId | null; - draftIsTemporary: boolean; - temporarySessions: Set; namingSessionIds: Set; unreadSessionIds: Set; sessionDrafts: Record; sessionMeta: SessionMetaMap; - recentProjects: RecentProject[]; } export interface MessagesContextValue { @@ -56,9 +47,7 @@ export interface MessagesContextValue { turnRuns: Record; childSessions: InternalAgentState["childSessions"]; messageHistoryHasMore: boolean; - messageWindowHasNewer: boolean; isLoadingOlderMessages: boolean; - isLoadingNewerMessages: boolean; } export interface ModelContextValue { @@ -89,7 +78,6 @@ export interface ConnectionContextValue { workspaceDirectory: string | null; defaultChatDirectory: string | null; workspaceServerUrl: string | null; - workspaceUsername: string | null; isLocalWorkspace: boolean; activeDirectory: string | null; bootState: InternalAgentState["bootState"]; @@ -102,13 +90,9 @@ export interface ConnectionContextValue { } export interface ActionsContextValue { - addProject: (config: ConnectionConfig, options?: { suppressError?: boolean }) => Promise; removeProject: (directory: string) => Promise; - disconnect: () => Promise; selectSession: (id: string | null) => Promise; loadOlderMessages: () => Promise; - loadNewerMessages: () => Promise; - createSession: (title?: string, directory?: string) => Promise; deleteSession: (id: string) => Promise; renameSession: (id: string, title: string) => Promise; sendPrompt: (text: string, images?: string[], mode?: QueueMode) => Promise; @@ -125,7 +109,6 @@ export interface ActionsContextValue { revertVariant: () => void; clearError: () => void; refreshProviders: () => Promise; - refreshSessions: () => Promise; getQueuedPrompts: (sessionId: string) => QueuedPrompt[]; removeFromQueue: (sessionId: string, promptId: string) => void; reorderQueue: (sessionId: string, fromIndex: number, toIndex: number) => void; @@ -145,7 +128,6 @@ export interface ActionsContextValue { setDefaultChatDirectory: (directory: string | null) => void; setDraftDirectory: (directory: string) => void; setDraftBackend: (backendId: AgentBackendId) => void; - setDraftTemporary: (temporary: boolean) => void; revertToMessage: (messageID: string) => Promise; unrevert: () => Promise; forkFromMessage: (messageID: string) => Promise; @@ -156,7 +138,6 @@ export interface ActionsContextValue { setProjectPinned: (directory: string, pinned: boolean) => void; registerWorktree: (worktreeDir: string, parentDir: string, branch: string) => void; unregisterWorktree: (worktreeDir: string) => void; - touchWorktree: (worktreeDir: string) => void; clearWorktreeCleanup: () => void; createWorkspace: (input: { name: string; @@ -171,7 +152,6 @@ export interface ActionsContextValue { removeWorkspace: (workspaceId: string) => Promise; switchWorkspace: (workspaceId: string) => void; reorderWorkspaces: (fromIndex: number, toIndex: number) => void; - reorderProjects: (fromIndex: number, toIndex: number) => void; reorderVisibleProjects: (orderedDirectories: string[]) => void; } diff --git a/src/hooks/agent-message-state.test.ts b/src/hooks/agent-message-state.test.ts new file mode 100644 index 0000000..91c4db1 --- /dev/null +++ b/src/hooks/agent-message-state.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, test } from "@voidzero-dev/vite-plus-test"; +import type { Message, Part } from "@opencode-ai/sdk/v2/client"; +import type { MessageEntry } from "@/hooks/agent-state-types"; +import { mergeMessageSnapshot, removeMatchingOptimisticUserMessage } from "./agent-message-state"; + +function textPart(id: string, messageID: string, text: string, start = 1): Part { + return { + id, + type: "text", + text, + sessionID: "session-1", + messageID, + time: { start }, + } as Part; +} + +function message(id: string, created: number, parts: Part[]): MessageEntry { + return { + info: { + id, + sessionID: "session-1", + role: "assistant", + time: { created }, + } as Message, + parts, + }; +} + +describe("mergeMessageSnapshot", () => { + test("empty stale snapshot does not wipe existing live messages", () => { + const existing = [message("live", 10, [textPart("p1", "live", "streaming")])]; + + const merged = mergeMessageSnapshot([], existing); + + expect(merged).toEqual(existing); + }); + + test("preserves existing parts missing from stale same-message snapshot", () => { + const existing = [ + message("assistant", 10, [ + textPart("text", "assistant", "hello", 1), + textPart("tool", "assistant", "tool output", 2), + ]), + ]; + const incoming = [message("assistant", 10, [textPart("text", "assistant", "hello", 1)])]; + + const merged = mergeMessageSnapshot(incoming, existing); + + expect(merged[0]?.parts.map((part) => part.id)).toEqual(["text", "tool"]); + }); + + test("keeps existing messages newer than snapshot tail", () => { + const existing = [ + message("old", 1, [textPart("old-part", "old", "old")]), + message("new-live", 20, [textPart("new-part", "new-live", "new")]), + ]; + const incoming = [message("old", 1, [textPart("old-part", "old", "old from server")])]; + + const merged = mergeMessageSnapshot(incoming, existing); + + expect(merged.map((entry) => entry.info.id)).toEqual(["old", "new-live"]); + }); + + test("canonical user message replaces matching optimistic user message", () => { + const optimistic = { + info: { + id: "local-user:turn-1", + sessionID: "session-1", + role: "user", + time: { created: 10 }, + } as Message, + parts: [textPart("local-user:turn-1:text", "local-user:turn-1", "hello", 10)], + }; + const canonical = { + info: { + id: "server-user", + sessionID: "session-1", + role: "user", + time: { created: 11 }, + } as Message, + parts: [textPart("server-user:text", "server-user", "hello", 11)], + }; + + const merged = mergeMessageSnapshot([canonical], [optimistic]); + + expect(merged.map((entry) => entry.info.id)).toEqual(["server-user"]); + }); + + test("live canonical user message removes matching optimistic user message", () => { + const optimistic = { + info: { + id: "local-user:turn-1", + sessionID: "session-1", + role: "user", + time: { created: 10 }, + } as Message, + parts: [textPart("local-user:turn-1:text", "local-user:turn-1", "hello", 10)], + }; + const canonical = { + info: { + id: "server-user", + sessionID: "session-1", + role: "user", + time: { created: 11 }, + } as Message, + parts: [textPart("server-user:text", "server-user", "hello", 11)], + }; + + const merged = removeMatchingOptimisticUserMessage([optimistic, canonical], canonical); + + expect(merged.map((entry) => entry.info.id)).toEqual(["server-user"]); + }); +}); diff --git a/src/hooks/agent-message-state.ts b/src/hooks/agent-message-state.ts index 255ba7d..83ddd2d 100644 --- a/src/hooks/agent-message-state.ts +++ b/src/hooks/agent-message-state.ts @@ -12,11 +12,73 @@ export const MAX_SESSION_BUFFER_CACHE = 8; type DeltaTrackedPart = Part & { _deltaPositions?: Record }; -export function getMessageCreatedAt(message: { info: Message }): number { - return message.info.time.created ?? 0; +const OPTIMISTIC_USER_PREFIX = "local-user:"; + +export function getMessageText(entry: MessageEntry): string { + return entry.parts + .flatMap((part) => { + const record = part as Record; + return part.type === "text" && typeof record.text === "string" ? [record.text] : []; + }) + .join("\n") + .trim(); +} + +export function isOptimisticUserMessage(entry: MessageEntry): boolean { + return entry.info.id.startsWith(OPTIMISTIC_USER_PREFIX) && entry.info.role === "user"; +} + +export function removeMatchingOptimisticUserMessage( + messages: MessageEntry[], + canonical: MessageEntry, +): MessageEntry[] { + if (canonical.info.role !== "user" || isOptimisticUserMessage(canonical)) return messages; + const canonicalText = getMessageText(canonical); + if (!canonicalText) return messages; + const key = `${canonical.info.sessionID}\0${canonicalText}`; + const filtered = messages.filter( + (message) => + !( + isOptimisticUserMessage(message) && + `${message.info.sessionID}\0${getMessageText(message)}` === key + ), + ); + return filtered.length === messages.length ? messages : filtered; +} + +export function createOptimisticUserMessage({ + id, + sessionID, + text, + createdAt, +}: { + id: string; + sessionID: string; + text: string; + createdAt: number; +}): MessageEntry { + const messageID = `${OPTIMISTIC_USER_PREFIX}${id}`; + return { + info: { + id: messageID, + sessionID, + role: "user", + time: { created: createdAt }, + } as Message, + parts: [ + { + id: `${messageID}:text`, + type: "text", + text, + sessionID, + messageID, + time: { start: createdAt, end: createdAt }, + } as Part, + ], + }; } -export function getPartOrderValue(part: Part): number { +function getPartOrderValue(part: Part): number { const timedPart = part as Part & { time?: { start?: number; end?: number } }; return timedPart.time?.start ?? timedPart.time?.end ?? 0; } @@ -248,6 +310,59 @@ export function normalizeMessageEntries( }); } +export function mergeMessageSnapshot( + incomingMessages: MessageEntry[], + existingMessages: MessageEntry[], +): MessageEntry[] { + const normalizedMessages = normalizeMessageEntries(incomingMessages, existingMessages); + if (normalizedMessages.length === 0) return limitMessageWindow(existingMessages); + + const canonicalUserTexts = new Set( + normalizedMessages + .filter((message) => message.info.role === "user") + .map((message) => `${message.info.sessionID}\0${getMessageText(message)}`), + ); + const optimisticIdsToDrop = new Set( + existingMessages + .filter( + (message) => + isOptimisticUserMessage(message) && + canonicalUserTexts.has(`${message.info.sessionID}\0${getMessageText(message)}`), + ) + .map((message) => message.info.id), + ); + + const existingByMsgId = new Map(); + for (const message of existingMessages) existingByMsgId.set(message.info.id, message); + + const mergedMessages = normalizedMessages.map((incoming) => { + const existing = existingByMsgId.get(incoming.info.id); + if (!existing) return incoming; + + const incomingPartIds = new Set(incoming.parts.map((part) => part.id)); + const preservedParts = existing.parts.filter((part) => !incomingPartIds.has(part.id)); + if (preservedParts.length === 0) return incoming; + + return { + ...incoming, + parts: [...incoming.parts, ...preservedParts].sort( + (a, b) => getPartOrderValue(a) - getPartOrderValue(b) || a.id.localeCompare(b.id), + ), + }; + }); + + const incomingIds = new Set(incomingMessages.map((message) => message.info.id)); + const serverLast = normalizedMessages[normalizedMessages.length - 1]; + const serverLastCreated = serverLast?.info.time.created ?? 0; + for (const entry of existingMessages) { + if (incomingIds.has(entry.info.id) || optimisticIdsToDrop.has(entry.info.id)) continue; + const entryCreated = entry.info.time.created ?? 0; + if (entryCreated > serverLastCreated) mergedMessages.push(entry); + } + + return limitMessageWindow(mergedMessages); +} + export function limitMessageWindow(messages: MessageEntry[]): MessageEntry[] { if (messages.length <= MAX_MESSAGE_WINDOW) return messages; return messages.slice(messages.length - MAX_MESSAGE_WINDOW); diff --git a/src/hooks/agent-reducer.test.ts b/src/hooks/agent-reducer.test.ts new file mode 100644 index 0000000..e8ef649 --- /dev/null +++ b/src/hooks/agent-reducer.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "@voidzero-dev/vite-plus-test"; +import type { AgentBackendId } from "@/agents"; +import type { Session } from "@/hooks/agent-state-types"; +import { mergeProjectBackendSessions } from "./agent-reducer"; + +function session(id: string, backendId: AgentBackendId, directory = "/repo", updated = 1): Session { + return { + id, + title: id, + directory, + _projectDir: directory, + _workspaceId: "workspace-1", + _backendId: backendId, + time: { created: updated, updated }, + } as Session; +} + +describe("mergeProjectBackendSessions", () => { + test("replaces only sessions from listed backends", () => { + const current = [session("open-old", "opencode"), session("pi-old", "pi")]; + const incoming = [session("pi-new", "pi", "/repo", 2)]; + + const merged = mergeProjectBackendSessions({ + current, + workspaceId: "workspace-1", + directory: "/repo", + incoming, + backendIds: ["pi"], + }); + + expect(merged.map((item) => item.id).sort()).toEqual(["open-old", "pi-new"]); + }); + + test("preserves sessions when backend listing failed", () => { + const current = [session("open-old", "opencode"), session("pi-old", "pi")]; + + const merged = mergeProjectBackendSessions({ + current, + workspaceId: "workspace-1", + directory: "/repo", + incoming: [], + backendIds: [], + }); + + expect(merged.map((item) => item.id).sort()).toEqual(["open-old", "pi-old"]); + }); + + test("incoming id wins even when previous copy belonged to another directory", () => { + const current = [session("same", "opencode", "/old", 1)]; + const incoming = [session("same", "opencode", "/repo", 2)]; + + const merged = mergeProjectBackendSessions({ + current, + workspaceId: "workspace-1", + directory: "/repo", + incoming, + backendIds: ["opencode"], + }); + + expect(merged).toHaveLength(1); + expect(merged[0]?._projectDir).toBe("/repo"); + }); +}); diff --git a/src/hooks/agent-reducer.ts b/src/hooks/agent-reducer.ts index 84200cf..d0c9507 100644 --- a/src/hooks/agent-reducer.ts +++ b/src/hooks/agent-reducer.ts @@ -17,17 +17,21 @@ import type { import { applyStreamingDeltaToPart, bufferNonActiveEvent, + createOptimisticUserMessage, createPlaceholderMessageEntry, createPlaceholderPart, getChildSessionId, limitMessageWindow, MAX_SESSION_BUFFER_CACHE, + mergeMessageSnapshot, mergeSnapshotPartWithExisting, normalizeMessageEntries, + removeMatchingOptimisticUserMessage, tagPartWithDeltaPositions, updateMessageArray, } from "@/hooks/agent-message-state"; import { + getSessionBackendId, getSessionSelectedAgent, getSessionSelectedModel, getSessionSelectedVariant, @@ -36,13 +40,11 @@ import { sortSessionsNewestFirst, } from "@/hooks/agent-session-utils"; import { - getStoredDefaultChatDirectory, normalizeWorkspace, persistProjectMetaMap, persistSessionMetaMap, persistWorktreeParents, type ProjectMeta, - type RecentProject, type SessionMeta, type WorktreeParentMap, } from "@/hooks/agent-state-persistence"; @@ -116,10 +118,6 @@ type Action = type: "REORDER_WORKSPACES"; payload: { fromIndex: number; toIndex: number }; } - | { - type: "REORDER_WORKSPACE_PROJECTS"; - payload: { workspaceId: string; fromIndex: number; toIndex: number }; - } | { type: "REORDER_VISIBLE_WORKSPACE_PROJECTS"; payload: { workspaceId: string; orderedDirectories: string[] }; @@ -136,11 +134,14 @@ type Action = type: "REMOVE_PROJECT"; payload: { projectKey: string; directory: string }; } - | { type: "CLEAR_ALL_PROJECTS" } - | { type: "SET_SESSIONS"; payload: Session[] } | { type: "MERGE_PROJECT_SESSIONS"; - payload: { projectKey: string; directory: string; sessions: Session[] }; + payload: { + projectKey: string; + directory: string; + sessions: Session[]; + backendIds?: AgentBackendId[]; + }; } | { type: "SET_ACTIVE_SESSION"; payload: string | null } | { type: "SET_SESSION_DRAFT"; payload: { key: string; text: string } } @@ -154,8 +155,11 @@ type Action = mode?: "replace" | "prepend" | "append"; }; } + | { + type: "PROMPT_SUBMITTED"; + payload: { id: string; sessionID: string; text: string; createdAt: number }; + } | { type: "SET_LOADING_OLDER_MESSAGES"; payload: boolean } - | { type: "SET_LOADING_NEWER_MESSAGES"; payload: boolean } | { type: "SET_BUSY"; payload: boolean } | { type: "TURN_RUN_STARTED"; @@ -234,8 +238,6 @@ type Action = payload: { sessionID: string; promptID: string; text: string }; } | { type: "QUEUE_CLEAR"; payload: { sessionID: string } } - | { type: "SET_RECENT_PROJECTS"; payload: RecentProject[] } - | { type: "SET_HOME_DIRECTORY"; payload: string | null } | { type: "SET_DEFAULT_CHAT_DIRECTORY"; payload: string | null } | { type: "START_DRAFT_SESSION"; @@ -244,9 +246,6 @@ type Action = | { type: "SET_DRAFT_DIRECTORY"; payload: string } | { type: "SET_DRAFT_BACKEND"; payload: AgentBackendId } | { type: "CLEAR_DRAFT_SESSION" } - | { type: "SET_DRAFT_TEMPORARY"; payload: boolean } - | { type: "MARK_SESSION_TEMPORARY"; payload: string } - | { type: "UNMARK_SESSION_TEMPORARY"; payload: string } | { type: "SET_SESSION_NAMING"; payload: { sessionId: string; naming: boolean } } | { type: "SET_SESSION_META"; @@ -261,7 +260,6 @@ type Action = payload: { worktreeDir: string; parentDir: string; branch: string }; } | { type: "UNREGISTER_WORKTREE"; payload: string } - | { type: "TOUCH_WORKTREE"; payload: string } | { type: "SET_PENDING_WORKTREE_CLEANUP"; payload: { worktreeDir: string; parentDir: string } | null; @@ -286,6 +284,35 @@ type Action = payload: { oldId: string; newId: string; session: Session }; }; +export function mergeProjectBackendSessions({ + current, + workspaceId, + directory, + incoming, + backendIds, +}: { + current: Session[]; + workspaceId: string; + directory: string; + incoming: Session[]; + backendIds?: AgentBackendId[]; +}) { + if (backendIds && backendIds.length === 0) return sortSessionsNewestFirst(current); + const backendScope = backendIds ? new Set(backendIds) : null; + const incomingIds = new Set(incoming.map((session) => session.id)); + return sortSessionsNewestFirst([ + ...current.filter((session) => { + if (incomingIds.has(session.id)) return false; + if (getSessionWorkspaceId(session) !== workspaceId) return true; + if ((session._projectDir ?? session.directory) !== directory) return true; + if (!backendScope) return false; + const backendId = getSessionBackendId(session); + return !backendId || !backendScope.has(backendId); + }), + ...incoming, + ]); +} + export function reducer(state: InternalAgentState, action: Action): InternalAgentState { switch (action.type) { case "SET_WORKSPACES": @@ -335,29 +362,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen return { ...state, workspaces: nextWorkspaces }; } - case "REORDER_WORKSPACE_PROJECTS": { - const { workspaceId, fromIndex, toIndex } = action.payload; - let changed = false; - const nextWorkspaces = state.workspaces.map((workspace) => { - if (workspace.id !== workspaceId) return workspace; - const projects = workspace.projects ?? []; - if (projects.length <= 1) return workspace; - if (fromIndex < 0 || fromIndex >= projects.length) return workspace; - const clampedTo = Math.max(0, Math.min(toIndex, projects.length - 1)); - if (clampedTo === fromIndex) return workspace; - const nextProjects = [...projects]; - const [moved] = nextProjects.splice(fromIndex, 1); - if (!moved) return workspace; - nextProjects.splice(clampedTo, 0, moved); - changed = true; - return { - ...workspace, - projects: nextProjects, - }; - }); - return changed ? { ...state, workspaces: nextWorkspaces } : state; - } - case "REORDER_VISIBLE_WORKSPACE_PROJECTS": { const { workspaceId, orderedDirectories } = action.payload; const orderedSet = new Set(orderedDirectories); @@ -537,12 +541,9 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen ? { activeSessionId: null, messages: [], - messageForwardBuffer: [], messageHistoryHasMore: false, messageHistoryCursor: null, - messageWindowHasNewer: false, isLoadingOlderMessages: false, - isLoadingNewerMessages: false, isBusy: false, } : {}), @@ -554,42 +555,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen }; } - case "CLEAR_ALL_PROJECTS": - return { - ...state, - connections: {}, - projectWorkspaceMap: {}, - sessions: [], - activeSessionId: null, - messages: [], - messageForwardBuffer: [], - messageHistoryHasMore: false, - messageHistoryCursor: null, - messageWindowHasNewer: false, - isLoadingMessages: false, - isLoadingOlderMessages: false, - isLoadingNewerMessages: false, - isBusy: false, - pendingPermissions: {}, - pendingQuestions: {}, - busySessionIds: new Set(), - temporarySessions: new Set(), - namingSessionIds: new Set(), - childSessions: {}, - trackedChildSessionIds: new Set(), - _pendingSnapshots: [], - _sessionBuffers: {}, - afterPartPending: new Set(), - _afterPartTriggered: new Set(), - _deletedSessionIds: new Set(), - draftSessionDirectory: null, - draftSessionBackendId: null, - draftIsTemporary: false, - }; - - case "SET_SESSIONS": - return { ...state, sessions: sortSessionsNewestFirst(action.payload) }; - case "SET_BOOT_STATE": { return { ...state, @@ -600,24 +565,17 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen } case "MERGE_PROJECT_SESSIONS": { - const { projectKey, directory, sessions } = action.payload; + const { projectKey, directory, sessions, backendIds } = action.payload; const { workspaceId } = parseProjectKey(projectKey); - const filtered = state.sessions.filter( - (s) => - !( - getSessionWorkspaceId(s) === workspaceId && (s._projectDir ?? s.directory) === directory - ), - ); - // Deduplicate by session ID: if the incoming batch contains a - // session that already exists under a *different* project - // directory (possible when directories share the same git repo / - // project_id on the server), keep the existing one and skip the - // duplicate from the new batch. - const existingIds = new Set(filtered.map((s) => s.id)); - const deduped = sessions.filter((s) => !existingIds.has(s.id)); return { ...state, - sessions: sortSessionsNewestFirst([...filtered, ...deduped]), + sessions: mergeProjectBackendSessions({ + current: state.sessions, + workspaceId, + directory, + incoming: sessions, + backendIds, + }), }; } @@ -744,13 +702,10 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen selectedAgent: nextSelectedAgent, variantSelections: nextVariantSelections, messages: initialMessages, - messageForwardBuffer: [], messageHistoryHasMore: restoredHasMore, messageHistoryCursor: restoredCursor, - messageWindowHasNewer: false, isLoadingMessages: sid !== null && !isCompleteBuffer, isLoadingOlderMessages: false, - isLoadingNewerMessages: false, isBusy: sid ? state.busySessionIds.has(sid) : false, unreadSessionIds: nextUnread, // Selecting a real session clears any pending draft @@ -809,42 +764,18 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen return { ...state, messages: limitMessageWindow(combinedMessages), - messageForwardBuffer: [], messageHistoryHasMore: false, messageHistoryCursor: null, - messageWindowHasNewer: false, - isLoadingNewerMessages: false, }; } - const existingByMsgId = new Map(); - for (const message of state.messages) { - existingByMsgId.set(message.info.id, message); - } - - if (normalizedMessages.length > 0) { - const serverLast = normalizedMessages[normalizedMessages.length - 1]; - const serverLastCreated = serverLast?.info.time.created ?? 0; - const incomingIds = new Set(action.payload.messages.map((message) => message.info.id)); - for (const [id, entry] of existingByMsgId) { - if (incomingIds.has(id)) continue; - const entryCreated = entry.info.time.created ?? 0; - if (entryCreated > serverLastCreated) { - normalizedMessages.push(entry); - } - } - } - let replayedState: InternalAgentState = { ...state, - messages: limitMessageWindow(normalizedMessages), - messageForwardBuffer: [], + messages: mergeMessageSnapshot(action.payload.messages, state.messages), messageHistoryHasMore: action.payload.hasMore, messageHistoryCursor: action.payload.nextCursor ?? null, - messageWindowHasNewer: false, isLoadingMessages: false, isLoadingOlderMessages: false, - isLoadingNewerMessages: false, _pendingSnapshots: [], }; for (const event of state._pendingSnapshots) { @@ -853,12 +784,24 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen return replayedState; } + case "PROMPT_SUBMITTED": { + const message = createOptimisticUserMessage(action.payload); + if (message.info.sessionID !== state.activeSessionId) { + return bufferNonActiveEvent(state, message.info.sessionID, message.info.id, () => ({ + info: message.info, + parts: Object.fromEntries(message.parts.map((part) => [part.id, part])), + })); + } + if (state.messages.some((entry) => entry.info.id === message.info.id)) return state; + return { + ...state, + messages: limitMessageWindow([...state.messages, message]), + }; + } + case "SET_LOADING_OLDER_MESSAGES": return { ...state, isLoadingOlderMessages: action.payload }; - case "SET_LOADING_NEWER_MESSAGES": - return { ...state, isLoadingNewerMessages: action.payload }; - case "SET_BUSY": return { ...state, isBusy: action.payload }; @@ -1057,8 +1000,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen const { [deletedId]: _deletedQueue, ...remainingQueues } = state.queuedPrompts; const { [deletedId]: _deletedBuffer, ...remainingBuffers } = state._sessionBuffers; - const nextTemp = new Set(state.temporarySessions); - nextTemp.delete(deletedId); const nextUnread = new Set(state.unreadSessionIds); nextUnread.delete(deletedId); const nextNaming = new Set(state.namingSessionIds); @@ -1117,7 +1058,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen sessions: state.sessions.filter((s) => s.id !== deletedId), queuedPrompts: remainingQueues, _sessionBuffers: remainingBuffers, - temporarySessions: nextTemp, unreadSessionIds: nextUnread, namingSessionIds: nextNaming, sessionDrafts: nextDrafts, @@ -1129,12 +1069,9 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen ? { activeSessionId: null, messages: [], - messageForwardBuffer: [], messageHistoryHasMore: false, messageHistoryCursor: null, - messageWindowHasNewer: false, isLoadingOlderMessages: false, - isLoadingNewerMessages: false, isBusy: false, } : {}), @@ -1176,7 +1113,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen ...state, ...turnPatch, messages: appendedMessages, - messageForwardBuffer: [], // If limitMessageWindow trimmed messages, older history exists ...(didTrim ? { messageHistoryHasMore: true } : {}), }; @@ -1276,14 +1212,17 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen } } - const partUpdatedMessages = limitMessageWindow(updatedWindow.messages); - const partDidTrim = partUpdatedMessages.length < updatedWindow.messages.length; + const canonicalEntry = updatedWindow.messages.find((m) => m.info.id === part.messageID); + const dedupedMessages = canonicalEntry + ? removeMatchingOptimisticUserMessage(updatedWindow.messages, canonicalEntry) + : updatedWindow.messages; + const partUpdatedMessages = limitMessageWindow(dedupedMessages); + const partDidTrim = partUpdatedMessages.length < dedupedMessages.length; return { ...state, ...childTrackPatch, ...afterPartPatch, messages: partUpdatedMessages, - messageForwardBuffer: [], // If limitMessageWindow trimmed messages, older history exists ...(partDidTrim ? { messageHistoryHasMore: true } : {}), }; @@ -1327,7 +1266,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen return { ...state, messages: deltaMessages, - messageForwardBuffer: [], // If limitMessageWindow trimmed messages, older history exists ...(deltaDidTrim ? { messageHistoryHasMore: true } : {}), }; @@ -1387,7 +1325,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen parts: m.parts.filter((p) => p.id !== partID), }; }), - messageForwardBuffer: [], }; } @@ -1428,7 +1365,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen return { ...state, messages: state.messages.filter((m) => m.info.id !== messageID), - messageForwardBuffer: [], }; } @@ -1619,20 +1555,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen return { ...state, _afterPartTriggered: next }; } - case "SET_RECENT_PROJECTS": - return { ...state, recentProjects: action.payload }; - - case "SET_HOME_DIRECTORY": { - const defaultChatDirectory = - getStoredDefaultChatDirectory() ?? - (action.payload ? normalizeProjectPath(action.payload) : null); - return { - ...state, - homeDirectory: action.payload, - defaultChatDirectory, - }; - } - case "SET_DEFAULT_CHAT_DIRECTORY": return { ...state, defaultChatDirectory: action.payload }; @@ -1643,13 +1565,10 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen draftSessionBackendId: action.payload.backendId, activeSessionId: null, messages: [], - messageForwardBuffer: [], messageHistoryHasMore: false, messageHistoryCursor: null, - messageWindowHasNewer: false, isLoadingMessages: false, isLoadingOlderMessages: false, - isLoadingNewerMessages: false, isBusy: false, }; @@ -1667,24 +1586,8 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen ...state, draftSessionDirectory: null, draftSessionBackendId: null, - draftIsTemporary: false, }; - case "SET_DRAFT_TEMPORARY": - return { ...state, draftIsTemporary: action.payload }; - - case "MARK_SESSION_TEMPORARY": { - const next = new Set(state.temporarySessions); - next.add(action.payload); - return { ...state, temporarySessions: next }; - } - - case "UNMARK_SESSION_TEMPORARY": { - const next = new Set(state.temporarySessions); - next.delete(action.payload); - return { ...state, temporarySessions: next }; - } - case "SET_SESSION_META": { const { sessionId, meta } = action.payload; const nextMeta = { ...state.sessionMeta }; @@ -1726,20 +1629,6 @@ export function reducer(state: InternalAgentState, action: Action): InternalAgen return { ...state, worktreeParents: next }; } - case "TOUCH_WORKTREE": { - const existing = state.worktreeParents[action.payload]; - if (!existing) return state; - const next: WorktreeParentMap = { - ...state.worktreeParents, - [action.payload]: { - ...existing, - lastOpenedAt: new Date().toISOString(), - }, - }; - persistWorktreeParents(next); - return { ...state, worktreeParents: next }; - } - case "SET_PENDING_WORKTREE_CLEANUP": return { ...state, pendingWorktreeCleanup: action.payload }; diff --git a/src/hooks/agent-session-utils.ts b/src/hooks/agent-session-utils.ts index ad0f415..7c0e3b4 100644 --- a/src/hooks/agent-session-utils.ts +++ b/src/hooks/agent-session-utils.ts @@ -1,7 +1,7 @@ import { normalizeProjectPath } from "@/lib/utils"; import type { AgentBackendId } from "@/agents"; import type { ProjectMetaMap } from "@/hooks/agent-state-persistence"; -import type { MessageEntry, Session } from "@/hooks/agent-state-types"; +import type { Session } from "@/hooks/agent-state-types"; import type { SelectedModel } from "@/types/electron"; const PROJECT_KEY_SEPARATOR = "\u0000"; @@ -65,7 +65,7 @@ export function shouldAutoNameSession(session: Session | undefined | null) { return !title || title.toLowerCase() === "untitled"; } -export function getSessionSortTime(session: Session): number { +function getSessionSortTime(session: Session): number { return session.time.updated ?? session.time.created ?? 0; } @@ -84,54 +84,3 @@ export function isHiddenProject( ): boolean { return projectMeta[makeProjectKey(workspaceId, directory)]?.hidden === true; } - -function extractMessageText(entry: MessageEntry): string { - const segments: string[] = []; - for (const part of entry.parts) { - if (part.type === "text" && typeof part.text === "string" && part.text.trim()) { - segments.push(part.text.trim()); - continue; - } - if (part.type === "tool") { - const toolName = part.tool || "tool"; - const status = part.state?.status || "completed"; - segments.push(`[tool:${toolName} ${status}]`); - } - } - return segments.join("\n\n").trim(); -} - -export function buildSessionMigrationPrompt(input: { - entries: MessageEntry[]; - sourceDirectory: string; - targetDirectory: string; - title?: string; -}): string { - const transcript = input.entries - .map((entry) => { - const text = extractMessageText(entry); - if (!text) return null; - const role = entry.info.role === "assistant" ? "Assistant" : "User"; - return `${role}:\n${text}`; - }) - .filter((value): value is string => Boolean(value)); - const MAX_MESSAGES = 12; - const selectedTranscript = transcript.slice(-MAX_MESSAGES); - const trimmedCount = transcript.length - selectedTranscript.length; - const transcriptBlock = selectedTranscript.join("\n\n---\n\n").slice(0, 12000); - const trimNotice = - trimmedCount > 0 ? `Earlier conversation omitted: ${trimmedCount} message(s).\n\n` : ""; - return [ - "INTERNAL_SESSION_MOVE_CONTEXT", - `This conversation was moved from \`${input.sourceDirectory}\` to \`${input.targetDirectory}\`.`, - "Work in the target directory from now on and treat the following transcript as prior context.", - "Do not ask the user to confirm the directory and do not mention this migration unless the user explicitly asks.", - "Do not perform a visible directory-check response for this migration step. Simply continue working in the new directory on the next real user message.", - input.title ? `Original title: ${input.title}` : null, - trimNotice ? trimNotice.trimEnd() : null, - "Transcript:", - transcriptBlock || "No prior transcript available.", - ] - .filter(Boolean) - .join("\n\n"); -} diff --git a/src/hooks/agent-state-persistence.ts b/src/hooks/agent-state-persistence.ts index 7ae66f7..e947dbe 100644 --- a/src/hooks/agent-state-persistence.ts +++ b/src/hooks/agent-state-persistence.ts @@ -1,15 +1,7 @@ -import { DEFAULT_SERVER_URL, MAX_RECENT_PROJECTS, STORAGE_KEYS } from "@/lib/constants"; +import { DEFAULT_SERVER_URL, STORAGE_KEYS } from "@/lib/constants"; import { persistOrRemoveJSON, storageGet, storageParsed, storageSetJSON } from "@/lib/safe-storage"; import { normalizeProjectPath } from "@/lib/utils"; -import type { ConnectionStatus, SelectedModel, Workspace } from "@/types/electron"; - -export interface RecentProject { - workspaceId?: string; - directory: string; - serverUrl: string; - username?: string; - lastConnected: number; -} +import type { SelectedModel, Workspace } from "@/types/electron"; export const NOTIFICATIONS_ENABLED_KEY = STORAGE_KEYS.NOTIFICATIONS_ENABLED; export const LOCAL_WORKSPACE_ID = "local"; @@ -102,11 +94,7 @@ export function getStoredDefaultChatDirectory(): string | null { return stored ? normalizeProjectPath(stored) : null; } -export function resolveDefaultChatDirectory(homeDir: string | null): string | null { - return getStoredDefaultChatDirectory() ?? (homeDir ? normalizeProjectPath(homeDir) : null); -} - -export interface WorktreeMetadata { +interface WorktreeMetadata { parentDir: string; branch: string; createdAt: string; @@ -115,10 +103,6 @@ export interface WorktreeMetadata { export type WorktreeParentMap = Record; -export function getWorktreeParentDir(map: WorktreeParentMap, dir: string): string | undefined { - return map[dir]?.parentDir; -} - export function getWorktreeParents(): WorktreeParentMap { const raw = storageParsed>(STORAGE_KEYS.WORKTREE_PARENTS) ?? {}; const result: WorktreeParentMap = {}; @@ -250,46 +234,6 @@ export function getActiveWorkspaceId(workspaces: Workspace[]) { return workspaces[0]?.id ?? LOCAL_WORKSPACE_ID; } -export function getRecentProjects(): RecentProject[] { - const projects = storageParsed(STORAGE_KEYS.RECENT_PROJECTS) ?? []; - return projects - .map((project) => ({ - ...project, - directory: normalizeProjectPath(project.directory), - serverUrl: project.serverUrl.replace(/\/+$/, ""), - username: project.username?.trim() || undefined, - workspaceId: project.workspaceId?.trim() || undefined, - })) - .filter((project) => !!project.directory); -} - -export function addRecentProject(project: RecentProject): RecentProject[] { - const normalizedDirectory = normalizeProjectPath(project.directory); - const normalizedServerUrl = project.serverUrl.replace(/\/+$/, ""); - const normalizedUsername = project.username?.trim() || undefined; - const normalizedWorkspaceId = project.workspaceId?.trim() || undefined; - const existing = getRecentProjects().filter((candidate) => { - return !( - (candidate.workspaceId?.trim() || undefined) === normalizedWorkspaceId && - normalizeProjectPath(candidate.directory) === normalizedDirectory && - candidate.serverUrl.replace(/\/+$/, "") === normalizedServerUrl && - (candidate.username?.trim() || undefined) === normalizedUsername - ); - }); - const updated = [ - { - ...project, - workspaceId: normalizedWorkspaceId, - directory: normalizedDirectory, - serverUrl: normalizedServerUrl, - username: normalizedUsername, - }, - ...existing, - ].slice(0, MAX_RECENT_PROJECTS); - storageSetJSON(STORAGE_KEYS.RECENT_PROJECTS, updated); - return updated; -} - export function getUnreadSessionIds(): Set { const arr = storageParsed(STORAGE_KEYS.UNREAD_SESSIONS); return arr ? new Set(arr) : new Set(); @@ -303,7 +247,3 @@ export function areNotificationsEnabled(): boolean { const raw = storageGet(STORAGE_KEYS.NOTIFICATIONS_ENABLED); return raw === null || raw === "true"; } - -export function hasAnyConnection(connections: Record): boolean { - return Object.values(connections).some((c) => c.state === "connected"); -} diff --git a/src/hooks/agent-state-types.ts b/src/hooks/agent-state-types.ts index c61d8e8..4196340 100644 --- a/src/hooks/agent-state-types.ts +++ b/src/hooks/agent-state-types.ts @@ -11,7 +11,6 @@ import type { import type { AgentBackendId } from "@/agents"; import type { ProjectMetaMap, - RecentProject, SessionMetaMap, WorktreeParentMap, } from "@/hooks/agent-state-persistence"; @@ -70,20 +69,14 @@ export interface InternalAgentState { activeSessionId: string | null; /** Messages for the active session */ messages: MessageEntry[]; - /** Newer active-session messages trimmed out of the current window */ - messageForwardBuffer: MessageEntry[]; /** Whether older messages exist before the loaded active-session window */ messageHistoryHasMore: boolean; /** Opaque cursor for fetching the next page of older messages */ messageHistoryCursor: string | null; - /** Whether newer messages exist after the loaded active-session window */ - messageWindowHasNewer: boolean; /** Whether messages are being fetched for a newly selected session */ isLoadingMessages: boolean; /** Whether older messages are currently being prepended to the active window */ isLoadingOlderMessages: boolean; - /** Whether newer messages are currently being restored into the active window */ - isLoadingNewerMessages: boolean; /** Whether a prompt response is in-flight */ isBusy: boolean; /** Pending permission requests keyed by sessionID */ @@ -116,20 +109,12 @@ export interface InternalAgentState { commands: Command[]; /** Per-session queued prompts (sent automatically when session becomes idle) */ queuedPrompts: Record; - /** Recently opened projects */ - recentProjects: RecentProject[]; - /** User home directory used as fallback for chat-first mode. */ - homeDirectory: string | null; /** Default working directory for chats started from the global chat entry. */ defaultChatDirectory: string | null; /** Directory for a draft (not-yet-created) session. Null when no draft is active. */ draftSessionDirectory: string | null; /** Backend chosen for draft/new session. */ draftSessionBackendId: AgentBackendId | null; - /** Whether the current draft should create a temporary (non-persisted) session */ - draftIsTemporary: boolean; - /** Set of session IDs marked as temporary (auto-deleted on navigate away) */ - temporarySessions: Set; /** Set of session IDs that are waiting for generated title */ namingSessionIds: Set; /** Set of session IDs that have unread content (finished generating while not active) */ @@ -200,5 +185,4 @@ export interface InternalAgentState { } export type AgentBackendState = InternalAgentState; -export type OpenCodeState = InternalAgentState; export type { QueueMode, QueuedPrompt } from "@/lib/session-drafts"; diff --git a/src/hooks/use-agent-backend.ts b/src/hooks/use-agent-backend.ts index 62e4ebf..16c2a18 100644 --- a/src/hooks/use-agent-backend.ts +++ b/src/hooks/use-agent-backend.ts @@ -3,9 +3,9 @@ import type { AgentBackendId } from "@/agents"; import { AGENT_BACKEND_IDS, getAllAgentBackends, getCurrentAgentBackend } from "@/agents"; import { useSessionState } from "@/hooks/use-agent-state"; import { STORAGE_KEYS } from "@/lib/constants"; -import { onSettingsChange, storageGet, storageSet } from "@/lib/safe-storage"; +import { onSettingsChange, storageGet } from "@/lib/safe-storage"; -export function getStoredAgentBackendId(): AgentBackendId { +function getStoredAgentBackendId(): AgentBackendId { const stored = storageGet(STORAGE_KEYS.AGENT_BACKEND); if (stored === "claude-code") return "claude-code"; if (stored === "pi") return "pi"; @@ -13,10 +13,6 @@ export function getStoredAgentBackendId(): AgentBackendId { return "opencode"; } -export function setStoredAgentBackendId(backendId: AgentBackendId) { - storageSet(STORAGE_KEYS.AGENT_BACKEND, backendId); -} - export function useCurrentAgentBackendId() { const [backendId, setBackendId] = useState(() => getStoredAgentBackendId()); @@ -42,7 +38,7 @@ export function useCurrentAgentBackendId() { return backendId; } -export function useAllAgentBackends() { +function useAllAgentBackends() { return useMemo(() => { const all = getAllAgentBackends(window.electronAPI); return Object.fromEntries( @@ -51,7 +47,7 @@ export function useAllAgentBackends() { }, []); } -export function useActiveAgentBackendId() { +function useActiveAgentBackendId() { const preferredBackendId = useCurrentAgentBackendId(); const { sessions, activeSessionId, draftSessionBackendId } = useSessionState(); const activeSession = sessions.find((session) => session.id === activeSessionId); diff --git a/src/hooks/use-agent-impl-core.tsx b/src/hooks/use-agent-impl-core.tsx index e953b49..f52b0fc 100644 --- a/src/hooks/use-agent-impl-core.tsx +++ b/src/hooks/use-agent-impl-core.tsx @@ -8,7 +8,7 @@ * Uses v2 SDK types which include variant support on models. */ -import type { Agent, Part, Provider, QuestionAnswer } from "@opencode-ai/sdk/v2/client"; +import type { Agent, Provider, QuestionAnswer } from "@opencode-ai/sdk/v2/client"; import { type ReactNode, @@ -35,10 +35,8 @@ import { variantKey, } from "@/hooks/use-agent-variant-core"; import { - addRecentProject, getActiveWorkspaceId, getProjectMetaMap, - getRecentProjects, getSessionMetaMap, getStoredDefaultChatDirectory, getUnreadSessionIds, @@ -51,13 +49,10 @@ import { createLocalWorkspace, persistUnreadSessionIds, persistWorkspaces, - resolveDefaultChatDirectory, type SessionColor, } from "@/hooks/agent-state-persistence"; import { getChildSessionId, - getMessageCreatedAt, - getPartOrderValue, MESSAGE_PAGE_SIZE, tagPartWithDeltaPositions, } from "@/hooks/agent-message-state"; @@ -96,13 +91,9 @@ import { type SessionContextValue, } from "@/hooks/agent-contexts"; export { - getWorktreeParentDir, - hasAnyConnection, LOCAL_WORKSPACE_ID, NOTIFICATIONS_ENABLED_KEY, - resolveDefaultChatDirectory, type SessionColor, - type WorktreeMetadata, } from "@/hooks/agent-state-persistence"; import { DEFAULT_SERVER_URL, STORAGE_KEYS } from "@/lib/constants"; import { @@ -281,13 +272,10 @@ const initialState: InternalAgentState = { sessions: [], activeSessionId: null, messages: [], - messageForwardBuffer: [], messageHistoryHasMore: false, messageHistoryCursor: null, - messageWindowHasNewer: false, isLoadingMessages: false, isLoadingOlderMessages: false, - isLoadingNewerMessages: false, isBusy: false, pendingPermissions: {}, pendingQuestions: {}, @@ -304,13 +292,9 @@ const initialState: InternalAgentState = { variantSelections: {}, commands: [], queuedPrompts: getQueuedPrompts(), - recentProjects: getRecentProjects(), - homeDirectory: null, defaultChatDirectory: getStoredDefaultChatDirectory(), draftSessionDirectory: null, draftSessionBackendId: null, - draftIsTemporary: false, - temporarySessions: new Set(), namingSessionIds: new Set(), unreadSessionIds: getUnreadSessionIds(), sessionDrafts: getSessionDrafts(), @@ -333,28 +317,7 @@ const initialState: InternalAgentState = { // Actions // --------------------------------------------------------------------------- -export function getChildSessionParts( - childSessions: InternalAgentState["childSessions"], - childSessionId: string, -): Part[] { - const child = childSessions[childSessionId]; - if (!child) return []; - - return Object.values(child) - .toSorted((a, b) => getMessageCreatedAt(a) - getMessageCreatedAt(b)) - .filter((m) => m.info.role !== "user") - .flatMap((m) => - Object.values(m.parts) - .toSorted((a, b) => getPartOrderValue(a) - getPartOrderValue(b)) - .filter((p) => { - if (p.type === "tool") return true; - if (p.type === "text" && "text" in p && p.text) return true; - return false; - }), - ); -} - -export function InternalAgentProvider({ +function InternalAgentProvider({ children, detachedProject, }: { @@ -690,26 +653,6 @@ export function InternalAgentProvider({ } }, []); - useEffect(() => { - let cancelled = false; - window.electronAPI - ?.getHomeDir?.() - .then((dir) => { - if (cancelled) return; - dispatch({ - type: "SET_HOME_DIRECTORY", - payload: dir ? normalizeProjectPath(dir) : null, - }); - }) - .catch(() => { - if (cancelled) return; - dispatch({ type: "SET_HOME_DIRECTORY", payload: null }); - }); - return () => { - cancelled = true; - }; - }, []); - const { currentVariant, setModel, @@ -864,6 +807,7 @@ export function InternalAgentProvider({ dispatch({ type: "SET_COMMANDS", payload: commandsData }); } catch (error) { + if (requestId !== resourceLoadRequestRef.current) return; dispatch({ type: "SET_ERROR", payload: getErrorMessage(error), @@ -874,7 +818,10 @@ export function InternalAgentProvider({ ); const addProject = useCallback( - async (config: ConnectionConfig, options?: { suppressError?: boolean; hidden?: boolean }) => { + async ( + config: ConnectionConfig, + options?: { suppressError?: boolean; hidden?: boolean; backendIds?: AgentBackendId[] }, + ) => { if (allBackends.length === 0 || !config.directory) return; const workspaceId = config.workspaceId ?? stateRef.current.activeWorkspaceId ?? LOCAL_WORKSPACE_ID; @@ -904,8 +851,15 @@ export function InternalAgentProvider({ }, }, }); + const targetBackends = options?.backendIds?.length + ? allBackends.filter((backend) => + options.backendIds?.includes(backend.id as AgentBackendId), + ) + : allBackends; + if (targetBackends.length === 0) return; + const backendConnectResults = await Promise.allSettled( - allBackends.map(async (backend) => { + targetBackends.map(async (backend) => { await backend.host.addProject({ ...config, workspaceId }); return backend; }), @@ -942,23 +896,32 @@ export function InternalAgentProvider({ const sessionResults = await Promise.all( connectedBackends.map(async (backend) => { try { - return await backend.runtime.listSessions({ - directory: config.directory, - workspaceId, - }); + return { + backendId: backend.id as AgentBackendId, + sessions: await backend.runtime.listSessions({ + directory: config.directory, + workspaceId, + }), + }; } catch { - return [] as Session[]; + return null; } }), ); - dispatch({ - type: "MERGE_PROJECT_SESSIONS", - payload: { - projectKey, - directory: config.directory, - sessions: sessionResults.flat() as Session[], - }, - }); + const successfulSessionResults = sessionResults.filter( + (result): result is { backendId: AgentBackendId; sessions: Session[] } => result !== null, + ); + if (successfulSessionResults.length > 0) { + dispatch({ + type: "MERGE_PROJECT_SESSIONS", + payload: { + projectKey, + directory: config.directory, + sessions: successfulSessionResults.flatMap((result) => result.sessions), + backendIds: successfulSessionResults.map((result) => result.backendId), + }, + }); + } try { const statuses = Object.fromEntries( ( @@ -985,15 +948,6 @@ export function InternalAgentProvider({ } catch { /* ignore – spinner will appear on next backend event */ } - if (loadedResourceProjectKeyRef.current === null) { - await loadServerResources( - connectedBackends.some((backend) => backend.id === preferredBackendId) - ? preferredBackendId - : (connectedBackends[0]?.id as AgentBackendId), - config.directory, - workspaceId, - ); - } const worktreeParentMap = getWorktreeParents(); const isWorktree = Boolean(worktreeParentMap[config.directory]); const workspaceDirectory = isWorktree @@ -1015,19 +969,8 @@ export function InternalAgentProvider({ storageSet(STORAGE_KEYS.SERVER_URL, config.baseUrl); storageSetOrRemove(STORAGE_KEYS.USERNAME, config.username); } - // Update recent projects only for the workspace root, not worktrees. - if (config.directory && !isWorktree && !options?.hidden) { - const updated = addRecentProject({ - workspaceId, - directory: config.directory, - serverUrl: config.baseUrl, - username: config.username, - lastConnected: Date.now(), - }); - dispatch({ type: "SET_RECENT_PROJECTS", payload: updated }); - } }, - [allBackends, loadServerResources, preferredBackendId], + [allBackends], ); const ensureDirectoryConnection = useCallback( @@ -1211,16 +1154,46 @@ export function InternalAgentProvider({ } } - // Connect all projects in parallel instead of sequentially - await Promise.allSettled( - allProjectConfigs.map((config) => addProject(config, { suppressError: true })), - ); + if (cancelled) return; + dispatch({ type: "SET_BOOT_STATE", payload: { state: "ready" } }); + + const startupBackendId = backendsById[preferredBackendId] + ? preferredBackendId + : ((allBackends[0]?.id ?? "opencode") as AgentBackendId); + + // Warm saved projects in background. Do not keep startup banner blocked by + // session scans or heavyweight CLI runtimes. Load the selected backend first, + // then hydrate the other backends slowly so their sessions still appear. + void (async () => { + await Promise.allSettled( + allProjectConfigs.map((config) => + addProject(config, { + suppressError: true, + backendIds: [startupBackendId], + }), + ), + ); + + await new Promise((resolve) => setTimeout(resolve, 500)); + + const deferredBackendIds = allBackends + .map((backend) => backend.id as AgentBackendId) + .filter((backendId) => backendId !== startupBackendId); + + for (const backendId of deferredBackendIds) { + for (const config of allProjectConfigs) { + if (cancelled) return; + await addProject(config, { + suppressError: true, + backendIds: [backendId], + }); + } + } + })(); } catch { /* ignore localStorage errors */ + if (!cancelled) dispatch({ type: "SET_BOOT_STATE", payload: { state: "ready" } }); } - - if (cancelled) return; - dispatch({ type: "SET_BOOT_STATE", payload: { state: "ready" } }); }; void bootstrap(); @@ -1228,7 +1201,7 @@ export function InternalAgentProvider({ return () => { cancelled = true; }; - }, [allBackends, addProject, detachedProject]); + }, [allBackends, addProject, backendsById, detachedProject, preferredBackendId]); useEffect(() => { if (!bridge || detachedProject) return; @@ -1244,11 +1217,12 @@ export function InternalAgentProvider({ type: "START_DRAFT_SESSION", payload: { directory: state.defaultChatDirectory, - backendId: "opencode", + backendId: preferredBackendId, }, }); }, [ detachedProject, + preferredBackendId, state.activeSessionId, state.draftSessionDirectory, state.defaultChatDirectory, @@ -1406,16 +1380,6 @@ export function InternalAgentProvider({ state.connections, ]); - const disconnect = useCallback(async () => { - if (allBackends.length === 0) return; - await Promise.all(allBackends.map((backend) => backend.host.disconnect())); - cleanupSessionRefs(); - expectedDirectoriesRef.current.clear(); - loadedResourceProjectKeyRef.current = null; - loadedResourceBackendIdRef.current = null; - dispatch({ type: "CLEAR_ALL_PROJECTS" }); - }, [allBackends, cleanupSessionRefs]); - const openDirectory = useCallback(async (): Promise => { if (!(workspaceProfile?.kind === "local-cli" || activeWorkspace?.isLocal)) { return null; @@ -1518,41 +1482,6 @@ export function InternalAgentProvider({ [addProject, backendsById, preferredBackendId, connectedDirectorySet], ); - const refreshSessions = useCallback(async () => { - if (allBackends.length === 0) return; - const projectKeys = Object.keys(stateRef.current.projectWorkspaceMap); - if (projectKeys.length === 0) { - dispatch({ type: "SET_SESSIONS", payload: [] }); - return; - } - const projectResults = await Promise.all( - projectKeys.map(async (projectKey) => { - const { directory, workspaceId } = parseProjectKey(projectKey); - const sessions = ( - await Promise.all( - allBackends.map(async (backend) => { - try { - return await backend.runtime.listSessions({ - directory, - workspaceId, - }); - } catch { - return [] as Session[]; - } - }), - ) - ).flat(); - return { projectKey, directory, sessions }; - }), - ); - for (const result of projectResults) { - dispatch({ - type: "MERGE_PROJECT_SESSIONS", - payload: result, - }); - } - }, [allBackends]); - // Single ref to avoid stale closures and prevent unnecessary callback recreation const stateRef = useRef(state); stateRef.current = state; @@ -1621,22 +1550,6 @@ export function InternalAgentProvider({ [forceSessionTitle, resolveCurrentSessionId], ); - /** Best-effort cleanup of a temporary session if it exists. */ - const cleanupTemporarySession = useCallback( - (excludeId?: string | null) => { - const prevId = stateRef.current.activeSessionId; - if (prevId && prevId !== excludeId && stateRef.current.temporarySessions.has(prevId)) { - dispatch({ type: "SESSION_DELETED", payload: prevId }); - getBackendForSessionId(prevId) - ?.runtime.deleteSession(prevId) - .catch(() => { - /* best-effort cleanup of temporary session */ - }); - } - }, - [getBackendForSessionId], - ); - const fetchMessagePage = useCallback( async ( sessionId: string, @@ -1742,8 +1655,6 @@ export function InternalAgentProvider({ async (id: string | null, options?: { session?: Session | null }) => { if (id === stateRef.current.activeSessionId) return; - cleanupTemporarySession(id); - const applySelectionFromMessages = (messages: MessageEntry[]) => { const derived = deriveSelectionFromMessages(messages); if (!derived?.selectedModel) return; @@ -1822,7 +1733,7 @@ export function InternalAgentProvider({ workspaceId: projectTarget?.workspaceId, }); }, - [cleanupTemporarySession, fetchMessagePage, hydrateChildSessionsForMessages], + [fetchMessagePage, hydrateChildSessionsForMessages], ); const refreshActiveSessionMessages = useCallback( @@ -1929,10 +1840,6 @@ export function InternalAgentProvider({ } }, [fetchMessagePage]); - const loadNewerMessages = useCallback(async (): Promise => { - return false; - }, []); - const createSession = useCallback( async (title?: string, directory?: string): Promise => { const targetBackendId = @@ -2075,7 +1982,6 @@ export function InternalAgentProvider({ if (!sessionId && draftDirectory) { if (draftCreatingRef.current) return null; draftCreatingRef.current = true; - const wasTemporary = stateRef.current.draftIsTemporary; try { const newSession = await createSession(undefined, draftDirectory); if (!newSession) { @@ -2084,12 +1990,6 @@ export function InternalAgentProvider({ } dispatch({ type: "CLEAR_DRAFT_SESSION" }); sessionId = newSession.id; - if (wasTemporary) { - dispatch({ - type: "MARK_SESSION_TEMPORARY", - payload: newSession.id, - }); - } } catch { draftCreatingRef.current = false; return null; @@ -2165,17 +2065,28 @@ export function InternalAgentProvider({ state.agents, state.selectedAgent, ); + const turnId = crypto.randomUUID(); + const startedAt = Date.now(); dispatch({ type: "TURN_RUN_STARTED", payload: { - id: crypto.randomUUID(), + id: turnId, sessionID: sessionId, - startedAt: Date.now(), + startedAt, providerID: model?.providerID, modelID: model?.modelID, thinkingLevel: variant, }, }); + dispatch({ + type: "PROMPT_SUBMITTED", + payload: { + id: turnId, + sessionID: sessionId, + text, + createdAt: startedAt, + }, + }); const projectTarget = getSessionProjectTarget( stateRef.current.sessions.find((session) => session.id === sessionId), @@ -2256,7 +2167,6 @@ export function InternalAgentProvider({ agentsRef.current, selectedAgentRef.current, ); - const wasTemporary = stateRef.current.draftIsTemporary; const startedAt = Date.now(); try { const session = await runtime.startSession({ @@ -2296,12 +2206,6 @@ export function InternalAgentProvider({ }); } dispatch({ type: "CLEAR_DRAFT_SESSION" }); - if (wasTemporary) { - dispatch({ - type: "MARK_SESSION_TEMPORARY", - payload: session.id, - }); - } await selectSession(session.id, { session: titledSession }); scheduleSessionMessageReconcile(session.id, { directory: session.directory, @@ -2723,13 +2627,12 @@ export function InternalAgentProvider({ } dispatch({ type: "SET_DEFAULT_CHAT_DIRECTORY", - payload: normalizedDirectory ?? resolveDefaultChatDirectory(stateRef.current.homeDirectory), + payload: normalizedDirectory, }); }, []); const startDraftSession = useCallback( (directory: string) => { - cleanupTemporarySession(); dispatch({ type: "START_DRAFT_SESSION", payload: { @@ -2738,7 +2641,7 @@ export function InternalAgentProvider({ }, }); }, - [activeSession, cleanupTemporarySession, preferredBackendId], + [activeSession, preferredBackendId], ); const startNewChat = useCallback(async () => { @@ -2756,10 +2659,6 @@ export function InternalAgentProvider({ dispatch({ type: "SET_DRAFT_BACKEND", payload: backendId }); }, []); - const setDraftTemporary = useCallback((temporary: boolean) => { - dispatch({ type: "SET_DRAFT_TEMPORARY", payload: temporary }); - }, []); - /** Re-fetch providers from the server and update global state. */ const refreshProviders = useCallback(async () => { await loadServerResources( @@ -3021,12 +2920,6 @@ export function InternalAgentProvider({ dispatch({ type: "UNREGISTER_WORKTREE", payload: normalizedWorktreeDir }); }, []); - const touchWorktree = useCallback((worktreeDir: string) => { - const normalizedWorktreeDir = normalizeProjectPath(worktreeDir); - if (!normalizedWorktreeDir) return; - dispatch({ type: "TOUCH_WORKTREE", payload: normalizedWorktreeDir }); - }, []); - const clearWorktreeCleanup = useCallback(() => { dispatch({ type: "SET_PENDING_WORKTREE_CLEANUP", payload: null }); }, []); @@ -3124,15 +3017,6 @@ export function InternalAgentProvider({ }); }, []); - const reorderProjects = useCallback((fromIndex: number, toIndex: number) => { - const workspaceId = stateRef.current.activeWorkspaceId; - if (!workspaceId) return; - dispatch({ - type: "REORDER_WORKSPACE_PROJECTS", - payload: { workspaceId, fromIndex, toIndex }, - }); - }, []); - const reorderVisibleProjects = useCallback((orderedDirectories: string[]) => { const workspaceId = stateRef.current.activeWorkspaceId; if (!workspaceId) return; @@ -3161,14 +3045,11 @@ export function InternalAgentProvider({ pendingQuestions: state.pendingQuestions, draftSessionDirectory: state.draftSessionDirectory, draftSessionBackendId: state.draftSessionBackendId, - draftIsTemporary: state.draftIsTemporary, - temporarySessions: state.temporarySessions, namingSessionIds: state.namingSessionIds, unreadSessionIds: state.unreadSessionIds, sessionDrafts: state.sessionDrafts, sessionMeta: state.sessionMeta, childSessions: state.childSessions, - recentProjects: state.recentProjects, }), [ activeWorkspaceSessions, @@ -3182,14 +3063,11 @@ export function InternalAgentProvider({ state.pendingQuestions, state.draftSessionDirectory, state.draftSessionBackendId, - state.draftIsTemporary, - state.temporarySessions, state.namingSessionIds, state.unreadSessionIds, state.sessionDrafts, state.sessionMeta, state.childSessions, - state.recentProjects, ], ); @@ -3205,9 +3083,7 @@ export function InternalAgentProvider({ : {}, childSessions: state.childSessions, messageHistoryHasMore: state.messageHistoryHasMore, - messageWindowHasNewer: state.messageWindowHasNewer, isLoadingOlderMessages: state.isLoadingOlderMessages, - isLoadingNewerMessages: state.isLoadingNewerMessages, }), [ state.messages, @@ -3215,9 +3091,7 @@ export function InternalAgentProvider({ state.turnRuns, state.childSessions, state.messageHistoryHasMore, - state.messageWindowHasNewer, state.isLoadingOlderMessages, - state.isLoadingNewerMessages, ], ); @@ -3287,9 +3161,6 @@ export function InternalAgentProvider({ workspaceServerUrl: workspaceProfile?.fields.serverUrl ? (activeWorkspace?.serverUrl ?? workspaceConnection?.serverUrl ?? null) : null, - workspaceUsername: workspaceProfile?.fields.username - ? (activeWorkspace?.username ?? null) - : null, isLocalWorkspace: workspaceProfile?.kind === "local-cli" ? true @@ -3338,13 +3209,9 @@ export function InternalAgentProvider({ const actionsCtx = useMemo( () => ({ - addProject, removeProject, - disconnect, selectSession, loadOlderMessages, - loadNewerMessages, - createSession, deleteSession, renameSession, sendPrompt, @@ -3361,7 +3228,6 @@ export function InternalAgentProvider({ revertVariant: doRevertVariant, clearError, refreshProviders, - refreshSessions, getQueuedPrompts, removeFromQueue, reorderQueue, @@ -3376,7 +3242,6 @@ export function InternalAgentProvider({ setDefaultChatDirectory, setDraftDirectory, setDraftBackend, - setDraftTemporary, revertToMessage, unrevert, forkFromMessage, @@ -3387,24 +3252,18 @@ export function InternalAgentProvider({ setProjectPinned, registerWorktree, unregisterWorktree, - touchWorktree, clearWorktreeCleanup, createWorkspace, updateWorkspace, removeWorkspace, switchWorkspace, reorderWorkspaces, - reorderProjects, reorderVisibleProjects, }), [ - addProject, removeProject, - disconnect, selectSession, loadOlderMessages, - loadNewerMessages, - createSession, deleteSession, renameSession, sendPrompt, @@ -3420,7 +3279,6 @@ export function InternalAgentProvider({ doRevertVariant, clearError, refreshProviders, - refreshSessions, getQueuedPrompts, removeFromQueue, reorderQueue, @@ -3435,7 +3293,6 @@ export function InternalAgentProvider({ setDefaultChatDirectory, setDraftDirectory, setDraftBackend, - setDraftTemporary, revertToMessage, unrevert, forkFromMessage, @@ -3446,33 +3303,16 @@ export function InternalAgentProvider({ setProjectPinned, registerWorktree, unregisterWorktree, - touchWorktree, clearWorktreeCleanup, createWorkspace, updateWorkspace, removeWorkspace, switchWorkspace, reorderWorkspaces, - reorderProjects, reorderVisibleProjects, ], ); - // Clean up temporary sessions on window unload (app close / refresh) - useEffect(() => { - const cleanup = () => { - for (const id of stateRef.current.temporarySessions) { - getBackendForSessionId(id) - ?.runtime.deleteSession(id) - .catch(() => { - /* best-effort cleanup on unload */ - }); - } - }; - window.addEventListener("beforeunload", cleanup); - return () => window.removeEventListener("beforeunload", cleanup); - }, [getBackendForSessionId]); - return ( @@ -3560,6 +3400,5 @@ export function useActions(): ActionsContextValue { } // Compatibility aliases. App-facing code should prefer generic names. -export const OpenCodeProvider = InternalAgentProvider; export const AgentBackendProvider = InternalAgentProvider; export type AgentBackendState = InternalAgentState; diff --git a/src/hooks/use-agent-state.ts b/src/hooks/use-agent-state.ts index b1b05d6..f3d4033 100644 --- a/src/hooks/use-agent-state.ts +++ b/src/hooks/use-agent-state.ts @@ -1,8 +1,5 @@ export { AgentBackendProvider, - getChildSessionParts, - getWorktreeParentDir, - hasAnyConnection, LOCAL_WORKSPACE_ID, NOTIFICATIONS_ENABLED_KEY, resolveServerDefaultModel, @@ -21,4 +18,4 @@ export type { Session, } from "./agent-state-types"; -export type { SessionColor, WorktreeMetadata } from "./agent-state-persistence"; +export type { SessionColor } from "./agent-state-persistence"; diff --git a/src/hooks/use-agent-variant-core.ts b/src/hooks/use-agent-variant-core.ts index 7cd74db..4e679f4 100644 --- a/src/hooks/use-agent-variant-core.ts +++ b/src/hooks/use-agent-variant-core.ts @@ -34,7 +34,7 @@ export function updateVariantSelections( return next; } -export function getEnabledVariantKeys(model: Model | undefined): string[] { +function getEnabledVariantKeys(model: Model | undefined): string[] { if (!model?.variants) return []; return Object.keys(model.variants).filter((key) => !model.variants?.[key]?.disabled); } diff --git a/src/index.ts b/src/index.ts index 50f7cd9..4eb4b47 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,4 +17,4 @@ const server = serve({ }, }); -console.log(`Server running at ${server.url}`); +console.info(`Server running at ${server.url}`); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 724f52c..0c22815 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -9,7 +9,7 @@ // Server defaults // --------------------------------------------------------------------------- -export const DEFAULT_SERVER_PORT = 4096; +const DEFAULT_SERVER_PORT = 4096; export const DEFAULT_SERVER_URL = `http://127.0.0.1:${DEFAULT_SERVER_PORT}`; // --------------------------------------------------------------------------- @@ -25,7 +25,6 @@ export const STORAGE_KEYS = { SELECTED_MODEL: "opencode:selectedModel", SELECTED_AGENT: "opencode:selectedAgent", VARIANT_SELECTIONS: "opencode:variantSelections", - RECENT_PROJECTS: "opencode:recentProjects", UNREAD_SESSIONS: "opencode:unreadSessionIds", SESSION_DRAFTS: "opencode:sessionDrafts", SESSION_DRAFT_IMAGES: "opencode:sessionDraftImages", @@ -54,12 +53,6 @@ export const STORAGE_KEYS = { // UI timing (ms) // --------------------------------------------------------------------------- -/** Debounce delay before syntax-highlighting fires (ms). */ -export const HIGHLIGHT_DEBOUNCE_MS = 150; - -/** Duration the "Copied!" badge stays visible after a clipboard copy (ms). */ -export const COPY_FEEDBACK_MS = 2000; - /** Delay before sending a prompt after a merge operation (ms). */ export const POST_MERGE_DELAY_MS = 300; @@ -72,9 +65,6 @@ export const MAX_TEXTAREA_HEIGHT_PX = 200; /** Number of sessions to show per "page" in the sidebar. */ export const SESSION_PAGE_SIZE = 5; -/** Maximum number of recent projects to remember. */ -export const MAX_RECENT_PROJECTS = 10; - /** Maximum number of recent models to remember. */ export const MAX_RECENT_MODELS = 8; diff --git a/src/lib/session-drafts.ts b/src/lib/session-drafts.ts index eb4bd55..a4a8d38 100644 --- a/src/lib/session-drafts.ts +++ b/src/lib/session-drafts.ts @@ -3,7 +3,7 @@ import { STORAGE_KEYS } from "@/lib/constants"; import { persistOrRemoveJSON, storageParsed } from "@/lib/safe-storage"; export type SessionDraftMap = Record; -export type SessionDraftImagesMap = Record; +type SessionDraftImagesMap = Record; export type QueueMode = "queue" | "interrupt" | "after-part"; export type QueuedPrompt = { id: string; @@ -15,7 +15,7 @@ export type QueuedPrompt = { variant?: string; mode: QueueMode; }; -export type QueuedPromptsMap = Record; +type QueuedPromptsMap = Record; export function getSessionDraftKey(input: { sessionId?: string | null; diff --git a/src/lib/session-namer.ts b/src/lib/session-namer.ts index 61a1550..ec72667 100644 --- a/src/lib/session-namer.ts +++ b/src/lib/session-namer.ts @@ -45,7 +45,7 @@ function cleanTitle(input: string): string { .slice(0, 80); } -export function fallbackSessionTitle(prompt: string): string { +function fallbackSessionTitle(prompt: string): string { if ( /^\s*https?:\/\/(?:www\.)?(youtube\.com|youtu\.be)\//i.test(prompt) && /\bsummarize\b/i.test(prompt) diff --git a/src/lib/sidebar-order.ts b/src/lib/sidebar-order.ts index e4b8aee..817f385 100644 --- a/src/lib/sidebar-order.ts +++ b/src/lib/sidebar-order.ts @@ -1,4 +1,4 @@ -export interface SidebarSortableSessionLike { +interface SidebarSortableSessionLike { id: string; time: { created?: number; @@ -6,7 +6,7 @@ export interface SidebarSortableSessionLike { }; } -export function getSidebarSessionSortTime(session: SidebarSortableSessionLike): number { +function getSidebarSessionSortTime(session: SidebarSortableSessionLike): number { return session.time.updated ?? session.time.created ?? 0; } diff --git a/src/lib/sidebar-pins.ts b/src/lib/sidebar-pins.ts index 8c4b35d..f79dd32 100644 --- a/src/lib/sidebar-pins.ts +++ b/src/lib/sidebar-pins.ts @@ -1,23 +1,23 @@ import { normalizeProjectPath } from "@/lib/utils"; -export interface SidebarPinMetaLike { +interface SidebarPinMetaLike { pinnedAt?: string | null; assignedProjectDir?: string | null; } -export interface SidebarPinSessionLike { +interface SidebarPinSessionLike { id: string; directory: string; _projectDir?: string; } -export interface SidebarWorktreeParentLike { +interface SidebarWorktreeParentLike { parentDir: string; } -export type SidebarProjectEntry = [string, TSession[]]; +type SidebarProjectEntry = [string, TSession[]]; -export type SidebarPinnedEntry = +type SidebarPinnedEntry = | { kind: "project"; directory: string; @@ -31,7 +31,7 @@ export type SidebarPinnedEntry = pinnedAt: string; }; -export interface SidebarPinPartitionResult { +interface SidebarPinPartitionResult { pinnedEntries: SidebarPinnedEntry[]; projectEntries: Array>; projectSessionsByDirectory: Record; diff --git a/src/lib/todos.ts b/src/lib/todos.ts index ea982bf..e9d05e2 100644 --- a/src/lib/todos.ts +++ b/src/lib/todos.ts @@ -44,9 +44,6 @@ export const todoStatusConfig: Record< }, }; -/** Ordered status keys for display (in-progress first for visibility). */ -export const STATUS_ORDER = ["in_progress", "pending", "completed", "cancelled"] as const; - // --------------------------------------------------------------------------- // Extraction // --------------------------------------------------------------------------- diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bfbd576..62cbcfd 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -50,7 +50,7 @@ export function findModel( export const DEFAULT_AGENT_NAME = "build"; /** Check whether an agent is selectable (primary/all mode and not hidden). */ -export function isSelectableAgent(agent: Agent): boolean { +function isSelectableAgent(agent: Agent): boolean { return (agent.mode === "primary" || agent.mode === "all") && !agent.hidden; } @@ -308,17 +308,3 @@ export function pruneRecord(prev: Record, validKeys: Set): } return changed ? next : prev; } - -/** Return a new Set with the given items added. */ -export function setWith(s: Set, ...items: T[]): Set { - const next = new Set(s); - for (const item of items) next.add(item); - return next; -} - -/** Return a new Set with the given items removed. */ -export function setWithout(s: Set, ...items: T[]): Set { - const next = new Set(s); - for (const item of items) next.delete(item); - return next; -} diff --git a/src/lib/web-electron-api.ts b/src/lib/web-electron-api.ts index 4c9ac9d..ef35d86 100644 --- a/src/lib/web-electron-api.ts +++ b/src/lib/web-electron-api.ts @@ -1,4 +1,5 @@ -import type { ElectronAPI } from "@/types/electron"; +import type { AgentBackendId } from "@/agents"; +import type { ElectronAPI, InstallProgress } from "@/types/electron"; type Listener = (data: unknown) => void; @@ -191,7 +192,7 @@ export function installWebElectronAPI() { if (window.electronAPI) return; subscribeEvents(); - window.electronAPI = { + const api = { settings: { getAllSync: getAllSettingsSync, getSync: settingsGetSync, @@ -209,6 +210,7 @@ export function installWebElectronAPI() { getPlatform: () => invoke("platform:get"), getSystemLocale: () => invoke("platform:locale"), detectBackends: () => invoke("platform:detectBackends"), + isPackaged: () => invoke("app:isPackaged"), onMaximizeChange: () => () => {}, openDirectory: () => invoke("dialog:openDirectory"), detachProject: (projectDir: string) => invoke("window:detachProject", projectDir), @@ -228,6 +230,9 @@ export function installWebElectronAPI() { openInTerminal: (dirPath: string, command = "") => invoke("shell:openInTerminal", dirPath, command), getHomeDir: () => invoke("platform:homeDir"), + installBackend: (backendId: AgentBackendId) => invoke("backend:install", backendId), + onInstallProgress: (callback: (progress: InstallProgress) => void) => + on("backend:install-progress", callback as Listener), worktree: { detectSetup: (worktreePath: string) => invoke("worktree:detect-setup", worktreePath), runSetup: (worktreePath: string, command: string) => @@ -398,5 +403,7 @@ export function installWebElectronAPI() { onSkillsInstallProgress: (callback: Listener) => on("opencode:skills:install-progress", callback), }, - } as unknown as ElectronAPI; + }; + + window.electronAPI = api as unknown as ElectronAPI; } diff --git a/styles/globals.css b/styles/globals.css index e4b411b..857f4b9 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,7 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "../node_modules/@wooorm/starry-night/style/both.css"; -@import "katex/dist/katex.min.css"; @source "../node_modules/streamdown/dist/*.js"; @font-face { @@ -329,14 +328,6 @@ /* Inline code – styled via MarkdownRenderer component classes */ - /* Math */ - & .katex-display { - @apply my-4 overflow-x-auto overflow-y-hidden; - } - & .katex { - font-size: 1em; - } - /* Strong and emphasis */ & strong { @apply font-bold; diff --git a/tsconfig.json b/tsconfig.json index f8dc197..a9c540a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,8 +26,8 @@ }, // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, + "noUnusedLocals": true, + "noUnusedParameters": true, "noPropertyAccessFromIndexSignature": false }, diff --git a/vite.config.ts b/vite.config.ts index 5dba235..062c3c8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -43,7 +43,11 @@ function openguiWebBackend() { configureServer(server: { httpServer?: { once: (event: "close", listener: () => void) => void }; }) { - if (process.env.OPENGUI_SKIP_WEB_BACKEND === "1" || process.env.NODE_ENV === "test") { + if ( + process.env.OPENGUI_SKIP_WEB_BACKEND === "1" || + process.env.NODE_ENV === "test" || + process.env.VITEST === "true" + ) { return; }