diff --git a/apps/daemon/src/host/acp-provider-execution.ts b/apps/daemon/src/host/acp-provider-execution.ts index dca45eacc..6cc3bfb14 100644 --- a/apps/daemon/src/host/acp-provider-execution.ts +++ b/apps/daemon/src/host/acp-provider-execution.ts @@ -29,6 +29,12 @@ import type { AcpConfigState, AcpAgentDiagnostics, AcpDebugRequest, AcpDebugRunR const ACP_PROVIDER_ID = "acp"; +const normalizePlanStatus = (status: unknown): "pending" | "in_progress" | "completed" => { + if (status === "completed" || status === "done") return "completed"; + if (status === "in_progress") return "in_progress"; + return "pending"; +}; + type PendingAcpPermission = { sessionId: string; toolCallId: string; @@ -47,7 +53,16 @@ type PendingAcpPermission = { */ export class AcpProviderExecutionPort implements ProviderExecutionPort { private runtimePromise: Promise | null = null; - private activeTurns = new Map(); + private activeTurns = new Map< + string, + { + controller: AbortController; + eventId: string; + runId: string; + donePromise: Promise; + doneResolve: () => void; + } + >(); private pendingPermissions = new Map(); private readonly contentMapper = new AcpContentMapper(); @@ -138,10 +153,16 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { const runtime = await this.getRuntime(); const record = await this.getSessionRecord(sessionId); const controller = new AbortController(); + let doneResolve!: () => void; + const donePromise = new Promise((resolve) => { + doneResolve = resolve; + }); this.activeTurns.set(sessionId, { controller, eventId: assistantMessageId, runId: requestId, + donePromise, + doneResolve, }); void this.runTurn( @@ -154,7 +175,11 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { assistantMessageId, record?.workdir, ).finally(() => { - this.activeTurns.delete(sessionId); + const current = this.activeTurns.get(sessionId); + if (current && current.runId === requestId) { + this.activeTurns.delete(sessionId); + } + doneResolve(); }); return { requestId, messageId: assistantMessageId }; @@ -374,6 +399,7 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { workdir?: string, ): Promise { const blocks: Array> = []; + let planRevision = 0; try { for await (const notification of runtime.runPromptTurn({ conversationId: sessionId, @@ -385,8 +411,26 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { if (controller.signal.aborted) break; const mapped = this.contentMapper.map(notification); + + if (mapped.planEntries && mapped.planEntries.length > 0) { + planRevision += 1; + const plan = mapped.planEntries + .map((entry) => ({ step: (entry.content ?? "").trim(), status: normalizePlanStatus(entry.status) })) + .filter((item) => item.step.length > 0); + if (plan.length > 0) { + this.eventPublisher.publish("chat.plan.updated", { + sessionId, + messageId: assistantMessageId, + plan, + revision: planRevision, + updatedAt: new Date().toISOString(), + }); + } + } + const now = Date.now(); for (const block of mapped.blocks) { + if (block.type === "plan") continue; const last = blocks.at(-1); if (block.type === "content" && last?.type === "content") { last.content = `${last.content ?? ""}${block.content ?? ""}`; @@ -498,9 +542,43 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { }); } - async steerActiveTurn(_sessionId: string, _content: string | SendMessageInput): Promise { - await this.cancelGeneration(_sessionId); - await this.sendMessage(_sessionId, _content); + async steerActiveTurn(sessionId: string, content: string | SendMessageInput): Promise { + await this.interruptActiveTurn(sessionId); + await this.sendMessage(sessionId, content); + } + + /** + * Non-destructively interrupts the in-flight ACP prompt for a session so a new + * turn can follow (used by steer). Unlike {@link cancelGeneration}, this does + * NOT tear down the session or unbind the agent process: it asks the agent to + * cancel the active `session/prompt` request, aborts local streaming, and + * waits for the turn to settle before returning. + */ + private async interruptActiveTurn(sessionId: string): Promise { + const active = this.activeTurns.get(sessionId); + if (!active) return; + + active.controller.abort(); + + for (const [toolCallId, pending] of this.pendingPermissions) { + if (pending.sessionId !== sessionId) continue; + this.pendingPermissions.delete(toolCallId); + pending.resolve({ outcome: { outcome: "cancelled" } }); + } + + try { + const runtime = await this.getRuntime(); + const session = runtime.sessionManager.getSession(sessionId); + if (session) { + await session.connection.agent.notify(acpMethods.agent.session.cancel, { + sessionId: session.sessionId, + } as schema.CancelNotification); + } + } catch (error) { + console.warn("[ACP] Failed to send session/cancel for steer:", error); + } + + await Promise.race([active.donePromise, new Promise((resolve) => setTimeout(resolve, 4000))]).catch(() => {}); } async getAcpSessionCommands(conversationId: string): Promise< diff --git a/apps/daemon/test/acpProviderExecution.test.ts b/apps/daemon/test/acpProviderExecution.test.ts index 86f4dc827..22f64a00a 100644 --- a/apps/daemon/test/acpProviderExecution.test.ts +++ b/apps/daemon/test/acpProviderExecution.test.ts @@ -32,13 +32,19 @@ describe("AcpProviderExecutionPort", () => { }); it("publishes and persists decoded ACP assistant text chunks", async () => { - const addMessage = vi.fn(async () => "persisted-assistant-1"); + const finalizeAssistantMessage = vi.fn(async () => undefined); + const setMessageError = vi.fn(async () => undefined); const publish = vi.fn(); - const port = new AcpProviderExecutionPort({} as never, { addMessage } as never, { publish } as never, { - dataDir: "/tmp", - appVersion: "1.0.0", - db: { prepare: vi.fn() }, - }); + const port = new AcpProviderExecutionPort( + {} as never, + { finalizeAssistantMessage, setMessageError } as never, + { publish } as never, + { + dataDir: "/tmp", + appVersion: "1.0.0", + db: { prepare: vi.fn() }, + }, + ); const runtime = { async *runPromptTurn() { yield { @@ -96,7 +102,7 @@ describe("AcpProviderExecutionPort", () => { expect.objectContaining({ requestId: "request-1", sessionId: "conversation-1", - messageId: "persisted-assistant-1", + messageId: "assistant-1", blocks: expect.arrayContaining([ expect.objectContaining({ content: "OpenCode", status: "success" }), expect.objectContaining({ type: "reasoning_content", content: "The user" }), @@ -104,7 +110,83 @@ describe("AcpProviderExecutionPort", () => { ]), }), ); - expect(addMessage).toHaveBeenCalledWith("conversation-1", "assistant", expect.stringContaining("OpenCode")); + expect(finalizeAssistantMessage).toHaveBeenCalledWith( + "assistant-1", + expect.arrayContaining([expect.objectContaining({ content: "OpenCode", status: "success" })]), + expect.any(String), + ); + expect(setMessageError).not.toHaveBeenCalled(); + }); + + it("routes ACP plan updates to the plan widget and skips inline plan blocks", async () => { + const finalizeAssistantMessage = vi.fn(async () => undefined); + const setMessageError = vi.fn(async () => undefined); + const publish = vi.fn(); + const port = new AcpProviderExecutionPort( + {} as never, + { finalizeAssistantMessage, setMessageError } as never, + { publish } as never, + { + dataDir: "/tmp", + appVersion: "1.0.0", + db: { prepare: vi.fn() }, + }, + ); + const runtime = { + async *runPromptTurn() { + yield { + sessionId: "acp-session", + update: { + sessionUpdate: "plan", + entries: [ + { content: "Analyze", status: "completed" }, + { content: "Implement", status: "in_progress" }, + { content: "Test", status: "pending" }, + ], + }, + }; + yield { + sessionId: "acp-session", + update: { + sessionUpdate: "plan", + entries: [ + { content: "Analyze", status: "completed" }, + { content: "Implement", status: "completed" }, + { content: "Test", status: "in_progress" }, + ], + }, + }; + }, + }; + + await (port as any).runTurn( + runtime, + "conversation-1", + { id: "opencode", name: "OpenCode" }, + [{ type: "text", text: "hello" }], + new AbortController(), + "request-1", + "assistant-1", + ); + + const planCalls = publish.mock.calls.filter((call) => call[0] === "chat.plan.updated"); + expect(planCalls).toHaveLength(2); + expect(planCalls[0][1]).toMatchObject({ + sessionId: "conversation-1", + messageId: "assistant-1", + revision: 1, + plan: [ + { step: "Analyze", status: "completed" }, + { step: "Implement", status: "in_progress" }, + { step: "Test", status: "pending" }, + ], + }); + expect(planCalls[1][1]).toMatchObject({ revision: 2 }); + + for (const call of publish.mock.calls) { + if (call[0] !== "chat.stream.updated") continue; + expect((call[1] as { blocks: Array<{ type: string }> }).blocks.some((b) => b.type === "plan")).toBe(false); + } }); it("allows ACP tool permissions once in full access mode", async () => { @@ -213,16 +295,11 @@ describe("AcpProviderExecutionPort", () => { }); it("times out a hanging permission resolver with a cancelled outcome", async () => { - vi.useFakeTimers(); - try { - const onTimeout = vi.fn(); - const promise = resolvePermissionWithTimeout(() => new Promise(() => {}), 1000, onTimeout); - await vi.advanceTimersByTimeAsync(1000); - await expect(promise).resolves.toEqual({ outcome: { outcome: "cancelled" } }); - expect(onTimeout).toHaveBeenCalledOnce(); - } finally { - vi.useRealTimers(); - } + const onTimeout = vi.fn(); + await expect(resolvePermissionWithTimeout(() => new Promise(() => {}), 50, onTimeout)).resolves.toEqual({ + outcome: { outcome: "cancelled" }, + }); + expect(onTimeout).toHaveBeenCalledOnce(); }); it("returns the resolver result when it settles before the timeout", async () => { diff --git a/apps/daemon/test/daemonSessionRoutes.test.ts b/apps/daemon/test/daemonSessionRoutes.test.ts index b18ab3160..3751f5d08 100644 --- a/apps/daemon/test/daemonSessionRoutes.test.ts +++ b/apps/daemon/test/daemonSessionRoutes.test.ts @@ -887,6 +887,7 @@ describe("daemon ACP session routes", () => { vi.spyOn(provider as any, "getRuntime").mockResolvedValue(runtime); vi.spyOn(provider as any, "cancelGeneration").mockResolvedValue(undefined); + vi.spyOn(provider as any, "interruptActiveTurn").mockResolvedValue(undefined); vi.spyOn(provider as any, "sendMessage").mockResolvedValue({ requestId: null, messageId: null }); await expect(provider.setAcpSessionConfigOption("session-1", "__acp_legacy_model__", "model-b")).resolves.toEqual({ @@ -905,7 +906,7 @@ describe("daemon ACP session routes", () => { }); await expect(provider.steerActiveTurn("session-1", "steer this")).resolves.toBeUndefined(); - expect(provider.cancelGeneration).toHaveBeenCalledWith("session-1"); + expect(provider.interruptActiveTurn).toHaveBeenCalledWith("session-1"); expect(provider.sendMessage).toHaveBeenCalledWith("session-1", "steer this"); expect(runtime.processManager.updateBoundProcessConfigState).toHaveBeenCalledWith("session-1", { source: "legacy", diff --git a/apps/desktop/src/main/routes/settings/settingsAdapter.ts b/apps/desktop/src/main/routes/settings/settingsAdapter.ts index d2023a640..af24b460e 100644 --- a/apps/desktop/src/main/routes/settings/settingsAdapter.ts +++ b/apps/desktop/src/main/routes/settings/settingsAdapter.ts @@ -30,6 +30,7 @@ export const readSettingsSnapshot = (configPresenter: IConfigPresenter): Setting traceDebugEnabled: configPresenter.getSetting("traceDebugEnabled") ?? false, copyWithCotEnabled: configPresenter.getCopyWithCotEnabled(), loggingEnabled: configPresenter.getLoggingEnabled(), + showContinueIndicator: configPresenter.getSetting("showContinueIndicator") ?? false, }); export const pickSettingsSnapshot = ( @@ -93,6 +94,9 @@ export const applySettingChange = (configPresenter: IConfigPresenter, change: Se case "loggingEnabled": configPresenter.setLoggingEnabled(change.value); return; + case "showContinueIndicator": + configPresenter.setSetting("showContinueIndicator", change.value); + return; } }; diff --git a/bun.lock b/bun.lock index e5b2e2b95..87c1ad8e8 100644 --- a/bun.lock +++ b/bun.lock @@ -331,10 +331,12 @@ "@argos/client-sdk": "workspace:*", "@argos/shared": "workspace:*", "@argos/shared-contracts": "workspace:*", + "@base-ui/react": "^1.6.0", "@fontsource-variable/inter": "^5.3.0", "@iconify-json/lucide": "^1.2.121", "@iconify-json/vscode-icons": "^1.2.68", "@iconify/react": "^6.0.2", + "@tanstack/highlight": "^0.0.9", "@tanstack/react-query": "catalog:", "@tanstack/react-router": "catalog:", "@tanstack/react-store": "catalog:", @@ -350,7 +352,6 @@ "react": "catalog:", "react-dom": "catalog:", "react-markdown": "catalog:", - "rehype-highlight": "^7.0.2", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "sonner": "catalog:", @@ -386,7 +387,7 @@ "babel-plugin-react-compiler": "^1.0.0", "jsdom": "catalog:", "mermaid": "^11.16.0", - "monaco-editor": "^0.56.0", + "monaco-editor": "^0.55.1", "stream-monaco": "^0.0.49", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "catalog:", @@ -614,6 +615,10 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@base-ui/react": ["@base-ui/react@1.6.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.3.1", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw=="], + + "@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], @@ -1350,6 +1355,8 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], + "@tanstack/highlight": ["@tanstack/highlight@0.0.9", "", {}, "sha512-vWuVpuSprbsEq5gle5vp2CEBNVue65m09idkbv0rcCPFn3XnxzZg1Q9kownk24iCag6tJdfd/gtKSwllPbQvSA=="], + "@tanstack/history": ["@tanstack/history@1.162.0", "", {}, "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA=="], "@tanstack/query-core": ["@tanstack/query-core@5.101.4", "", {}, "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw=="], @@ -2366,8 +2373,6 @@ "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], - "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], "lucide-react": ["lucide-react@1.28.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg=="], @@ -2518,7 +2523,7 @@ "module-error": ["module-error@1.0.2", "", {}, "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA=="], - "monaco-editor": ["monaco-editor@0.56.0", "", { "dependencies": { "dompurify": "3.4.8", "marked": "14.0.0" } }, "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg=="], + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], @@ -2744,8 +2749,6 @@ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], - "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], @@ -2762,6 +2765,8 @@ "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], + "reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], @@ -3374,8 +3379,6 @@ "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "lowlight/highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], - "macos-version/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -3388,7 +3391,7 @@ "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "monaco-editor/dompurify": ["dompurify@3.4.8", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ=="], + "monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], @@ -3436,8 +3439,6 @@ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "stream-monaco/monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], - "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -3690,10 +3691,6 @@ "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "stream-monaco/monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], - - "stream-monaco/monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], - "tiny-runtime-injector/axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "tiny-runtime-injector/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], diff --git a/opencode.json b/opencode.json new file mode 100644 index 000000000..12502e3d6 --- /dev/null +++ b/opencode.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "deepwiki": { + "type": "remote", + "url": "https://mcp.deepwiki.com/mcp", + "enabled": true + } + } +} diff --git a/packages/backend-core/src/dispatch/settings/settingsAdapter.ts b/packages/backend-core/src/dispatch/settings/settingsAdapter.ts index d2023a640..af24b460e 100644 --- a/packages/backend-core/src/dispatch/settings/settingsAdapter.ts +++ b/packages/backend-core/src/dispatch/settings/settingsAdapter.ts @@ -30,6 +30,7 @@ export const readSettingsSnapshot = (configPresenter: IConfigPresenter): Setting traceDebugEnabled: configPresenter.getSetting("traceDebugEnabled") ?? false, copyWithCotEnabled: configPresenter.getCopyWithCotEnabled(), loggingEnabled: configPresenter.getLoggingEnabled(), + showContinueIndicator: configPresenter.getSetting("showContinueIndicator") ?? false, }); export const pickSettingsSnapshot = ( @@ -93,6 +94,9 @@ export const applySettingChange = (configPresenter: IConfigPresenter, change: Se case "loggingEnabled": configPresenter.setLoggingEnabled(change.value); return; + case "showContinueIndicator": + configPresenter.setSetting("showContinueIndicator", change.value); + return; } }; diff --git a/packages/shared-contracts/src/routes/settings.routes.ts b/packages/shared-contracts/src/routes/settings.routes.ts index 662463953..eda79c765 100644 --- a/packages/shared-contracts/src/routes/settings.routes.ts +++ b/packages/shared-contracts/src/routes/settings.routes.ts @@ -17,6 +17,7 @@ export const SETTINGS_KEYS = [ "traceDebugEnabled", "copyWithCotEnabled", "loggingEnabled", + "showContinueIndicator", ] as const; export const SettingsKeySchema = zod.enum(SETTINGS_KEYS); @@ -37,6 +38,7 @@ export const SettingsSnapshotValuesSchema = zod.object({ traceDebugEnabled: zod.boolean(), copyWithCotEnabled: zod.boolean(), loggingEnabled: zod.boolean(), + showContinueIndicator: zod.boolean(), }); export const SettingsChangeSchema = zod.discriminatedUnion("key", [ @@ -100,6 +102,10 @@ export const SettingsChangeSchema = zod.discriminatedUnion("key", [ key: zod.literal("loggingEnabled"), value: zod.boolean(), }), + zod.object({ + key: zod.literal("showContinueIndicator"), + value: zod.boolean(), + }), ]); export const settingsGetSnapshotRoute = defineRouteContract({ diff --git a/apps/desktop/components.json b/packages/ui/components.json similarity index 87% rename from apps/desktop/components.json rename to packages/ui/components.json index e8a419fca..b55b9e01f 100644 --- a/apps/desktop/components.json +++ b/packages/ui/components.json @@ -1,11 +1,11 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "radix-rhea", + "style": "base-rhea", "rsc": false, "tsx": true, "tailwind": { "config": "", - "css": "src/renderer/src/assets/style.css", + "css": "src/assets/style.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" diff --git a/packages/ui/package.json b/packages/ui/package.json index e193a4cda..e7ae2a2eb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,10 +16,12 @@ "@argos/client-sdk": "workspace:*", "@argos/shared": "workspace:*", "@argos/shared-contracts": "workspace:*", + "@base-ui/react": "^1.6.0", "@fontsource-variable/inter": "^5.3.0", "@iconify-json/lucide": "^1.2.121", "@iconify-json/vscode-icons": "^1.2.68", "@iconify/react": "^6.0.2", + "@tanstack/highlight": "^0.0.9", "@tanstack/react-query": "catalog:", "@tanstack/react-router": "catalog:", "@tanstack/react-store": "catalog:", @@ -35,7 +37,6 @@ "react": "catalog:", "react-dom": "catalog:", "react-markdown": "catalog:", - "rehype-highlight": "^7.0.2", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "sonner": "catalog:", @@ -71,7 +72,7 @@ "babel-plugin-react-compiler": "^1.0.0", "jsdom": "catalog:", "mermaid": "^11.16.0", - "monaco-editor": "^0.56.0", + "monaco-editor": "^0.55.1", "stream-monaco": "^0.0.49", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "catalog:", diff --git a/packages/ui/settings/components/AboutUsSettings.tsx b/packages/ui/settings/components/AboutUsSettings.tsx index 9a6452c6b..5db968d05 100644 --- a/packages/ui/settings/components/AboutUsSettings.tsx +++ b/packages/ui/settings/components/AboutUsSettings.tsx @@ -150,6 +150,7 @@ export default function AboutUsSettings() {