diff --git a/bun.lock b/bun.lock index db6a170d2..514551950 100644 --- a/bun.lock +++ b/bun.lock @@ -735,6 +735,7 @@ "patchedDependencies": { "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch", + "virtua@0.49.1": "patches/virtua@0.49.1.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", diff --git a/nix/hashes.json b/nix/hashes.json index f3a9da2ee..75a54eac6 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-szOnLhAI4d3O4NaQvCRBufklmtRU8AR8p5LyFxjlTXo=", - "aarch64-linux": "sha256-mfyL4Pswl6HeObPg+TMK1r7RjvLCMgDWYrFnfHkXGj4=", - "aarch64-darwin": "sha256-XMK5IFCIxlSwH21Nqya8ZkAQ4Skp6GghukIl7NMp+so=", - "x86_64-darwin": "sha256-W7nLMO6qhR32442yMebEJJgaZRNpqTbZ8BJhdqMK3S4=" + "x86_64-linux": "sha256-Nol27sqG6za6oR32MqZrmRxch0/XFeQ61C9Yqtxo7Hk=", + "aarch64-linux": "sha256-+jfkGr4tuv/s/v890XaFTRqUxwA8H8KVMiys6sEXi/4=", + "aarch64-darwin": "sha256-FYtKluNazzuQcrr2iHm7B2pJfYrAHkbcj4kWs2FtBgc=", + "x86_64-darwin": "sha256-PpyWDkzb0p50US7Bs3m3A4PZ0JM3dccGgTKYN9JFYR0=" } } diff --git a/package.json b/package.json index 6e3b811e1..86f5a1eac 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", + "virtua@0.49.1": "patches/virtua@0.49.1.patch", "@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch" } } diff --git a/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts b/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts new file mode 100644 index 000000000..db191d257 --- /dev/null +++ b/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts @@ -0,0 +1,395 @@ +import { expect, test, type Locator, type Page, type Route } from "@playwright/test" + +const directory = "C:/OpenCode/TimelineStateRegression" +const projectID = "proj_timeline_state_regression" +const sessionID = "ses_timeline_state_regression" +const userMessageID = "msg_user_regression" +const assistantMessageID = "msg_assistant_regression" +const editPartID = "prt_0001_edit" +const textPartID = "prt_9999_text" +const title = "Timeline collapse state regression" +const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } + +type EventPayload = { + directory: string + payload: Record +} + +declare global { + interface Window { + __timelineDiffProbe: { + reset: () => void + shadowRoots: () => number + } + } +} + +const userMessage = { + info: { + id: userMessageID, + sessionID, + role: "user", + time: { created: 1700000000000 }, + summary: { diffs: [] }, + agent: "build", + model, + }, + parts: [ + { + id: "prt_user_text", + sessionID, + messageID: userMessageID, + type: "text", + text: "Please edit the file.", + }, + ], +} + +const editPart = { + id: editPartID, + sessionID, + messageID: assistantMessageID, + type: "tool", + callID: "call_edit_regression", + tool: "edit", + state: { + status: "completed", + input: { filePath: "src/regression.ts" }, + output: "Edited src/regression.ts", + title: "src/regression.ts", + metadata: { + filediff: { + file: "src/regression.ts", + additions: 1, + deletions: 1, + before: "export const value = 'before'\n", + after: "export const value = 'after'\n", + }, + diff: "diff --git a/src/regression.ts b/src/regression.ts\n-export const value = 'before'\n+export const value = 'after'\n", + }, + time: { start: 1700000001000, end: 1700000002000 }, + }, +} + +const streamedTextPart = { + id: textPartID, + sessionID, + messageID: assistantMessageID, + type: "text", + text: "Streaming added a later assistant text part.", +} + +const assistantMessage = { + info: { + id: assistantMessageID, + sessionID, + role: "assistant", + time: { created: 1700000001000 }, + parentID: userMessageID, + modelID: model.modelID, + providerID: model.providerID, + mode: "build", + agent: "build", + path: { cwd: directory, root: directory }, + cost: 0.01, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + variant: "max", + }, + parts: [editPart], +} + +test.describe("regression: session timeline local row state", () => { + test("keeps a manually collapsed tool collapsed when later assistant content streams", async ({ page }) => { + const events: EventPayload[] = [] + await mockServer(page, events) + await configurePage(page) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await expect(page.getByRole("heading", { name: title })).toBeVisible() + + const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first() + await expect(wrapper).toBeVisible() + await expectExpanded(wrapper, true) + + await wrapper.evaluate((element) => { + ;(element as HTMLElement).dataset.regressionMarker = "before-stream" + }) + await wrapper.locator('[data-slot="collapsible-trigger"]').first().click() + await expectExpanded(wrapper, false) + + events.push({ + directory, + payload: { + type: "message.part.updated", + properties: { part: streamedTextPart }, + }, + }) + + await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 }) + + expect(await readToolState(page)).toEqual({ + expanded: false, + row: "AssistantPart", + streamedTextVisible: true, + }) + }) + + test("does not remount an edit diff when sibling parts or diff counts update", async ({ page }) => { + const events: EventPayload[] = [] + await installDiffProbe(page) + await mockServer(page, events) + await configurePage(page) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await expect(page.getByRole("heading", { name: title })).toBeVisible() + + const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first() + await expect(wrapper).toBeVisible() + await expect(wrapper.locator('[data-component="file"][data-mode="diff"]').first()).toBeVisible() + await markDiffProbe(page) + + events.push({ + directory, + payload: { + type: "message.part.updated", + properties: { part: streamedTextPart }, + }, + }) + + await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 }) + expect(await readDiffProbe(page)).toEqual({ fileMarker: "before", shadowRoots: 0, toolMarker: "before" }) + + await markDiffProbe(page) + events.push({ + directory, + payload: { + type: "message.part.updated", + properties: { part: editPartWithAdditions(2) }, + }, + }) + + await expect(wrapper.locator('[data-slot="diff-changes-additions"]').filter({ hasText: "+2" }).first()).toBeVisible( + { timeout: 10_000 }, + ) + expect(await readDiffProbe(page)).toEqual({ fileMarker: "before", shadowRoots: 0, toolMarker: "before" }) + }) +}) + +async function configurePage(page: Page) { + await page.addInitScript(() => { + localStorage.setItem( + "settings.v3", + JSON.stringify({ + general: { + editToolPartsExpanded: true, + shellToolPartsExpanded: true, + showReasoningSummaries: true, + showSessionProgressBar: true, + }, + }), + ) + }) +} + +async function expectExpanded(locator: Locator, expected: boolean) { + await expect.poll(() => locator.evaluate(readExpanded)).toBe(expected) +} + +async function readToolState(page: Page) { + return page + .locator(`[data-timeline-part-id="${editPartID}"]`) + .first() + .evaluate( + (element, textPartID) => ({ + expanded: (() => { + const trigger = element.querySelector('[data-slot="collapsible-trigger"]') + const aria = trigger?.getAttribute("aria-expanded") + if (aria === "true") return true + if (aria === "false") return false + + const root = element.querySelector('[data-component="collapsible"]') + if (root?.hasAttribute("data-expanded")) return true + if (root?.hasAttribute("data-closed")) return false + + const content = element.querySelector('[data-slot="collapsible-content"]') + return !!content && content.getBoundingClientRect().height > 0 + })(), + row: element.closest("[data-timeline-row]")?.getAttribute("data-timeline-row"), + streamedTextVisible: !!document.querySelector(`[data-timeline-part-id="${textPartID}"]`), + }), + textPartID, + ) +} + +async function installDiffProbe(page: Page) { + await page.addInitScript(() => { + let shadowRootCount = 0 + const attachShadow = Element.prototype.attachShadow + Element.prototype.attachShadow = function (init) { + shadowRootCount += 1 + return attachShadow.call(this, init) + } + window.__timelineDiffProbe = { + reset: () => { + shadowRootCount = 0 + }, + shadowRoots: () => shadowRootCount, + } + }) +} + +async function markDiffProbe(page: Page) { + await page + .locator(`[data-timeline-part-id="${editPartID}"]`) + .first() + .evaluate((element) => { + const tool = element as HTMLElement + const file = tool.querySelector('[data-component="file"][data-mode="diff"]') + if (!file) throw new Error("missing edit diff file") + + tool.dataset.timelineProbe = "before" + file.dataset.timelineProbe = "before" + window.__timelineDiffProbe.reset() + }) +} + +async function readDiffProbe(page: Page) { + return page + .locator(`[data-timeline-part-id="${editPartID}"]`) + .first() + .evaluate((element) => { + const tool = element as HTMLElement + const file = tool.querySelector('[data-component="file"][data-mode="diff"]') + return { + fileMarker: file?.dataset.timelineProbe, + shadowRoots: window.__timelineDiffProbe.shadowRoots(), + toolMarker: tool.dataset.timelineProbe, + } + }) +} + +function editPartWithAdditions(additions: number) { + return { + ...editPart, + state: { + ...editPart.state, + metadata: { + ...editPart.state.metadata, + filediff: { + ...editPart.state.metadata.filediff, + additions, + }, + }, + }, + } +} + +function readExpanded(element: Element) { + const trigger = element.querySelector('[data-slot="collapsible-trigger"]') + const aria = trigger?.getAttribute("aria-expanded") + if (aria === "true") return true + if (aria === "false") return false + + const root = element.querySelector('[data-component="collapsible"]') + if (root?.hasAttribute("data-expanded")) return true + if (root?.hasAttribute("data-closed")) return false + + const content = element.querySelector('[data-slot="collapsible-content"]') + return !!content && content.getBoundingClientRect().height > 0 +} + +async function mockServer(page: Page, events: EventPayload[]) { + await page.route("**/*", async (route) => { + const url = new URL(route.request().url()) + const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" + if (url.port !== targetPort) return route.fallback() + + const path = url.pathname + if (path === "/global/event") return sse(route, events.splice(0)) + if ( + path === "/global/config" || + path === "/config" || + path === "/provider/auth" || + path === "/mcp" || + path === "/session/status" + ) + return json(route, {}) + if ( + ["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/status", "/vcs/diff"].includes( + path, + ) + ) + return json(route, []) + if (path === "/provider") return json(route, provider()) + if (path === "/path") + return json(route, { state: directory, config: directory, worktree: directory, directory, home: "C:/OpenCode" }) + if (path === "/project") return json(route, [project()]) + if (path === "/project/current") return json(route, project()) + if (path === "/agent") return json(route, [{ name: "build", mode: "primary" }]) + if (path === "/vcs") return json(route, { branch: "main", default_branch: "main" }) + if (path === "/session") return json(route, [session()]) + if (path === `/session/${sessionID}`) return json(route, session()) + if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(path)) return json(route, []) + if (path === `/session/${sessionID}/message`) return json(route, [userMessage, assistantMessage]) + return json(route, {}) + }) +} + +function project() { + return { + id: projectID, + worktree: directory, + vcs: "git", + name: "timeline-state-regression", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + } +} + +function session() { + return { + id: sessionID, + slug: "timeline-state-regression", + projectID, + directory, + title, + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + } +} + +function provider() { + return { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "claude-opus-4-6" }, + } +} + +function json(route: Route, body: unknown, headers?: Record) { + return route.fulfill({ + status: 200, + contentType: "application/json", + headers: { "access-control-allow-origin": "*", "access-control-expose-headers": "x-next-cursor", ...headers }, + body: JSON.stringify(body ?? null), + }) +} + +function sse(route: Route, events: EventPayload[]) { + return route.fulfill({ + status: 200, + contentType: "text/event-stream", + headers: { "access-control-allow-origin": "*" }, + body: events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""), + }) +} + +function base64Encode(value: string) { + return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") +} diff --git a/packages/app/e2e/regression/session-timeline-context-resize.spec.ts b/packages/app/e2e/regression/session-timeline-context-resize.spec.ts new file mode 100644 index 000000000..98f34b2b9 --- /dev/null +++ b/packages/app/e2e/regression/session-timeline-context-resize.spec.ts @@ -0,0 +1,300 @@ +import { expect, test, type Page, type Route } from "@playwright/test" + +const directory = "C:/OpenCode/ContextResizeRegression" +const projectID = "proj_context_resize_regression" +const sessionID = "ses_context_resize_regression" +const title = "Context resize regression" +const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } +const contextIDs = ["prt_0100_read", "prt_0101_glob", "prt_0102_grep", "prt_0103_list"] +const followingTextID = "prt_0104_text" + +type Message = { + info: Record & { id: string; role: "user" | "assistant" } + parts: Record[] +} + +const messages = [...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(), ...turn(10, true)] + +test.describe("regression: session timeline context group resize", () => { + test("remeasures a recent explored context group before the next paint", async ({ page }) => { + await page.setViewportSize({ width: 1400, height: 900 }) + await mockServer(page) + await configurePage(page) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await expect(page.getByRole("heading", { name: title })).toBeVisible() + await expect(page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first()).toBeVisible() + await expect(page.locator(`[data-timeline-part-id="${followingTextID}"]`).first()).toBeVisible() + await settle(page) + + const samples = await sampleExpansion(page) + const visibleOverlap = samples.filter((sample) => sample.frame >= 1 && sample.overlap > 0.5) + + console.log("context resize samples", JSON.stringify(samples, null, 2)) + + expect(samples[0]?.overlap).toBe(0) + expect(visibleOverlap).toEqual([]) + expect(samples.at(-1)?.expanded).toBe("true") + }) +}) + +async function configurePage(page: Page) { + await page.addInitScript(() => { + localStorage.setItem( + "settings.v3", + JSON.stringify({ + general: { + editToolPartsExpanded: true, + shellToolPartsExpanded: true, + showReasoningSummaries: true, + showSessionProgressBar: true, + }, + }), + ) + }) +} + +async function sampleExpansion(page: Page) { + return page.evaluate( + ({ contextIDs, followingTextID }) => + new Promise< + { + frame: number + label: string + scrollTop: number + scrollHeight: number + contextBottom: number + textTop: number + overlap: number + gap: number + expanded: string | null + }[] + >((resolve) => { + const context = document.querySelector(`[data-timeline-part-ids="${contextIDs.join(",")}"]`) + const text = document.querySelector(`[data-timeline-part-id="${followingTextID}"]`) + const scroller = context?.closest(".scroll-view__viewport") + const trigger = context?.querySelector('[data-slot="collapsible-trigger"]') + const contextRow = context?.closest('[data-timeline-row="AssistantPart"]') + const textRow = text?.closest('[data-timeline-row="AssistantPart"]') + if (!context || !text || !scroller || !trigger || !contextRow || !textRow) + throw new Error("missing regression nodes") + + scroller.scrollTop = scroller.scrollHeight + const samples: { + frame: number + label: string + scrollTop: number + scrollHeight: number + contextBottom: number + textTop: number + overlap: number + gap: number + expanded: string | null + }[] = [] + const capture = (frame: number, label: string) => { + const contextRect = contextRow.getBoundingClientRect() + const textRect = textRow.getBoundingClientRect() + samples.push({ + frame, + label, + scrollTop: Math.round(scroller.scrollTop * 10) / 10, + scrollHeight: Math.round(scroller.scrollHeight * 10) / 10, + contextBottom: Math.round(contextRect.bottom * 10) / 10, + textTop: Math.round(textRect.top * 10) / 10, + overlap: Math.max(0, Math.round((contextRect.bottom - textRect.top) * 10) / 10), + gap: Math.max(0, Math.round((textRect.top - contextRect.bottom) * 10) / 10), + expanded: trigger.getAttribute("aria-expanded"), + }) + } + + capture(-1, "before") + trigger.click() + capture(0, "sync-after-click") + + let frame = 1 + const tick = () => { + capture(frame, "raf") + frame += 1 + if (frame > 8) { + resolve(samples) + return + } + requestAnimationFrame(tick) + } + requestAnimationFrame(tick) + }), + { contextIDs, followingTextID }, + ) +} + +function turn(index: number, target: boolean): Message[] { + const userID = id("msg_user", index) + const assistantID = id("msg_assistant", index) + return [ + { + info: { + id: userID, + sessionID, + role: "user", + time: { created: 1700000000000 + index * 10_000 }, + summary: { diffs: [] }, + agent: "build", + model, + }, + parts: [{ id: id("prt_user", index), sessionID, messageID: userID, type: "text", text: `User message ${index}` }], + }, + { + info: { + id: assistantID, + sessionID, + role: "assistant", + time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 2_000 }, + parentID: userID, + modelID: model.modelID, + providerID: model.providerID, + mode: "build", + agent: "build", + path: { cwd: directory, root: directory }, + cost: 0.01, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + variant: "max", + finish: "stop", + }, + parts: target + ? [ + contextTool(contextIDs[0]!, assistantID, "read", { filePath: "src/recent-a.ts", offset: 0, limit: 120 }), + contextTool(contextIDs[1]!, assistantID, "glob", { path: directory, pattern: "**/*.ts" }), + contextTool(contextIDs[2]!, assistantID, "grep", { path: directory, pattern: "Explored", include: "*.ts" }), + contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }), + { + id: followingTextID, + sessionID, + messageID: assistantID, + type: "text", + text: "This assistant text is immediately after the explored context group.", + }, + ] + : [ + { + id: id("prt_text", index), + sessionID, + messageID: assistantID, + type: "text", + text: `Assistant filler ${index}. ${"filler ".repeat(60)}`, + }, + ], + }, + ] +} + +function contextTool(partID: string, messageID: string, tool: string, input: Record) { + return { + id: partID, + sessionID, + messageID, + type: "tool", + callID: `call_${partID}`, + tool, + state: { + status: "completed", + input, + output: `Completed ${tool}.\n${"detail line\n".repeat(8)}`, + title: input.filePath || input.path || input.pattern || "completed", + metadata: {}, + time: { start: 1700000000000, end: 1700000000100 }, + }, + } +} + +async function mockServer(page: Page) { + await page.route("**/*", async (route) => { + const url = new URL(route.request().url()) + const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" + if (url.port !== targetPort) return route.fallback() + + const path = url.pathname + if (path === "/global/event" || path === "/event") return sse(route) + if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(path)) + return json(route, {}) + if ( + ["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/status", "/vcs/diff"].includes( + path, + ) + ) + return json(route, []) + if (path === "/provider") return json(route, provider()) + if (path === "/path") + return json(route, { state: directory, config: directory, worktree: directory, directory, home: "C:/OpenCode" }) + if (path === "/project") return json(route, [project()]) + if (path === "/project/current") return json(route, project()) + if (path === "/agent") return json(route, [{ name: "build", mode: "primary" }]) + if (path === "/vcs") return json(route, { branch: "main", default_branch: "main" }) + if (path === "/session") return json(route, [session()]) + if (path === `/session/${sessionID}`) return json(route, session()) + if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(path)) return json(route, []) + if (path === `/session/${sessionID}/message`) return json(route, messages) + return json(route, {}) + }) +} + +async function settle(page: Page) { + await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))) +} + +function id(prefix: string, index: number) { + return `${prefix}_${String(index).padStart(4, "0")}` +} + +function project() { + return { + id: projectID, + worktree: directory, + vcs: "git", + name: "context-resize-regression", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + } +} + +function session() { + return { + id: sessionID, + slug: "context-resize-regression", + projectID, + directory, + title, + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + } +} + +function provider() { + return { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "claude-opus-4-6" }, + } +} + +function json(route: Route, body: unknown, headers?: Record) { + return route.fulfill({ + status: 200, + contentType: "application/json", + headers: { "access-control-allow-origin": "*", "access-control-expose-headers": "x-next-cursor", ...headers }, + body: JSON.stringify(body ?? null), + }) +} + +function sse(route: Route) { + return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" }) +} + +function base64Encode(value: string) { + return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") +} diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index b30d2309a..339cda8ed 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -30,8 +30,8 @@ import { Dynamic } from "solid-js/web" import { CommandProvider } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" -import { GlobalSDKProvider } from "@/context/global-sdk" -import { GlobalSyncProvider } from "@/context/global-sync" +import { ServerSDKProvider } from "@/context/server-sdk" +import { ServerSyncProvider } from "@/context/server-sync" import { HighlightsProvider } from "@/context/highlights" import { LanguageProvider, type Locale, useLanguage } from "@/context/language" import { LayoutProvider } from "@/context/layout" @@ -304,8 +304,8 @@ export function AppInterface(props: { - - + + {routerProps.children}} @@ -316,8 +316,8 @@ export function AppInterface(props: { - - + + diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 30069999e..a0ccc161f 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -12,15 +12,15 @@ import { showToast } from "@opencode-ai/ui/toast" import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useProviders } from "@/hooks/use-providers" export function DialogConnectProvider(props: { provider: string }) { const dialog = useDialog() - const globalSync = useGlobalSync() - const globalSDK = useGlobalSDK() + const serverSync = useServerSync() + const serverSDK = useServerSDK() const language = useLanguage() const providers = useProviders() @@ -41,7 +41,7 @@ export function DialogConnectProvider(props: { provider: string }) { }) const provider = createMemo( - () => providers.all().get(props.provider) ?? globalSync.data.provider.all.get(props.provider)!, + () => providers.all().get(props.provider) ?? serverSync.data.provider.all.get(props.provider)!, ) const fallback = createMemo(() => [ { @@ -52,16 +52,16 @@ export function DialogConnectProvider(props: { provider: string }) { const [auth] = createResource( () => props.provider, async () => { - const cached = globalSync.data.provider_auth[props.provider] + const cached = serverSync.data.provider_auth[props.provider] if (cached) return cached - const res = await globalSDK.client.provider.auth() + const res = await serverSDK.client.provider.auth() if (!alive.value) return fallback() - globalSync.set("provider_auth", res.data ?? {}) + serverSync.set("provider_auth", res.data ?? {}) return res.data?.[props.provider] ?? fallback() }, ) - const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider]) - const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback()) + const loading = createMemo(() => auth.loading && !serverSync.data.provider_auth[props.provider]) + const methods = createMemo(() => auth.latest ?? serverSync.data.provider_auth[props.provider] ?? fallback()) const [store, setStore] = createStore({ methodIndex: undefined as undefined | number, authorization: undefined as undefined | ProviderAuthAuthorization, @@ -158,7 +158,7 @@ export function DialogConnectProvider(props: { provider: string }) { } dispatch({ type: "auth.pending" }) const start = Date.now() - await globalSDK.client.provider.oauth + await serverSDK.client.provider.oauth .authorize( { providerID: props.provider, @@ -330,7 +330,7 @@ export function DialogConnectProvider(props: { provider: string }) { }) async function complete() { - await globalSDK.client.global.dispose() + await serverSDK.client.global.dispose() dialog.close() showToast({ variant: "success", @@ -407,7 +407,7 @@ export function DialogConnectProvider(props: { provider: string }) { } setFormStore("error", undefined) - await globalSDK.client.auth.set({ + await serverSDK.client.auth.set({ providerID: props.provider, auth: { type: "api", @@ -478,7 +478,7 @@ export function DialogConnectProvider(props: { provider: string }) { } setFormStore("error", undefined) - const result = await globalSDK.client.provider.oauth + const result = await serverSDK.client.provider.oauth .callback({ providerID: props.provider, method: store.methodIndex, @@ -531,7 +531,7 @@ export function DialogConnectProvider(props: { provider: string }) { onMount(() => { void (async () => { - const result = await globalSDK.client.provider.oauth + const result = await serverSDK.client.provider.oauth .callback({ providerID: props.provider, method: store.methodIndex, diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index 7d449849e..ad30236b0 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -9,8 +9,8 @@ import { showToast } from "@opencode-ai/ui/toast" import { batch, For } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form" import { DialogSelectProvider } from "./dialog-select-provider" @@ -21,8 +21,8 @@ type Props = { export function DialogCustomProvider(props: Props) { const dialog = useDialog() - const globalSync = useGlobalSync() - const globalSDK = useGlobalSDK() + const serverSync = useServerSync() + const serverSDK = useServerSDK() const language = useLanguage() const [form, setForm] = createStore({ @@ -105,8 +105,8 @@ export function DialogCustomProvider(props: Props) { const output = validateCustomProvider({ form, t: language.t, - disabledProviders: globalSync.data.config.disabled_providers ?? [], - existingProviderIDs: new Set(globalSync.data.provider.all.keys()), + disabledProviders: serverSync.data.config.disabled_providers ?? [], + existingProviderIDs: new Set(serverSync.data.provider.all.keys()), }) batch(() => { setForm("err", output.err) @@ -118,11 +118,11 @@ export function DialogCustomProvider(props: Props) { const saveMutation = useMutation(() => ({ mutationFn: async (result: NonNullable>) => { - const disabledProviders = globalSync.data.config.disabled_providers ?? [] + const disabledProviders = serverSync.data.config.disabled_providers ?? [] const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) if (result.key) { - await globalSDK.client.auth.set({ + await serverSDK.client.auth.set({ providerID: result.providerID, auth: { type: "api", @@ -131,7 +131,7 @@ export function DialogCustomProvider(props: Props) { }) } - await globalSync.updateConfig({ + await serverSync.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled, }) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 50b9daeef..21677b72e 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -6,8 +6,8 @@ import { useMutation } from "@tanstack/solid-query" import { Icon } from "@opencode-ai/ui/icon" import { createMemo, For, Show } from "solid-js" import { createStore } from "solid-js/store" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" import { type LocalProject, getAvatarColors } from "@/context/layout" import { getFilename } from "@opencode-ai/core/util/path" import { Avatar } from "@opencode-ai/ui/avatar" @@ -18,8 +18,8 @@ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] a export function DialogEditProject(props: { project: LocalProject }) { const dialog = useDialog() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() + const serverSDK = useServerSDK() + const serverSync = useServerSync() const language = useLanguage() const folderName = createMemo(() => getFilename(props.project.worktree)) @@ -78,19 +78,19 @@ export function DialogEditProject(props: { project: LocalProject }) { const start = store.startup.trim() if (props.project.id && props.project.id !== "global") { - await globalSDK.client.project.update({ + await serverSDK.client.project.update({ projectID: props.project.id, directory: props.project.worktree, name, icon: { color: store.color || "", override: store.iconOverride || "" }, commands: { start }, }) - globalSync.project.icon(props.project.worktree, store.iconOverride || undefined) + serverSync.project.icon(props.project.worktree, store.iconOverride || undefined) dialog.close() return } - globalSync.project.meta(props.project.worktree, { + serverSync.project.meta(props.project.worktree, { name, icon: { color: store.color || undefined, override: store.iconOverride || undefined }, commands: { start: start || undefined }, diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 005d28709..c7c3b5098 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -6,8 +6,8 @@ import type { ListRef } from "@opencode-ai/ui/list" import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import fuzzysort from "fuzzysort" import { createMemo, createResource, createSignal } from "solid-js" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" import { useLayout } from "@/context/layout" import { useLanguage } from "@/context/language" @@ -128,7 +128,7 @@ function uniqueRows(rows: Row[]) { } function useDirectorySearch(args: { - sdk: ReturnType + sdk: ReturnType start: () => string | undefined home: () => string }) { @@ -246,8 +246,8 @@ function useDirectorySearch(args: { } export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { - const sync = useGlobalSync() - const sdk = useGlobalSDK() + const sync = useServerSync() + const sdk = useServerSDK() const layout = useLayout() const dialog = useDialog() const language = useLanguage() diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index ac3bc03e4..835ace661 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -9,8 +9,8 @@ import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { useNavigate } from "@solidjs/router" import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" import { formatKeybind, useCommand, type CommandOption } from "@/context/command" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" import { useLayout } from "@/context/layout" import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" @@ -175,7 +175,7 @@ function createFileEntries(props: { function createSessionEntries(props: { workspaces: () => string[] label: (directory: string) => string - globalSDK: ReturnType + serverSDK: ReturnType language: ReturnType }) { const state: { @@ -207,7 +207,7 @@ function createSessionEntries(props: { state.inflight = Promise.all( dirs.map((directory) => { const description = props.label(directory) - return props.globalSDK.client.session + return props.serverSDK.client.session .list({ directory, roots: true }) .then((x) => (x.data ?? []) @@ -268,8 +268,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil const file = useFile() const dialog = useDialog() const navigate = useNavigate() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() + const serverSDK = useServerSDK() + const serverSync = useServerSync() const { params, tabs, view } = useSessionLayout() const filesOnly = () => props.mode === "files" const state = { cleanup: undefined as (() => void) | void, committed: false } @@ -292,21 +292,21 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil if (directory && !dirs.includes(directory)) return [...dirs, directory] return dirs }) - const homedir = createMemo(() => globalSync.data.path.home) + const homedir = createMemo(() => serverSync.data.path.home) const label = (directory: string) => { const current = project() const kind = current && directory === current.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") - const [store] = globalSync.child(directory, { bootstrap: false }) + const [store] = serverSync.child(directory, { bootstrap: false }) const home = homedir() const path = home ? directory.replace(home, "~") : directory const name = store.vcs?.branch ?? getFilename(directory) return `${kind} : ${name || path}` } - const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language }) + const { sessions } = createSessionEntries({ workspaces, label, serverSDK, language }) const items = async (text: string) => { const query = text.trim() diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index 5a28173ea..77cfcc039 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -6,7 +6,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" import { useLanguage } from "@/context/language" -import { useQueryOptions } from "@/context/global-sync" +import { useQueryOptions } from "@/context/server-sync" import { pathKey } from "@/utils/path-key" const statusLabels = { diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 4e075fd95..a087c366e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -66,7 +66,7 @@ import { PromptDragOverlay } from "./prompt-input/drag-overlay" import { promptPlaceholder } from "./prompt-input/placeholder" import { ImagePreview } from "@opencode-ai/ui/image-preview" import { useQueries } from "@tanstack/solid-query" -import { useQueryOptions } from "@/context/global-sync" +import { useQueryOptions } from "@/context/server-sync" import { pathKey } from "@/utils/path-key" import { getFilename } from "@opencode-ai/core/util/path" diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 83b6212dc..b1b289a27 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -162,8 +162,8 @@ beforeAll(async () => { }), })) - mock.module("@/context/global-sync", () => ({ - useGlobalSync: () => ({ + mock.module("@/context/server-sync", () => ({ + useServerSync: () => ({ child: (directory: string) => { syncedDirectories.push(directory) storedSessions[directory] ??= [] diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 05f0a3ed2..1570da016 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -5,7 +5,7 @@ import { Binary } from "@opencode-ai/core/util/binary" import { useNavigate, useParams } from "@solidjs/router" import { batch, type Accessor } from "solid-js" import type { FileSelection } from "@/context/file" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { useLocal } from "@/context/local" @@ -38,7 +38,7 @@ export type FollowupDraft = { type FollowupSendInput = { client: ReturnType["client"] - globalSync: ReturnType + serverSync: ReturnType sync: ReturnType draft: FollowupDraft messageID?: string @@ -53,7 +53,7 @@ const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttac export async function sendFollowupDraft(input: FollowupSendInput) { const text = draftText(input.draft.prompt) const images = draftImages(input.draft.prompt) - const [, setStore] = input.globalSync.child(input.draft.sessionDirectory) + const [, setStore] = input.serverSync.child(input.draft.sessionDirectory) const setBusy = () => { if (!input.optimisticBusy) return @@ -205,7 +205,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { const navigate = useNavigate() const sdk = useSDK() const sync = useSync() - const globalSync = useGlobalSync() + const serverSync = useServerSync() const local = useLocal() const permission = usePermission() const prompt = usePrompt() @@ -226,8 +226,8 @@ export function createPromptSubmit(input: PromptSubmitInput) { const sessionID = params.id if (!sessionID) return Promise.resolve() - globalSync.todo.set(sessionID, []) - const [, setStore] = globalSync.child(sdk.directory) + serverSync.todo.set(sessionID, []) + const [, setStore] = serverSync.child(sdk.directory) setStore("todo", sessionID, []) input.onAbort?.() @@ -273,7 +273,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { } const seed = (dir: string, info: Session) => { - const [, setStore] = globalSync.child(dir) + const [, setStore] = serverSync.child(dir) setStore("session", (list: Session[]) => { const result = Binary.search(list, info.id, (item) => item.id) const next = [...list] @@ -354,7 +354,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { directory: sessionDirectory, throwOnError: true, }) - globalSync.child(sessionDirectory) + serverSync.child(sessionDirectory) } input.onNewSessionWorktreeReset?.() @@ -557,7 +557,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { void sendFollowupDraft({ client, sync, - globalSync, + serverSync, draft, messageID, optimisticBusy: sessionDirectory === projectDirectory, diff --git a/packages/app/src/components/session/session-new-design-view.tsx b/packages/app/src/components/session/session-new-design-view.tsx index 302311d57..c9147fb58 100644 --- a/packages/app/src/components/session/session-new-design-view.tsx +++ b/packages/app/src/components/session/session-new-design-view.tsx @@ -1,7 +1,7 @@ import type { JSX } from "solid-js" import { createMemo } from "solid-js" import { useNavigate } from "@solidjs/router" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSync } from "@/context/server-sync" import { useLayout } from "@/context/layout" import { useSDK } from "@/context/sdk" import { useServer } from "@/context/server" @@ -15,7 +15,7 @@ import { WordmarkV2 } from "@opencode-ai/ui/v2/components/wordmark-v2.jsx" const MAIN_WORKTREE = "main" export function NewSessionDesignView(props: { worktree: string; children: JSX.Element }) { - const globalSync = useGlobalSync() + const serverSync = useServerSync() const layout = useLayout() const navigate = useNavigate() const sdk = useSDK() @@ -24,7 +24,7 @@ export function NewSessionDesignView(props: { worktree: string; children: JSX.El const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory) const projects = createMemo(() => { - const roots = globalSync.data.project.map((project) => project.worktree) + const roots = serverSync.data.project.map((project) => project.worktree) if (roots.includes(projectRoot())) return roots return [projectRoot(), ...roots] }) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index d9462e041..b94f42897 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -12,8 +12,8 @@ import { useParams } from "@solidjs/router" import { useLanguage } from "@/context/language" import { usePermission } from "@/context/permission" import { usePlatform, type DisplayBackend } from "@/context/platform" -import { useGlobalSync } from "@/context/global-sync" -import { useGlobalSDK } from "@/context/global-sdk" +import { useServerSync } from "@/context/server-sync" +import { useServerSDK } from "@/context/server-sdk" import { monoDefault, monoFontFamily, @@ -175,8 +175,8 @@ export const SettingsGeneral: Component = () => { const themeOptions = createMemo(() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) - const globalSync = useGlobalSync() - const globalSdk = useGlobalSDK() + const serverSync = useServerSync() + const globalSdk = useServerSDK() const [shells] = createResource( () => @@ -204,11 +204,11 @@ export const SettingsGeneral: Component = () => { }) const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") } - const currentShell = createMemo(() => globalSync.data.config.shell ?? "") + const currentShell = createMemo(() => serverSync.data.config.shell ?? "") const shellOptions = createMemo(() => { const list = shells.latest - const current = globalSync.data.config.shell + const current = serverSync.data.config.shell const nameCounts = new Map() for (const s of list) { @@ -343,7 +343,7 @@ export const SettingsGeneral: Component = () => { onSelect={(option) => { if (!option) return if (option.value === currentShell()) return - globalSync.updateConfig({ shell: option.value }) + serverSync.updateConfig({ shell: option.value }) }} variant="secondary" size="small" diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index cc69327f8..ffd85f97d 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -6,8 +6,8 @@ import { showToast } from "@opencode-ai/ui/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" import { DialogConnectProvider } from "./dialog-connect-provider" import { DialogSelectProvider } from "./dialog-select-provider" import { DialogCustomProvider } from "./dialog-custom-provider" @@ -30,8 +30,8 @@ const PROVIDER_NOTES = [ export const SettingsProviders: Component = () => { const dialog = useDialog() const language = useLanguage() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() + const serverSDK = useServerSDK() + const serverSync = useServerSync() const providers = useProviders() const connected = createMemo(() => { @@ -74,7 +74,7 @@ export const SettingsProviders: Component = () => { const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key const isConfigCustom = (providerID: string) => { - const provider = globalSync.data.config.provider?.[providerID] + const provider = serverSync.data.config.provider?.[providerID] if (!provider) return false if (provider.npm !== "@ai-sdk/openai-compatible") return false if (!provider.models || Object.keys(provider.models).length === 0) return false @@ -82,11 +82,11 @@ export const SettingsProviders: Component = () => { } const disableProvider = async (providerID: string, name: string) => { - const before = globalSync.data.config.disabled_providers ?? [] + const before = serverSync.data.config.disabled_providers ?? [] const next = before.includes(providerID) ? before : [...before, providerID] - globalSync.set("config", "disabled_providers", next) + serverSync.set("config", "disabled_providers", next) - await globalSync + await serverSync .updateConfig({ disabled_providers: next }) .then(() => { showToast({ @@ -97,7 +97,7 @@ export const SettingsProviders: Component = () => { }) }) .catch((err: unknown) => { - globalSync.set("config", "disabled_providers", before) + serverSync.set("config", "disabled_providers", before) const message = err instanceof Error ? err.message : String(err) showToast({ title: language.t("common.requestFailed"), description: message }) }) @@ -105,14 +105,14 @@ export const SettingsProviders: Component = () => { const disconnect = async (providerID: string, name: string) => { if (isConfigCustom(providerID)) { - await globalSDK.client.auth.remove({ providerID }).catch(() => undefined) + await serverSDK.client.auth.remove({ providerID }).catch(() => undefined) await disableProvider(providerID, name) return } - await globalSDK.client.auth + await serverSDK.client.auth .remove({ providerID }) .then(async () => { - await globalSDK.client.global.dispose() + await serverSDK.client.global.dispose() showToast({ variant: "success", icon: "circle-check", diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx index b38c98f14..1febaf2a2 100644 --- a/packages/app/src/components/status-popover-body.tsx +++ b/packages/app/src/components/status-popover-body.tsx @@ -15,7 +15,7 @@ import { useSDK } from "@/context/sdk" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { useSync } from "@/context/sync" import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" -import { useQueryOptions } from "@/context/global-sync" +import { useQueryOptions } from "@/context/server-sync" import { pathKey } from "@/utils/path-key" const pollMs = 10_000 diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 1df7c4352..7bcdfe121 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -16,7 +16,7 @@ import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { WindowsAppMenu } from "./windows-app-menu" import { applyPath, backPath, forwardPath } from "./titlebar-history" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSync } from "@/context/server-sync" import { decodeDirectory } from "@/pages/directory-layout" import { iife } from "@opencode-ai/core/util/iife" import { base64Encode } from "@opencode-ai/core/util/encode" @@ -221,7 +221,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { {(_) => { - const globalSync = useGlobalSync() + const serverSync = useServerSync() const navigate = useNavigate() const homeMatch = useMatch(() => "/") @@ -391,7 +391,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { const base = mapArray( () => tabsStore, (tab) => { - const sync = globalSync.createDirSyncContext(tab.dir) + const sync = serverSync.createDirSyncContext(tab.dir) const session = sync.session.get(tab.sessionId) return session ? { ...tab, info: session } : null }, diff --git a/packages/app/src/context/directory-sync.ts b/packages/app/src/context/directory-sync.ts index 1fdeb6822..a54701f0d 100644 --- a/packages/app/src/context/directory-sync.ts +++ b/packages/app/src/context/directory-sync.ts @@ -8,10 +8,11 @@ import { getSessionPrefetchPromise, setSessionPrefetch, } from "./global-sync/session-prefetch" -import { useGlobalSync } from "./global-sync" +import { createServerSyncContext, useServerSync } from "./server-sync" import type { Message, OpencodeClient, Part } from "@opencode-ai/sdk/v2/client" import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache" import { diffs as list, message as clean } from "@/utils/diffs" +import { useServerSDK } from "./server-sdk" const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) @@ -164,16 +165,17 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti }) } -export const createDirSyncContext = (client: OpencodeClient, directory: string) => { - const globalSync = useGlobalSync() +export const createDirSyncContext = (directory: string, serverSync: ReturnType) => { + const serverSDK = useServerSDK() + const client = serverSDK.createClient({ directory, throwOnError: true }) - type Child = ReturnType<(typeof globalSync)["child"]> + type Child = ReturnType<(typeof serverSync)["child"]> type Setter = Child[1] - const current = createMemo(() => globalSync.child(directory)) + const current = createMemo(() => serverSync.child(directory)) const target = (directory?: string) => { if (!directory || directory === directory) return current() - return globalSync.child(directory) + return serverSync.child(directory) } const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/") const initialMessagePageSize = 80 @@ -239,7 +241,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) if (!first) break const stale = [...(seen.get(first) ?? [])] seen.delete(first) - const [, setStore] = globalSync.child(first, { bootstrap: false }) + const [, setStore] = serverSync.child(first, { bootstrap: false }) evict(first, setStore, stale) } return created @@ -267,7 +269,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) if (sessionIDs.length === 0) return clearSessionPrefetch(directory, sessionIDs) for (const sessionID of sessionIDs) { - globalSync.todo.set(sessionID, undefined) + serverSync.todo.set(sessionID, undefined) } setStore( produce((draft) => { @@ -324,7 +326,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) for (const messageID of next.confirmed) { clearOptimistic(input.directory, input.sessionID, messageID) } - const [store] = globalSync.child(input.directory, { bootstrap: false }) + const [store] = serverSync.child(input.directory, { bootstrap: false }) const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : [] const message = input.mode === "prepend" ? merge(cached, next.session) : next.session batch(() => { @@ -373,8 +375,8 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) }, get project() { const store = current()[0] - const match = Binary.search(globalSync.data.project, store.project, (p) => p.id) - if (match.found) return globalSync.data.project[match.index] + const match = Binary.search(serverSync.data.project, store.project, (p) => p.id) + if (match.found) return serverSync.data.project[match.index] return undefined }, session: { @@ -418,7 +420,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) }) }, async sync(sessionID: string, opts?: { force?: boolean }) { - const [store, setStore] = globalSync.child(directory) + const [store, setStore] = serverSync.child(directory) const key = keyFor(directory, sessionID) touch(directory, setStore, sessionID) @@ -488,7 +490,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) }) }, async diff(sessionID: string, opts?: { force?: boolean }) { - const [store, setStore] = globalSync.child(directory) + const [store, setStore] = serverSync.child(directory) touch(directory, setStore, sessionID) if (store.session_diff[sessionID] !== undefined && !opts?.force) return @@ -501,13 +503,13 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) ) }, async todo(sessionID: string, opts?: { force?: boolean }) { - const [store, setStore] = globalSync.child(directory) + const [store, setStore] = serverSync.child(directory) touch(directory, setStore, sessionID) const existing = store.todo[sessionID] - const cached = globalSync.data.session_todo[sessionID] + const cached = serverSync.data.session_todo[sessionID] if (existing !== undefined) { if (cached === undefined) { - globalSync.todo.set(sessionID, existing) + serverSync.todo.set(sessionID, existing) } if (!opts?.force) return } @@ -522,7 +524,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) if (!tracked(directory, sessionID)) return const list = todo.data ?? [] setStore("todo", sessionID, reconcile(list, { key: "id" })) - globalSync.todo.set(sessionID, list) + serverSync.todo.set(sessionID, list) }), ) }, @@ -540,7 +542,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) return meta.loading[key] ?? false }, async loadMore(sessionID: string, count?: number) { - const [, setStore] = globalSync.child(directory) + const [, setStore] = serverSync.child(directory) touch(directory, setStore, sessionID) const key = keyFor(directory, sessionID) const step = count ?? historyMessagePageSize @@ -561,12 +563,12 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) }, }, evict(sessionID: string, _directory = directory) { - const [, setStore] = globalSync.child(_directory) + const [, setStore] = serverSync.child(_directory) seenFor(_directory).delete(sessionID) evict(_directory, setStore, [sessionID]) }, fetch: async (count = 10) => { - const [store, setStore] = globalSync.child(directory) + const [store, setStore] = serverSync.child(directory) setStore("limit", (x) => x + count) await client.session.list().then((x) => { const sessions = (x.data ?? []) @@ -578,7 +580,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string) }, more: createMemo(() => current()[0].session.length >= current()[0].limit), archive: async (sessionID: string) => { - const [, setStore] = globalSync.child(directory) + const [, setStore] = serverSync.child(directory) await client.session.update({ sessionID, time: { archived: Date.now() } }) setStore( produce((draft) => { diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx deleted file mode 100644 index 18ad5ab67..000000000 --- a/packages/app/src/context/global-sdk.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import type { Event } from "@opencode-ai/sdk/v2/client" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { makeEventListener } from "@solid-primitives/event-listener" -import { batch, onCleanup, onMount } from "solid-js" -import { createSdkForServer } from "@/utils/server" -import { useLanguage } from "./language" -import { usePlatform } from "./platform" -import { useServer } from "./server" - -const isAbortError = (error: unknown) => - error !== null && typeof error === "object" && "name" in error && error.name === "AbortError" - -export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({ - name: "GlobalSDK", - init: () => { - const language = useLanguage() - const server = useServer() - const platform = usePlatform() - const abort = new AbortController() - - const eventFetch = (() => { - if (!platform.fetch || !server.current) return - try { - const url = new URL(server.current.http.url) - const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" - if (url.protocol === "http:" && !loopback) return platform.fetch - } catch { - return - } - })() - - const currentServer = server.current - if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable")) - - const eventSdk = createSdkForServer({ - signal: abort.signal, - fetch: eventFetch, - server: currentServer.http, - }) - const emitter = createGlobalEmitter<{ - [key: string]: Event - }>() - - type Queued = { directory: string; payload: Event } - const FLUSH_FRAME_MS = 16 - const STREAM_YIELD_MS = 8 - const RECONNECT_DELAY_MS = 250 - - let queue: Queued[] = [] - let buffer: Queued[] = [] - const coalesced = new Map() - const staleDeltas = new Set() - let timer: ReturnType | undefined - let last = 0 - - const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}` - - const key = (directory: string, payload: Event) => { - if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}` - if (payload.type === "lsp.updated") return `lsp.updated:${directory}` - if (payload.type === "message.part.updated") { - const part = payload.properties.part - return `message.part.updated:${directory}:${part.messageID}:${part.id}` - } - } - - const flush = () => { - if (timer) clearTimeout(timer) - timer = undefined - - if (queue.length === 0) return - - const events = queue - const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined - queue = buffer - buffer = events - queue.length = 0 - coalesced.clear() - staleDeltas.clear() - - last = Date.now() - batch(() => { - for (const event of events) { - if (skip && event.payload.type === "message.part.delta") { - const props = event.payload.properties - if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue - } - emitter.emit(event.directory, event.payload) - } - }) - - buffer.length = 0 - } - - const schedule = () => { - if (timer) return - const elapsed = Date.now() - last - timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed)) - } - - let streamErrorLogged = false - const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) - const aborted = isAbortError - - let attempt: AbortController | undefined - let run: Promise | undefined - let started = false - const HEARTBEAT_TIMEOUT_MS = 15_000 - let lastEventAt = Date.now() - let heartbeat: ReturnType | undefined - const resetHeartbeat = () => { - lastEventAt = Date.now() - if (heartbeat) clearTimeout(heartbeat) - heartbeat = setTimeout(() => { - attempt?.abort() - }, HEARTBEAT_TIMEOUT_MS) - } - const clearHeartbeat = () => { - if (!heartbeat) return - clearTimeout(heartbeat) - heartbeat = undefined - } - - const start = () => { - if (started) return run - started = true - run = (async () => { - // oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit - while (!abort.signal.aborted && started) { - attempt = new AbortController() - lastEventAt = Date.now() - const onAbort = () => { - attempt?.abort() - } - abort.signal.addEventListener("abort", onAbort) - try { - const events = await eventSdk.global.event({ - signal: attempt.signal, - onSseError: (error) => { - if (aborted(error)) return - if (streamErrorLogged) return - streamErrorLogged = true - console.error("[global-sdk] event stream error", { - url: currentServer.http.url, - fetch: eventFetch ? "platform" : "webview", - error, - }) - }, - }) - let yielded = Date.now() - resetHeartbeat() - for await (const event of events.stream) { - resetHeartbeat() - streamErrorLogged = false - const directory = event.directory ?? "global" - if (event.payload.type === "sync") { - continue - } - - const payload = event.payload as Event - - const k = key(directory, payload) - if (k) { - const i = coalesced.get(k) - if (i !== undefined) { - queue[i] = { directory, payload } - if (payload.type === "message.part.updated") { - const part = payload.properties.part - staleDeltas.add(deltaKey(directory, part.messageID, part.id)) - } - continue - } - coalesced.set(k, queue.length) - } - queue.push({ directory, payload }) - schedule() - - if (Date.now() - yielded < STREAM_YIELD_MS) continue - yielded = Date.now() - await wait(0) - } - } catch (error) { - if (!aborted(error) && !streamErrorLogged) { - streamErrorLogged = true - console.error("[global-sdk] event stream failed", { - url: currentServer.http.url, - fetch: eventFetch ? "platform" : "webview", - error, - }) - } - } finally { - abort.signal.removeEventListener("abort", onAbort) - attempt = undefined - clearHeartbeat() - } - - if (abort.signal.aborted || !started) return - await wait(RECONNECT_DELAY_MS) - } - })().finally(() => { - run = undefined - flush() - }) - return run - } - - const stop = () => { - started = false - attempt?.abort() - clearHeartbeat() - } - - onMount(() => { - makeEventListener(document, "visibilitychange", () => { - if (document.visibilityState !== "visible") return - if (!started) return - if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return - attempt?.abort() - }) - }) - - onCleanup(() => { - stop() - abort.abort() - flush() - }) - - const sdk = createSdkForServer({ - server: server.current.http, - fetch: platform.fetch, - throwOnError: true, - }) - - const dirSyncContexts = new Map>() - const dirSdkContextRefCounts = new Map() - - return { - url: currentServer.http.url, - client: sdk, - event: { - on: emitter.on.bind(emitter), - listen: emitter.listen.bind(emitter), - start, - }, - createClient(opts: Omit[0], "server" | "fetch">) { - const s = server.current - if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable")) - return createSdkForServer({ - server: s.http, - fetch: platform.fetch, - ...opts, - }) - }, - createDirSyncContext: (directory: string) => { - onCleanup(() => { - dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) - 1) - if (dirSdkContextRefCounts.get(directory) === 0) { - dirSyncContexts.delete(directory) - dirSdkContextRefCounts.delete(directory) - } - }) - - const cached = dirSyncContexts.get(directory) - if (cached) { - dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) + 1) - return cached - } - const ctx = createDirSdkContext(directory) - dirSyncContexts.set(directory, ctx) - dirSdkContextRefCounts.set(directory, 1) - - return ctx - }, - } - }, -}) - -type SDKEventMap = { - [key in Event["type"]]: Extract -} - -function createDirSdkContext(directory: string) { - const globalSDK = useGlobalSDK() - - const client = globalSDK.createClient({ - directory, - throwOnError: true, - }) - - const emitter = createGlobalEmitter() - - const unsub = globalSDK.event.on(directory, (event) => { - emitter.emit(event.type, event) - }) - onCleanup(unsub) - - return { - directory, - client, - event: emitter, - get url() { - return globalSDK.url - }, - createClient(opts: Parameters[0]) { - return globalSDK.createClient(opts) - }, - } -} diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 971932fb0..b6e2f1085 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -18,7 +18,7 @@ import type { State, VcsCache } from "./types" import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" import { QueryClient, queryOptions } from "@tanstack/solid-query" -import { loadMcpQuery } from "../global-sync" +import { loadMcpQuery } from "../server-sync" import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" type GlobalStore = { @@ -105,7 +105,7 @@ export const loadProjectsQuery = (sdk: OpencodeClient) => }) export async function bootstrapGlobal(input: { - globalSDK: OpencodeClient + serverSDK: OpencodeClient requestFailedTitle: string translate: (key: string, vars?: Record) => string formatMoreCount: (count: number) => string @@ -113,12 +113,12 @@ export async function bootstrapGlobal(input: { queryClient: QueryClient }) { const slow = [ - () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)), - () => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)), - () => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)), + () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.serverSDK)), + () => input.queryClient.fetchQuery(loadProvidersQuery(null, input.serverSDK)), + () => input.queryClient.fetchQuery(loadPathQuery(null, input.serverSDK)), () => input.queryClient - .fetchQuery(loadProjectsQuery(input.globalSDK)) + .fetchQuery(loadProjectsQuery(input.serverSDK)) .then((data) => input.setGlobalStore("project", data)), ] await runAll(slow) diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts index 89d6ee1d8..d140b3e76 100644 --- a/packages/app/src/context/global-sync/child-store.test.ts +++ b/packages/app/src/context/global-sync/child-store.test.ts @@ -3,7 +3,7 @@ import { createRoot, getOwner, type Owner } from "solid-js" import { createStore } from "solid-js/store" import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context" import type { State } from "./types" -import type { QueryOptionsApi } from "../global-sync" +import type { QueryOptionsApi } from "../server-sync" let createChildStoreManager: typeof import("./child-store").createChildStoreManager diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 40c3c3ae9..db6dd3101 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -15,7 +15,7 @@ import { } from "./types" import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction" import { useQueries } from "@tanstack/solid-query" -import { QueryOptionsApi } from "../global-sync" +import { QueryOptionsApi } from "../server-sync" import { directoryKey, type DirectoryKey } from "./utils" import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 0d37dd26a..9306d362d 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -2,8 +2,8 @@ import { createStore, produce } from "solid-js/store" import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { makeEventListener } from "@solid-primitives/event-listener" -import { useGlobalSync } from "./global-sync" -import { useGlobalSDK } from "./global-sdk" +import { useServerSync } from "./server-sync" +import { useServerSDK } from "./server-sdk" import { useServer } from "./server" import { usePlatform } from "./platform" import { Project } from "@opencode-ai/sdk/v2" @@ -136,8 +136,8 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => { export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ name: "Layout", init: () => { - const globalSdk = useGlobalSDK() - const globalSync = useGlobalSync() + const globalSdk = useServerSDK() + const serverSync = useServerSync() const server = useServer() const platform = usePlatform() @@ -386,11 +386,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( } function enrich(project: { worktree: string; expanded: boolean }) { - const [childStore] = globalSync.child(project.worktree, { bootstrap: false }) + const [childStore] = serverSync.child(project.worktree, { bootstrap: false }) const projectID = childStore.project const metadata = projectID - ? globalSync.data.project.find((x) => x.id === projectID) - : globalSync.data.project.find((x) => x.worktree === project.worktree) + ? serverSync.data.project.find((x) => x.id === projectID) + : serverSync.data.project.find((x) => x.worktree === project.worktree) // Preserve local icon override from per-workspace localStorage cache (childStore.icon). // Without this, different subdirectories of the same git repo would share the same @@ -404,7 +404,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const roots = createMemo(() => { const map = new Map() - for (const project of globalSync.data.project) { + for (const project of serverSync.data.project) { const sandboxes = project.sandboxes ?? [] for (const sandbox of sandboxes) { map.set(sandbox, project.worktree) @@ -470,12 +470,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( createEffect(() => { const projects = enriched() if (projects.length === 0) return - if (!globalSync.ready) return + if (!serverSync.ready) return for (const project of projects) { if (!project.id) continue if (project.id === "global") continue - globalSync.project.icon(project.worktree, project.icon?.override) + serverSync.project.icon(project.worktree, project.icon?.override) } }) @@ -509,7 +509,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( colorRequested.set(worktree, color) if (project.id === "global") { - globalSync.project.meta(worktree, { icon: { color } }) + serverSync.project.meta(worktree, { icon: { color } }) continue } @@ -531,7 +531,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( sessionTimer = undefined void Promise.all( server.projects.list().map((project) => { - return globalSync.project.loadSessions(project.worktree) + return serverSync.project.loadSessions(project.worktree) }), ) }, 0) @@ -560,7 +560,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( open(directory: string) { const root = rootFor(directory) if (server.projects.list().find((x) => x.worktree === root)) return - void globalSync.project.loadSessions(root) + void serverSync.project.loadSessions(root) server.projects.open(root) }, close(directory: string) { diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index c926dc1d9..5b6982425 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -2,8 +2,8 @@ import { createStore, reconcile } from "solid-js/store" import { batch, createEffect, createMemo, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { createSimpleContext } from "@opencode-ai/ui/context" -import { useGlobalSDK } from "./global-sdk" -import { useGlobalSync } from "./global-sync" +import { useServerSDK } from "./server-sdk" +import { useServerSync } from "./server-sync" import { usePlatform } from "@/context/platform" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" @@ -109,8 +109,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi name: "Notification", init: () => { const params = useParams() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() + const serverSDK = useServerSDK() + const serverSync = useServerSync() const platform = usePlatform() const settings = useSettings() const language = useLanguage() @@ -207,10 +207,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi const lookup = async (directory: string, sessionID?: string) => { if (!sessionID) return undefined - const [syncStore] = globalSync.child(directory, { bootstrap: false }) + const [syncStore] = serverSync.child(directory, { bootstrap: false }) const match = Binary.search(syncStore.session, sessionID, (s) => s.id) if (match.found) return syncStore.session[match.index] - return globalSDK.client.session + return serverSDK.client.session .get({ directory, sessionID }) .then((x) => x.data) .catch(() => undefined) @@ -285,7 +285,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi }) } - const unsub = globalSDK.event.listen((e) => { + const unsub = serverSDK.event.listen((e) => { const event = e.details if (event.type !== "session.idle" && event.type !== "session.error") return diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx index 672f84f82..af55f3d26 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -3,8 +3,8 @@ import { createStore, produce } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import type { PermissionRequest } from "@opencode-ai/sdk/v2/client" import { Persist, persisted } from "@/utils/persist" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "./global-sync" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "./server-sync" import { useParams } from "@solidjs/router" import { decode64 } from "@/utils/base64" import { @@ -48,13 +48,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple name: "Permission", init: () => { const params = useParams() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() + const serverSDK = useServerSDK() + const serverSync = useServerSync() const permissionsEnabled = createMemo(() => { const directory = decode64(params.dir) if (!directory) return false - const [store] = globalSync.child(directory) + const [store] = serverSync.child(directory) return hasPermissionPromptRules(store.config.permission) }) @@ -86,7 +86,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple if (!ready()) return const directory = decode64(params.dir) if (!directory) return - const [childStore] = globalSync.child(directory) + const [childStore] = serverSync.child(directory) const perm = childStore.config.permission if (typeof perm === "string" && perm === "allow") { const key = directoryAcceptKey(directory) @@ -118,7 +118,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple } const respond: PermissionRespondFn = (input) => { - globalSDK.client.permission.respond(input).catch(() => { + serverSDK.client.permission.respond(input).catch(() => { responded.delete(input.permissionID) }) } @@ -139,7 +139,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple } function isAutoAccepting(sessionID: string, directory?: string) { - const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : [] + const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : [] return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory) } @@ -148,7 +148,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple } function shouldAutoRespond(permission: PermissionRequest, directory?: string) { - const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : [] + const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : [] return autoRespondsPermission(store.autoAccept, session, permission, directory) } @@ -159,7 +159,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple return next } - const unsubscribe = globalSDK.event.listen((e) => { + const unsubscribe = serverSDK.event.listen((e) => { const event = e.details if (event?.type !== "permission.asked") return @@ -178,7 +178,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple }), ) - globalSDK.client.permission + serverSDK.client.permission .list({ directory }) .then((x) => { if (!isAutoAcceptingDirectory(directory)) return @@ -210,7 +210,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple }), ) - globalSDK.client.permission + serverSDK.client.permission .list({ directory }) .then((x) => { if (enableVersion.get(key) !== version) return @@ -268,7 +268,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple }, permissionsEnabled, isPermissionAllowAll(directory: string) { - const [childStore] = globalSync.child(directory) + const [childStore] = serverSync.child(directory) const perm = childStore.config.permission return typeof perm === "string" && perm === "allow" }, diff --git a/packages/app/src/context/sdk.tsx b/packages/app/src/context/sdk.tsx index 7f9edf365..46e8c1b21 100644 --- a/packages/app/src/context/sdk.tsx +++ b/packages/app/src/context/sdk.tsx @@ -1,11 +1,11 @@ import { createSimpleContext } from "@opencode-ai/ui/context" -import { useGlobalSDK } from "./global-sdk" +import { useServerSDK } from "./server-sdk" export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", init: (props: { directory: string }) => { - const globalSDK = useGlobalSDK() + const serverSDK = useServerSDK() - return globalSDK.createDirSyncContext(props.directory) + return serverSDK.createDirSdkContext(props.directory) }, }) diff --git a/packages/app/src/context/server-sdk.tsx b/packages/app/src/context/server-sdk.tsx new file mode 100644 index 000000000..47446b2d8 --- /dev/null +++ b/packages/app/src/context/server-sdk.tsx @@ -0,0 +1,290 @@ +import type { Event } from "@opencode-ai/sdk/v2/client" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { createGlobalEmitter } from "@solid-primitives/event-bus" +import { makeEventListener } from "@solid-primitives/event-listener" +import { batch, onCleanup, onMount } from "solid-js" +import { createSdkForServer } from "@/utils/server" +import { useLanguage } from "./language" +import { usePlatform } from "./platform" +import { ServerConnection, useServer } from "./server" +import { createRefCountMap } from "@/utils/refcount" + +const isAbortError = (error: unknown) => + error !== null && typeof error === "object" && "name" in error && error.name === "AbortError" + +function createServerSdkContext(server: ServerConnection.Any) { + const platform = usePlatform() + const abort = new AbortController() + + const eventFetch = (() => { + if (!platform.fetch || !server) return + try { + const url = new URL(server.http.url) + const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" + if (url.protocol === "http:" && !loopback) return platform.fetch + } catch { + return + } + })() + + const eventSdk = createSdkForServer({ + signal: abort.signal, + fetch: eventFetch, + server: server.http, + }) + const emitter = createGlobalEmitter<{ + [key: string]: Event + }>() + + type Queued = { directory: string; payload: Event } + const FLUSH_FRAME_MS = 16 + const STREAM_YIELD_MS = 8 + const RECONNECT_DELAY_MS = 250 + + let queue: Queued[] = [] + let buffer: Queued[] = [] + const coalesced = new Map() + const staleDeltas = new Set() + let timer: ReturnType | undefined + let last = 0 + + const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}` + + const key = (directory: string, payload: Event) => { + if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}` + if (payload.type === "lsp.updated") return `lsp.updated:${directory}` + if (payload.type === "message.part.updated") { + const part = payload.properties.part + return `message.part.updated:${directory}:${part.messageID}:${part.id}` + } + } + + const flush = () => { + if (timer) clearTimeout(timer) + timer = undefined + + if (queue.length === 0) return + + const events = queue + const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined + queue = buffer + buffer = events + queue.length = 0 + coalesced.clear() + staleDeltas.clear() + + last = Date.now() + batch(() => { + for (const event of events) { + if (skip && event.payload.type === "message.part.delta") { + const props = event.payload.properties + if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue + } + emitter.emit(event.directory, event.payload) + } + }) + + buffer.length = 0 + } + + const schedule = () => { + if (timer) return + const elapsed = Date.now() - last + timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed)) + } + + let streamErrorLogged = false + const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + const aborted = isAbortError + + let attempt: AbortController | undefined + let run: Promise | undefined + let started = false + const HEARTBEAT_TIMEOUT_MS = 15_000 + let lastEventAt = Date.now() + let heartbeat: ReturnType | undefined + const resetHeartbeat = () => { + lastEventAt = Date.now() + if (heartbeat) clearTimeout(heartbeat) + heartbeat = setTimeout(() => { + attempt?.abort() + }, HEARTBEAT_TIMEOUT_MS) + } + const clearHeartbeat = () => { + if (!heartbeat) return + clearTimeout(heartbeat) + heartbeat = undefined + } + + const start = () => { + if (started) return run + started = true + run = (async () => { + // oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit + while (!abort.signal.aborted && started) { + attempt = new AbortController() + lastEventAt = Date.now() + const onAbort = () => { + attempt?.abort() + } + abort.signal.addEventListener("abort", onAbort) + try { + const events = await eventSdk.global.event({ + signal: attempt.signal, + onSseError: (error) => { + if (aborted(error)) return + if (streamErrorLogged) return + streamErrorLogged = true + console.error("[global-sdk] event stream error", { + url: server.http.url, + fetch: eventFetch ? "platform" : "webview", + error, + }) + }, + }) + let yielded = Date.now() + resetHeartbeat() + for await (const event of events.stream) { + resetHeartbeat() + streamErrorLogged = false + const directory = event.directory ?? "global" + if (event.payload.type === "sync") { + continue + } + + const payload = event.payload as Event + + const k = key(directory, payload) + if (k) { + const i = coalesced.get(k) + if (i !== undefined) { + queue[i] = { directory, payload } + if (payload.type === "message.part.updated") { + const part = payload.properties.part + staleDeltas.add(deltaKey(directory, part.messageID, part.id)) + } + continue + } + coalesced.set(k, queue.length) + } + queue.push({ directory, payload }) + schedule() + + if (Date.now() - yielded < STREAM_YIELD_MS) continue + yielded = Date.now() + await wait(0) + } + } catch (error) { + if (!aborted(error) && !streamErrorLogged) { + streamErrorLogged = true + console.error("[global-sdk] event stream failed", { + url: server.http.url, + fetch: eventFetch ? "platform" : "webview", + error, + }) + } + } finally { + abort.signal.removeEventListener("abort", onAbort) + attempt = undefined + clearHeartbeat() + } + + if (abort.signal.aborted || !started) return + await wait(RECONNECT_DELAY_MS) + } + })().finally(() => { + run = undefined + flush() + }) + return run + } + + const stop = () => { + started = false + attempt?.abort() + clearHeartbeat() + } + + onMount(() => { + makeEventListener(document, "visibilitychange", () => { + if (document.visibilityState !== "visible") return + if (!started) return + if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return + attempt?.abort() + }) + }) + + onCleanup(() => { + stop() + abort.abort() + flush() + }) + + const sdk = createSdkForServer({ + server: server.http, + fetch: platform.fetch, + throwOnError: true, + }) + + return { + url: server.http.url, + client: sdk, + event: { + on: emitter.on.bind(emitter), + listen: emitter.listen.bind(emitter), + start, + }, + createClient(opts: Omit[0], "server" | "fetch">) { + return createSdkForServer({ + server: server.http, + fetch: platform.fetch, + ...opts, + }) + }, + } +} + +export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleContext({ + name: "ServerSDK", + init: () => { + const language = useLanguage() + const server = useServer() + + if (!server.current) throw new Error(language.t("error.serverSDK.noServerAvailable")) + const sdk = createServerSdkContext(server.current) + return { + ...sdk, + createDirSdkContext: createRefCountMap((dir) => createDirSdkContext(dir, sdk)), + } + }, +}) + +type SDKEventMap = { + [key in Event["type"]]: Extract +} + +function createDirSdkContext(directory: string, serverSDK: ReturnType) { + const client = serverSDK.createClient({ + directory, + throwOnError: true, + }) + + const emitter = createGlobalEmitter() + + const unsub = serverSDK.event.on(directory, (event) => { + emitter.emit(event.type, event) + }) + onCleanup(unsub) + + return { + directory, + client, + event: emitter, + get url() { + return serverSDK.url + }, + createClient(opts: Parameters[0]) { + return serverSDK.createClient(opts) + }, + } +} diff --git a/packages/app/src/context/global-sync.test.ts b/packages/app/src/context/server-sync.test.ts similarity index 100% rename from packages/app/src/context/global-sync.test.ts rename to packages/app/src/context/server-sync.test.ts diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/server-sync.tsx similarity index 85% rename from packages/app/src/context/global-sync.tsx rename to packages/app/src/context/server-sync.tsx index 2e9ac1707..8b177e96c 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/server-sync.tsx @@ -5,7 +5,7 @@ import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, u import { createStore, produce, reconcile } from "solid-js/store" import { useLanguage } from "@/context/language" import type { InitError } from "../pages/error" -import { useGlobalSDK } from "./global-sdk" +import { useServerSDK } from "./server-sdk" import { bootstrapDirectory, bootstrapGlobal, @@ -29,7 +29,8 @@ import { createRefreshQueue } from "./global-sync/queue" import { directoryKey } from "./global-sync/utils" import { PathKey } from "@/utils/path-key" import { createDirSyncContext } from "./directory-sync" -import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" +import { createSimpleContext, NormalizedProviderListResponse } from "@opencode-ai/ui/context" +import { createRefCountMap } from "@/utils/refcount" type GlobalStore = { ready: boolean @@ -57,13 +58,13 @@ export const loadLspQuery = (directory: string, sdk: OpencodeClient) => queryFn: () => sdk.lsp.status().then((r) => r.data ?? []), }) -function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) { +function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) { return { - globalConfig: () => loadGlobalConfigQuery(globalSDK()), - projects: () => loadProjectsQuery(globalSDK()), + globalConfig: () => loadGlobalConfigQuery(serverSDK()), + projects: () => loadProjectsQuery(serverSDK()), providers: (directory: PathKey | null) => - loadProvidersQuery(directory, directory === null ? globalSDK() : sdkFor(directory)), - path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? globalSDK() : sdkFor(directory)), + loadProvidersQuery(directory, directory === null ? serverSDK() : sdkFor(directory)), + path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? serverSDK() : sdkFor(directory)), agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)), mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)), lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)), @@ -72,11 +73,11 @@ function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: Path } export type QueryOptionsApi = ReturnType -function createGlobalSync() { - const globalSDK = useGlobalSDK() +export function createServerSyncContext() { + const serverSDK = useServerSDK() const language = useLanguage() const owner = getOwner() - if (!owner) throw new Error("GlobalSync must be created within owner") + if (!owner) throw new Error("ServerSync must be created within owner") const sdkCache = new Map() const booting = new Map>() @@ -87,7 +88,7 @@ function createGlobalSync() { const key = directoryKey(directory) const cached = sdkCache.get(key) if (cached) return cached - const sdk = globalSDK.createClient({ + const sdk = serverSDK.createClient({ directory, throwOnError: true, }) @@ -95,7 +96,7 @@ function createGlobalSync() { return sdk } - const queryOptionsApi = makeQueryOptionsApi(() => globalSDK.client, sdkFor) + const queryOptionsApi = makeQueryOptionsApi(() => serverSDK.client, sdkFor) const [configQuery, providerQuery, pathQuery] = useQueries(() => ({ queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)], @@ -154,7 +155,7 @@ function createGlobalSync() { queryKey: ["bootstrap"], queryFn: async () => { await bootstrapGlobal({ - globalSDK: globalSDK.client, + serverSDK: serverSDK.client, requestFailedTitle: language.t("common.requestFailed"), translate: language.t, formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), @@ -248,7 +249,7 @@ function createGlobalSync() { loadRootSessionsWithFallback({ directory, limit, - list: (query) => globalSDK.client.session.list(query), + list: (query) => serverSDK.client.session.list(query), }) .then((x) => { const nonArchived = (x.data ?? []) @@ -334,7 +335,7 @@ function createGlobalSync() { return promise } - const unsub = globalSDK.event.listen((e) => { + const unsub = serverSDK.event.listen((e) => { const directory = e.name const key = directoryKey(directory) const event = e.details @@ -393,13 +394,13 @@ function createGlobalSync() { eventFrame = undefined eventTimer = setTimeout(() => { eventTimer = undefined - void globalSDK.event.start() + void serverSDK.event.start() }, 0) }) } else { eventTimer = setTimeout(() => { eventTimer = undefined - void globalSDK.event.start() + void serverSDK.event.start() }, 0) } }) @@ -415,7 +416,7 @@ function createGlobalSync() { } const updateConfigMutation = useMutation(() => ({ - mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }), + mutationFn: (config: Config) => serverSDK.client.global.config.update({ config }), onSuccess: () => { bootstrap.refetch() // Invalidate all provider queries so newly configured custom providers @@ -425,9 +426,6 @@ function createGlobalSync() { }, })) - const dirSyncContexts = new Map>() - const dirSyncContextRefCounts = new Map() - return { data: globalStore, set, @@ -446,42 +444,21 @@ function createGlobalSync() { todo: { set: setSessionTodo, }, - createDirSyncContext: (directory: string) => { - onCleanup(() => { - dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) - 1) - if (dirSyncContextRefCounts.get(directory) === 0) { - dirSyncContexts.delete(directory) - dirSyncContextRefCounts.delete(directory) - } - }) - - const cached = dirSyncContexts.get(directory) - if (cached) { - dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) + 1) - return cached - } - const ctx = createDirSyncContext(globalSDK.createClient({ directory, throwOnError: true }), directory) - dirSyncContexts.set(directory, ctx) - dirSyncContextRefCounts.set(directory, 1) - - return ctx - }, } } -const GlobalSyncContext = createContext>() +export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({ + name: "ServerSync", + init: () => { + const sync = createServerSyncContext() -export function GlobalSyncProvider(props: ParentProps) { - const value = createGlobalSync() - return {props.children} -} - -export function useGlobalSync() { - const context = useContext(GlobalSyncContext) - if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider") - return context -} + return { + ...sync, + createDirSyncContext: createRefCountMap((dir) => createDirSyncContext(dir, sync)), + } + }, +}) export function useQueryOptions() { - return useGlobalSync().queryOptions + return useServerSync().queryOptions } diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index e5bf1ccfa..1734d717b 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -1,5 +1,5 @@ import { Binary } from "@opencode-ai/core/util/binary" -import { useGlobalSync } from "./global-sync" +import { useServerSync } from "./server-sync" import { useSDK } from "./sdk" import type { Message, Part } from "@opencode-ai/sdk/v2/client" @@ -109,8 +109,8 @@ export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticR } export const useSync = () => { - const globalSync = useGlobalSync() + const serverSync = useServerSync() const sdk = useSDK() - return globalSync.createDirSyncContext(sdk.directory) + return serverSync.createDirSyncContext(sdk.directory) } diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 45ad8b07d..922b4ae85 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -1,4 +1,4 @@ -import { useGlobalSync } from "@/context/global-sync" +import { useServerSync } from "@/context/server-sync" import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" import { Iterable, pipe } from "effect" @@ -17,15 +17,15 @@ export const popularProviders = [ const popularProviderSet = new Set(popularProviders) export function useProviders() { - const globalSync = useGlobalSync() + const serverSync = useServerSync() const params = useParams() const dir = createMemo(() => decode64(params.dir) ?? "") const providers = () => { if (dir()) { - const [projectStore] = globalSync.child(dir()) + const [projectStore] = serverSync.child(dir()) if (projectStore.provider_ready) return projectStore.provider } - return globalSync.data.provider + return serverSync.data.provider } return { all: () => providers().all, diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 94f0158f3..5d124024a 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -413,7 +413,7 @@ export const dict = { "error.page.version": "الإصدار: {{version}}", "error.dev.rootNotFound": "لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟", - "error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟", + "error.serverSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟", "directory.error.invalidUrl": "دليل غير صالح في عنوان URL.", "error.chain.unknown": "خطأ غير معروف", "error.chain.causedBy": "بسبب:", @@ -837,8 +837,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "غير معروف", "error.page.circular": "[دائري]", - "error.globalSDK.noServerAvailable": "لا يوجد خادم متاح", - "error.globalSDK.serverNotAvailable": "الخادم غير متاح", + "error.serverSDK.noServerAvailable": "لا يوجد خادم متاح", + "error.serverSDK.serverNotAvailable": "الخادم غير متاح", "error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة", "error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة", "error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 070fa5319..7c6965e33 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -414,7 +414,7 @@ export const dict = { "error.page.version": "Versão: {{version}}", "error.dev.rootNotFound": "Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?", - "error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?", + "error.serverSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?", "directory.error.invalidUrl": "Diretório inválido na URL.", "error.chain.unknown": "Erro desconhecido", "error.chain.causedBy": "Causado por:", @@ -850,8 +850,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "desconhecido", "error.page.circular": "[Circular]", - "error.globalSDK.noServerAvailable": "Nenhum servidor disponível", - "error.globalSDK.serverNotAvailable": "Servidor indisponível", + "error.serverSDK.noServerAvailable": "Nenhum servidor disponível", + "error.serverSDK.serverNotAvailable": "Servidor indisponível", "error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente", "error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes", "error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 8f3e985aa..201b6ed81 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -462,7 +462,7 @@ export const dict = { "error.dev.rootNotFound": "Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?", - "error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?", + "error.serverSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?", "directory.error.invalidUrl": "Nevažeći direktorij u URL-u.", "error.chain.unknown": "Nepoznata greška", @@ -926,8 +926,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "nepoznato", "error.page.circular": "[Kružno]", - "error.globalSDK.noServerAvailable": "Nema dostupnog servera", - "error.globalSDK.serverNotAvailable": "Server nije dostupan", + "error.serverSDK.noServerAvailable": "Nema dostupnog servera", + "error.serverSDK.serverNotAvailable": "Server nije dostupan", "error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša", "error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta", "error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 9ae4b26bb..d52aca823 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -459,7 +459,7 @@ export const dict = { "error.dev.rootNotFound": "Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?", - "error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?", + "error.serverSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?", "directory.error.invalidUrl": "Ugyldig mappe i URL.", "error.chain.unknown": "Ukendt fejl", @@ -920,8 +920,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "ukendt", "error.page.circular": "[Cirkulær]", - "error.globalSDK.noServerAvailable": "Ingen server tilgængelig", - "error.globalSDK.serverNotAvailable": "Server ikke tilgængelig", + "error.serverSDK.noServerAvailable": "Ingen server tilgængelig", + "error.serverSDK.serverNotAvailable": "Server ikke tilgængelig", "error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache", "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata", "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 633ff8db2..4d5a64990 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -421,7 +421,7 @@ export const dict = { "error.page.version": "Version: {{version}}", "error.dev.rootNotFound": "Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?", - "error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?", + "error.serverSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?", "directory.error.invalidUrl": "Ungültiges Verzeichnis in der URL.", "error.chain.unknown": "Unbekannter Fehler", "error.chain.causedBy": "Verursacht durch:", @@ -862,8 +862,8 @@ export const dict = { "common.key.insert": "Einfg", "common.unknown": "unbekannt", "error.page.circular": "[Zirkulär]", - "error.globalSDK.noServerAvailable": "Kein Server verfügbar", - "error.globalSDK.serverNotAvailable": "Server nicht verfügbar", + "error.serverSDK.noServerAvailable": "Kein Server verfügbar", + "error.serverSDK.serverNotAvailable": "Server nicht verfügbar", "error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden", "error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden", "error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index bc4bf07b1..64258454e 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -482,9 +482,9 @@ export const dict = { "error.dev.rootNotFound": "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", - "error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?", - "error.globalSDK.noServerAvailable": "No server available", - "error.globalSDK.serverNotAvailable": "Server not available", + "error.serverSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?", + "error.serverSDK.noServerAvailable": "No server available", + "error.serverSDK.serverNotAvailable": "Server not available", "error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache", "error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata", "error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 30777c4b3..b71fe03e8 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -462,7 +462,7 @@ export const dict = { "error.dev.rootNotFound": "Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?", - "error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?", + "error.serverSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?", "directory.error.invalidUrl": "URL de directorio inválida.", "error.chain.unknown": "Error desconocido", @@ -933,8 +933,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "desconocido", "error.page.circular": "[Circular]", - "error.globalSDK.noServerAvailable": "Ningún servidor disponible", - "error.globalSDK.serverNotAvailable": "Servidor no disponible", + "error.serverSDK.noServerAvailable": "Ningún servidor disponible", + "error.serverSDK.serverNotAvailable": "Servidor no disponible", "error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente", "error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes", "error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index bde4a8fe2..385dbddde 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -417,7 +417,7 @@ export const dict = { "error.page.version": "Version : {{version}}", "error.dev.rootNotFound": "Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?", - "error.globalSync.connectFailed": + "error.serverSync.connectFailed": "Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?", "directory.error.invalidUrl": "Répertoire invalide dans l'URL.", "error.chain.unknown": "Erreur inconnue", @@ -860,8 +860,8 @@ export const dict = { "common.key.insert": "Inser", "common.unknown": "inconnu", "error.page.circular": "[Circulaire]", - "error.globalSDK.noServerAvailable": "Aucun serveur disponible", - "error.globalSDK.serverNotAvailable": "Serveur non disponible", + "error.serverSDK.noServerAvailable": "Aucun serveur disponible", + "error.serverSDK.serverNotAvailable": "Serveur non disponible", "error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant", "error.childStore.persistedProjectMetadataCreateFailed": "Échec de la création des métadonnées de projet persistantes", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 6dd404169..34ac398bd 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -413,7 +413,7 @@ export const dict = { "error.page.version": "バージョン: {{version}}", "error.dev.rootNotFound": "ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?", - "error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?", + "error.serverSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?", "directory.error.invalidUrl": "URL内のディレクトリが無効です。", "error.chain.unknown": "不明なエラー", "error.chain.causedBy": "原因:", @@ -844,8 +844,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "不明", "error.page.circular": "[循環]", - "error.globalSDK.noServerAvailable": "利用可能なサーバーがありません", - "error.globalSDK.serverNotAvailable": "サーバーが利用できません", + "error.serverSDK.noServerAvailable": "利用可能なサーバーがありません", + "error.serverSDK.serverNotAvailable": "サーバーが利用できません", "error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました", "error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました", "error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 9038af615..f1926f3dd 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -412,7 +412,7 @@ export const dict = { "error.page.version": "버전: {{version}}", "error.dev.rootNotFound": "루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.", - "error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?", + "error.serverSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?", "directory.error.invalidUrl": "URL에 유효하지 않은 디렉터리가 있습니다.", "error.chain.unknown": "알 수 없는 오류", "error.chain.causedBy": "원인:", @@ -839,8 +839,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "알 수 없음", "error.page.circular": "[순환]", - "error.globalSDK.noServerAvailable": "사용 가능한 서버 없음", - "error.globalSDK.serverNotAvailable": "서버를 사용할 수 없음", + "error.serverSDK.noServerAvailable": "사용 가능한 서버 없음", + "error.serverSDK.serverNotAvailable": "서버를 사용할 수 없음", "error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패", "error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패", "error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index f0fa26b58..b4adebdb7 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -463,7 +463,7 @@ export const dict = { "error.dev.rootNotFound": "Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?", - "error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?", + "error.serverSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?", "directory.error.invalidUrl": "Invalid directory in URL.", "error.chain.unknown": "Ukjent feil", @@ -927,8 +927,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "ukjent", "error.page.circular": "[Sirkulær]", - "error.globalSDK.noServerAvailable": "Ingen server tilgjengelig", - "error.globalSDK.serverNotAvailable": "Server ikke tilgjengelig", + "error.serverSDK.noServerAvailable": "Ingen server tilgjengelig", + "error.serverSDK.serverNotAvailable": "Server ikke tilgjengelig", "error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer", "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata", "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index cc7689ef1..02637e574 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -414,7 +414,7 @@ export const dict = { "error.page.version": "Wersja: {{version}}", "error.dev.rootNotFound": "Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?", - "error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?", + "error.serverSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?", "directory.error.invalidUrl": "Nieprawidłowy katalog w URL.", "error.chain.unknown": "Nieznany błąd", "error.chain.causedBy": "Spowodowany przez:", @@ -848,8 +848,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "nieznany", "error.page.circular": "[Cykliczne]", - "error.globalSDK.noServerAvailable": "Brak dostępnego serwera", - "error.globalSDK.serverNotAvailable": "Serwer niedostępny", + "error.serverSDK.noServerAvailable": "Brak dostępnego serwera", + "error.serverSDK.serverNotAvailable": "Serwer niedostępny", "error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej", "error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu", "error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 6f20d73ec..a965acf27 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -461,7 +461,7 @@ export const dict = { "error.dev.rootNotFound": "Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?", - "error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?", + "error.serverSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?", "directory.error.invalidUrl": "Недопустимая директория в URL.", "error.chain.unknown": "Неизвестная ошибка", @@ -929,8 +929,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "неизвестно", "error.page.circular": "[Циклично]", - "error.globalSDK.noServerAvailable": "Нет доступного сервера", - "error.globalSDK.serverNotAvailable": "Сервер недоступен", + "error.serverSDK.noServerAvailable": "Нет доступного сервера", + "error.serverSDK.serverNotAvailable": "Сервер недоступен", "error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш", "error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта", "error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index b978c6ea0..00df86a34 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -459,7 +459,7 @@ export const dict = { "error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?", - "error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?", + "error.serverSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?", "directory.error.invalidUrl": "ไดเรกทอรีใน URL ไม่ถูกต้อง", "error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก", @@ -916,8 +916,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "ไม่ทราบ", "error.page.circular": "[วงกลม]", - "error.globalSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์", - "error.globalSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน", + "error.serverSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์", + "error.serverSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน", "error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร", "error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร", "error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index e0a558455..27a6e03e3 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -465,7 +465,7 @@ export const dict = { "error.dev.rootNotFound": "Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?", - "error.globalSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?", + "error.serverSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?", "directory.error.invalidUrl": "URL'de geçersiz dizin.", "error.chain.unknown": "Bilinmeyen hata", @@ -935,8 +935,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "bilinmiyor", "error.page.circular": "[Döngüsel]", - "error.globalSDK.noServerAvailable": "Sunucu yok", - "error.globalSDK.serverNotAvailable": "Sunucu mevcut değil", + "error.serverSDK.noServerAvailable": "Sunucu yok", + "error.serverSDK.serverNotAvailable": "Sunucu mevcut değil", "error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı", "error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı", "error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index cf886f6a0..82d0635c5 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -483,9 +483,9 @@ export const dict = { "error.dev.rootNotFound": "Кореневий елемент не знайдено. Ви забули додати його до index.html? Або, можливо, атрибут id було написано з помилкою?", - "error.globalSync.connectFailed": "Не вдалося підключитися до сервера. Чи працює сервер за адресою `{{url}}`?", - "error.globalSDK.noServerAvailable": "Сервер недоступний", - "error.globalSDK.serverNotAvailable": "Сервер недоступний", + "error.serverSync.connectFailed": "Не вдалося підключитися до сервера. Чи працює сервер за адресою `{{url}}`?", + "error.serverSDK.noServerAvailable": "Сервер недоступний", + "error.serverSDK.serverNotAvailable": "Сервер недоступний", "error.childStore.persistedCacheCreateFailed": "Не вдалося створити постійний кеш", "error.childStore.persistedProjectMetadataCreateFailed": "Не вдалося створити постійні метадані проєкту", "error.childStore.persistedProjectIconCreateFailed": "Не вдалося створити постійну іконку проєкту", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 6808c0e73..f5c962a99 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -462,7 +462,7 @@ export const dict = { "error.page.report.discord": "在 Discord 上", "error.page.version": "版本:{{version}}", "error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?", - "error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", + "error.serverSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", "directory.error.invalidUrl": "URL 中的目录无效。", @@ -919,8 +919,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "未知", "error.page.circular": "[循环]", - "error.globalSDK.noServerAvailable": "无可用服务器", - "error.globalSDK.serverNotAvailable": "服务器不可用", + "error.serverSDK.noServerAvailable": "无可用服务器", + "error.serverSDK.serverNotAvailable": "服务器不可用", "error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败", "error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败", "error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index a5747bea0..edd6f7bc0 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -457,7 +457,7 @@ export const dict = { "error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html? 或者 id 屬性拼錯了?", - "error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?", + "error.serverSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?", "directory.error.invalidUrl": "URL 中的目錄無效。", "error.chain.unknown": "未知錯誤", @@ -907,8 +907,8 @@ export const dict = { "common.key.insert": "Insert", "common.unknown": "未知", "error.page.circular": "[循環]", - "error.globalSDK.noServerAvailable": "無可用的伺服器", - "error.globalSDK.serverNotAvailable": "伺服器無法使用", + "error.serverSDK.noServerAvailable": "無可用的伺服器", + "error.serverSDK.serverNotAvailable": "伺服器無法使用", "error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗", "error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗", "error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗", diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 00c6cd3ca..e69767fd4 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -19,7 +19,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogSelectServer } from "@/components/dialog-select-server" import { useServer } from "@/context/server" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" @@ -55,7 +55,7 @@ export default function Home() { } function HomeDesign() { - const sync = useGlobalSync() + const sync = useServerSync() const layout = useLayout() const platform = usePlatform() const dialog = useDialog() @@ -358,10 +358,10 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi } function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (session: Session) => void }) { - const globalSync = useGlobalSync() + const serverSync = useServerSync() const notification = useNotification() const permission = usePermission() - const [sessionStore] = globalSync.child(props.record.session.directory, { bootstrap: false }) + const [sessionStore] = serverSync.child(props.record.session.directory, { bootstrap: false }) const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) const unseenCount = createMemo(() => notification.session.unseenCount(props.record.session.id)) const hasError = createMemo(() => notification.session.unseenHasError(props.record.session.id)) @@ -459,7 +459,7 @@ function groupSessions(records: HomeSessionRecord[], language: ReturnType layout.ready()) const platform = usePlatform() @@ -136,7 +136,7 @@ export default function Layout(props: ParentProps) { if (!slug) return { slug, dir: "" } const dir = decode64(slug) if (!dir) return { slug, dir: "" } - const store = globalSync.peek(dir, { bootstrap: false }) + const store = serverSync.peek(dir, { bootstrap: false }) return { slug, store, @@ -226,7 +226,7 @@ export default function Layout(props: ParentProps) { active: () => state.hoverProject, el: () => state.nav?.querySelector("[data-component='sidebar-rail']") ?? state.nav, onActivate: (directory) => { - globalSync.child(directory) + serverSync.child(directory) setState("hoverProject", directory) }, }) @@ -410,7 +410,7 @@ export default function Layout(props: ParentProps) { alertedAtBySession.delete(sessionKey) } - const unsub = globalSDK.event.listen((e) => { + const unsub = serverSDK.event.listen((e) => { if (e.details?.type === "worktree.ready") { setBusy(e.name, false) WorktreeState.ready(e.name) @@ -444,7 +444,7 @@ export default function Layout(props: ParentProps) { const props = e.details.properties if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return - const [store] = globalSync.child(directory, { bootstrap: false }) + const [store] = serverSync.child(directory, { bootstrap: false }) const session = store.session.find((s) => s.id === props.sessionID) const sessionKey = `${directory}:${props.sessionID}` @@ -507,7 +507,7 @@ export default function Layout(props: ParentProps) { if (!currentDir() || !currentSession) return const sessionKey = `${currentDir()}:${currentSession}` dismissSessionAlert(sessionKey) - const [store] = globalSync.child(currentDir(), { bootstrap: false }) + const [store] = serverSync.child(currentDir(), { bootstrap: false }) const childSessions = store.session.filter((s) => s.parentID === currentSession) for (const child of childSessions) { dismissSessionAlert(`${currentDir()}:${child.id}`) @@ -545,11 +545,11 @@ export default function Layout(props: ParentProps) { const direct = projects.find((p) => pathKey(p.worktree) === key) if (direct) return direct - const [child] = globalSync.child(directory, { bootstrap: false }) + const [child] = serverSync.child(directory, { bootstrap: false }) const id = child.project if (!id) return - const meta = globalSync.data.project.find((p) => p.id === id) + const meta = serverSync.data.project.find((p) => p.id === id) const root = meta?.worktree if (!root) return @@ -640,7 +640,7 @@ export default function Layout(props: ParentProps) { const result: Session[] = [] for (const dir of dirs) { - const [dirStore] = globalSync.child(dir, { bootstrap: true }) + const [dirStore] = serverSync.child(dir, { bootstrap: true }) const dirSessions = sortedRootSessions(dirStore, now) result.push(...dirSessions) } @@ -692,7 +692,7 @@ export default function Layout(props: ParentProps) { createEffect(() => { route() - globalSDK.url + serverSDK.url prefetchToken.value += 1 clearSessionPrefetchInflight() @@ -739,13 +739,13 @@ export default function Layout(props: ParentProps) { } async function prefetchMessages(directory: string, sessionID: string, token: number) { - const [store, setStore] = globalSync.child(directory, { bootstrap: false }) + const [store, setStore] = serverSync.child(directory, { bootstrap: false }) return runSessionPrefetch({ directory, sessionID, task: (rev) => - retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk })) + retry(() => serverSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk })) .then((messages) => { if (prefetchToken.value !== token) return if (!isSessionPrefetchCurrent(directory, sessionID, rev)) return @@ -765,7 +765,7 @@ export default function Layout(props: ParentProps) { if (stale.length > 0) { clearSessionPrefetch(directory, stale) for (const id of stale) { - globalSync.todo.set(id, undefined) + serverSync.todo.set(id, undefined) } } @@ -830,7 +830,7 @@ export default function Layout(props: ParentProps) { const directory = session.directory if (!directory) return - const [store] = globalSync.child(directory, { bootstrap: false }) + const [store] = serverSync.child(directory, { bootstrap: false }) const cached = untrack(() => { const info = getSessionPrefetch(directory, session.id) return shouldSkipSessionPrefetch({ @@ -935,7 +935,7 @@ export default function Layout(props: ParentProps) { if (!target) return // warm up child store to prevent flicker - globalSync.child(target.worktree) + serverSync.child(target.worktree) void openProject(target.worktree) } @@ -944,7 +944,7 @@ export default function Layout(props: ParentProps) { const target = projects[index] if (!target) return - globalSync.child(target.worktree) + serverSync.child(target.worktree) void openProject(target.worktree) } @@ -973,12 +973,12 @@ export default function Layout(props: ParentProps) { } async function archiveSession(session: Session) { - const [store, setStore] = globalSync.child(session.directory) + const [store, setStore] = serverSync.child(session.directory) const sessions = store.session ?? [] const index = sessions.findIndex((s) => s.id === session.id) const nextSession = sessions[index + 1] ?? sessions[index - 1] - await globalSDK.client.session.update({ + await serverSDK.client.session.update({ directory: session.directory, sessionID: session.id, time: { archived: Date.now() }, @@ -1246,11 +1246,11 @@ export default function Layout(props: ParentProps) { ) if (known) return known[0] - const [child] = globalSync.child(directory, { bootstrap: false }) + const [child] = serverSync.child(directory, { bootstrap: false }) const id = child.project if (!id) return directory - const meta = globalSync.data.project.find((item) => item.id === id) + const meta = serverSync.data.project.find((item) => item.id === id) return meta?.worktree ?? directory } @@ -1298,7 +1298,7 @@ export default function Layout(props: ParentProps) { } const refreshDirs = async (target?: string) => { if (!target || target === root || canOpen(target)) return canOpen(target) - const listed = await globalSDK.client.worktree + const listed = await serverSDK.client.worktree .list({ directory: root }) .then((x) => x.data ?? []) .catch(() => [] as string[]) @@ -1307,13 +1307,13 @@ export default function Layout(props: ParentProps) { } const openSession = async (target: { directory: string; id: string }) => { if (!canOpen(target.directory)) return false - const [data] = globalSync.child(target.directory, { bootstrap: false }) + const [data] = serverSync.child(target.directory, { bootstrap: false }) if (data.session.some((item) => item.id === target.id)) { setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() }) navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`) return true } - const resolved = await globalSDK.client.session + const resolved = await serverSDK.client.session .get({ sessionID: target.id }) .then((x) => x.data) .catch(() => undefined) @@ -1333,7 +1333,7 @@ export default function Layout(props: ParentProps) { } const latest = latestRootSession( - dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]), + dirs.map((item) => serverSync.child(item, { bootstrap: false })[0]), Date.now(), ) if (latest && (await openSession(latest))) { @@ -1344,7 +1344,7 @@ export default function Layout(props: ParentProps) { await Promise.all( dirs.map(async (item) => ({ path: { directory: item }, - session: await globalSDK.client.session + session: await serverSDK.client.session .list({ directory: item }) .then((x) => x.data ?? []) .catch(() => []), @@ -1405,11 +1405,11 @@ export default function Layout(props: ParentProps) { const name = next === getFilename(project.worktree) ? "" : next if (project.id && project.id !== "global") { - await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name }) + await serverSDK.client.project.update({ projectID: project.id, directory: project.worktree, name }) return } - globalSync.project.meta(project.worktree, { name }) + serverSync.project.meta(project.worktree, { name }) } const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => { @@ -1506,7 +1506,7 @@ export default function Layout(props: ParentProps) { setBusy(directory, true) - const result = await globalSDK.client.worktree + const result = await serverSDK.client.worktree .remove({ directory: root, worktreeRemoveInput: { directory } }) .then((x) => x.data) .catch((err) => { @@ -1525,7 +1525,7 @@ export default function Layout(props: ParentProps) { clearLastProjectSession(root) } - globalSync.set( + serverSync.set( "project", produce((draft) => { const project = draft.find((item) => item.worktree === root) @@ -1564,7 +1564,7 @@ export default function Layout(props: ParentProps) { }) const dismiss = () => toaster.dismiss(progress) - const sessions: Session[] = await globalSDK.client.session + const sessions: Session[] = await serverSDK.client.session .list({ directory }) .then((x) => x.data ?? []) .catch(() => []) @@ -1575,9 +1575,9 @@ export default function Layout(props: ParentProps) { platform, getTerminalServerScope(server.current, server.key), ) - await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) + await serverSDK.client.instance.dispose({ directory }).catch(() => undefined) - const result = await globalSDK.client.worktree + const result = await serverSDK.client.worktree .reset({ directory: root, worktreeResetInput: { directory } }) .then((x) => x.data) .catch((err) => { @@ -1599,7 +1599,7 @@ export default function Layout(props: ParentProps) { sessions .filter((session) => session.time.archived === undefined) .map((session) => - globalSDK.client.session + serverSDK.client.session .update({ sessionID: session.id, directory: session.directory, @@ -1640,7 +1640,7 @@ export default function Layout(props: ParentProps) { }) onMount(() => { - globalSDK.client.file + serverSDK.client.file .status({ directory: props.directory }) .then((x) => { const files = x.data ?? [] @@ -1699,7 +1699,7 @@ export default function Layout(props: ParentProps) { }) const refresh = async () => { - const sessions = await globalSDK.client.session + const sessions = await serverSDK.client.session .list({ directory: props.directory }) .then((x) => x.data ?? []) .catch(() => []) @@ -1708,7 +1708,7 @@ export default function Layout(props: ParentProps) { } onMount(() => { - globalSDK.client.file + serverSDK.client.file .status({ directory: props.directory }) .then((x) => { const files = x.data ?? [] @@ -1842,7 +1842,7 @@ export default function Layout(props: ParentProps) { const next = new Set(dirs) for (const directory of next) { if (loadedSessionDirs.has(directory)) continue - void globalSync.project.loadSessions(directory) + void serverSync.project.loadSessions(directory) } loadedSessionDirs.clear() @@ -1939,7 +1939,7 @@ export default function Layout(props: ParentProps) { const createWorkspace = async (project: LocalProject) => { clearSidebarHoverState() - const created = await globalSDK.client.worktree + const created = await serverSDK.client.worktree .create({ directory: project.worktree }) .then((x) => x.data) .catch((err) => { @@ -1973,7 +1973,7 @@ export default function Layout(props: ParentProps) { return [created.directory, ...next] }) - globalSync.child(created.directory) + serverSync.child(created.directory) navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`) } @@ -2078,7 +2078,7 @@ export default function Layout(props: ParentProps) { if (!item) return false return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)() }) - const homedir = createMemo(() => globalSync.data.path.home) + const homedir = createMemo(() => serverSync.data.path.home) return (
{ - const globalSync = useGlobalSync() + const serverSync = useServerSync() const notification = useNotification() const permission = usePermission() const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])]) @@ -33,7 +33,7 @@ export const ProjectIcon = (props: { const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory))) const hasPermissions = createMemo(() => dirs().some((directory) => { - const [store] = globalSync.child(directory, { bootstrap: false }) + const [store] = serverSync.child(directory, { bootstrap: false }) return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory)) }), ) @@ -146,10 +146,10 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { const language = useLanguage() const notification = useNotification() const permission = usePermission() - const globalSync = useGlobalSync() + const serverSync = useServerSync() const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id)) const hasError = createMemo(() => notification.session.unseenHasError(props.session.id)) - const [sessionStore] = globalSync.child(props.session.directory) + const [sessionStore] = serverSync.child(props.session.directory) const hasPermissions = createMemo(() => { return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => { return !permission.autoResponds(item, props.session.directory) diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index b910dd209..7afaa87ff 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -7,7 +7,7 @@ import { HoverCard } from "@opencode-ai/ui/hover-card" import { Icon } from "@opencode-ai/ui/icon" import { createSortable } from "@thisbeyond/solid-dnd" import { useLayout, type LocalProject } from "@/context/layout" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items" @@ -274,7 +274,7 @@ export const SortableProject = (props: { ctx: ProjectSidebarContext sortNow: Accessor }): JSX.Element => { - const globalSync = useGlobalSync() + const serverSync = useServerSync() const language = useLanguage() const sortable = createSortable(props.project.worktree) const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree) @@ -294,23 +294,23 @@ export const SortableProject = (props: { const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu const label = (directory: string) => { - const [data] = globalSync.child(directory, { bootstrap: false }) + const [data] = serverSync.child(directory, { bootstrap: false }) const kind = directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id) return `${kind} : ${name}` } - const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0]) + const projectStore = createMemo(() => serverSync.child(props.project.worktree, { bootstrap: false })[0]) const isWorking = createMemo(() => dirs().some((directory) => { - const [store] = globalSync.child(directory, { bootstrap: false }) + const [store] = serverSync.child(directory, { bootstrap: false }) return Object.keys(store.session_status).some((id) => store.session_working(id)) }), ) const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow())) const workspaceSessions = (directory: string) => { - const [data] = globalSync.child(directory, { bootstrap: false }) + const [data] = serverSync.child(directory, { bootstrap: false }) return sortedRootSessions(data, props.sortNow()) } const tile = () => ( diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index f423c13d1..a8b6ad8f8 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -14,7 +14,7 @@ import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" import { type Session } from "@opencode-ai/sdk/v2/client" import { type LocalProject } from "@/context/layout" -import { useGlobalSync, useQueryOptions } from "@/context/global-sync" +import { useServerSync, useQueryOptions } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { pathKey } from "@/utils/path-key" import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" @@ -60,7 +60,7 @@ export const WorkspaceDragOverlay = (props: { activeWorkspace: Accessor workspaceLabel: (directory: string, branch?: string, projectId?: string) => string }): JSX.Element => { - const globalSync = useGlobalSync() + const serverSync = useServerSync() const language = useLanguage() const label = createMemo(() => { const project = props.sidebarProject() @@ -68,7 +68,7 @@ export const WorkspaceDragOverlay = (props: { const directory = props.activeWorkspace() if (!directory) return - const [workspaceStore] = globalSync.child(directory, { bootstrap: false }) + const [workspaceStore] = serverSync.child(directory, { bootstrap: false }) const kind = directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id) @@ -299,11 +299,11 @@ export const SortableWorkspace = (props: { }): JSX.Element => { const navigate = useNavigate() const params = useParams() - const globalSync = useGlobalSync() + const serverSync = useServerSync() const queryOptions = useQueryOptions() const language = useLanguage() const sortable = createSortable(props.directory) - const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false }) + const [workspaceStore, setWorkspaceStore] = serverSync.child(props.directory, { bootstrap: false }) const [menu, setMenu] = createStore({ open: false, pendingRename: false, @@ -328,7 +328,7 @@ export const SortableWorkspace = (props: { const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const loadMore = async () => { setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5) - await globalSync.project.loadSessions(props.directory) + await serverSync.project.loadSessions(props.directory) } const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`)) @@ -357,7 +357,7 @@ export const SortableWorkspace = (props: { createEffect(() => { if (!boot()) return - globalSync.child(props.directory, { bootstrap: true }) + serverSync.child(props.directory, { bootstrap: true }) }) return ( @@ -446,11 +446,11 @@ export const LocalWorkspace = (props: { sortNow: Accessor mobile?: boolean }): JSX.Element => { - const globalSync = useGlobalSync() + const serverSync = useServerSync() const queryOptions = useQueryOptions() const language = useLanguage() const workspace = createMemo(() => { - const [store, setStore] = globalSync.child(props.project.worktree) + const [store, setStore] = serverSync.child(props.project.worktree) return { store, setStore } }) const slug = createMemo(() => base64Encode(props.project.worktree)) @@ -461,7 +461,7 @@ export const LocalWorkspace = (props: { const loading = () => fetching() > 0 && count() === 0 const loadMore = async () => { workspace().setStore("limit", (limit) => (limit ?? 0) + 5) - await globalSync.project.loadSessions(props.project.worktree) + await serverSync.project.loadSessions(props.project.worktree) } return ( diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 7ee082465..671a88490 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -34,7 +34,7 @@ import { useLocation, useSearchParams } from "@solidjs/router" import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session" import { useComments } from "@/context/comments" import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { usePrompt } from "@/context/prompt" @@ -182,7 +182,7 @@ function createSessionHistoryLoader(input: SessionHistoryWindowInput) { } export default function Page() { - const globalSync = useGlobalSync() + const serverSync = useServerSync() const layout = useLayout() const local = useLocal() const file = useFile() @@ -560,11 +560,11 @@ export default function Page() { } function upsert(next: Project) { - const list = globalSync.data.project + const list = serverSync.data.project sync.set("project", next.id) const idx = list.findIndex((item) => item.id === next.id) if (idx >= 0) { - globalSync.set( + serverSync.set( "project", list.map((item, i) => (i === idx ? { ...item, ...next } : item)), ) @@ -572,10 +572,10 @@ export default function Page() { } const at = list.findIndex((item) => item.id > next.id) if (at >= 0) { - globalSync.set("project", [...list.slice(0, at), next, ...list.slice(at)]) + serverSync.set("project", [...list.slice(0, at), next, ...list.slice(at)]) return } - globalSync.set("project", [...list, next]) + serverSync.set("project", [...list, next]) } const gitMutation = useMutation(() => ({ @@ -673,7 +673,7 @@ export default function Page() { todoTimer = undefined if (!id) return if (status === "idle" && !blocked) return - const cached = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined) + const cached = untrack(() => sync.data.todo[id] !== undefined || serverSync.data.session_todo[id] !== undefined) todoFrame = requestAnimationFrame(() => { todoFrame = undefined @@ -1386,7 +1386,7 @@ export default function Page() { const ok = await sendFollowupDraft({ client: sdk.client, sync, - globalSync, + serverSync, draft: item, optimisticBusy: item.sessionDirectory === sdk.directory, }).catch((err) => { diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index a7213c4a7..2287fd674 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -3,7 +3,7 @@ import { createStore } from "solid-js/store" import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import { useParams } from "@solidjs/router" import { showToast } from "@opencode-ai/ui/toast" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { usePermission } from "@/context/permission" import { useSDK } from "@/context/sdk" @@ -27,7 +27,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => const params = useParams() const sdk = useSDK() const sync = useSync() - const globalSync = useGlobalSync() + const serverSync = useServerSync() const language = useLanguage() const permission = usePermission() @@ -50,7 +50,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => const todos = createMemo((): Todo[] => { const id = params.id if (!id) return [] - return globalSync.data.session_todo[id] ?? [] + return serverSync.data.session_todo[id] ?? [] }) const done = createMemo( @@ -111,7 +111,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => const clear = () => { const id = params.id if (!id) return - globalSync.todo.set(id, []) + serverSync.todo.set(id, []) sync.set("todo", id, []) } diff --git a/packages/app/src/pages/session/message-timeline.data.ts b/packages/app/src/pages/session/message-timeline.data.ts index c6294d3e3..0643a424e 100644 --- a/packages/app/src/pages/session/message-timeline.data.ts +++ b/packages/app/src/pages/session/message-timeline.data.ts @@ -23,7 +23,6 @@ export type TimelineRowMap = { userMessageID: string group: PartGroup previousAssistantPart: boolean - lastAssistantPart: boolean } Thinking: { userMessageID: string; reasoningHeading?: string } Retry: { userMessageID: string } @@ -50,7 +49,6 @@ export namespace TimelineRow { userMessageID: string group: PartGroup previousAssistantPart: boolean - lastAssistantPart: boolean }> {} export class Thinking extends Data.TaggedClass("Thinking")<{ userMessageID: string @@ -151,8 +149,6 @@ export namespace Timeline { ), ] : groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group })) - const assistantGroupCount = assistantItems.filter((item) => item.type === "part").length - if (comments.length > 0) rows.push( new TimelineRow.CommentStrip({ @@ -195,7 +191,6 @@ export namespace Timeline { userMessageID: userMessage.id, group: item.group, previousAssistantPart: assistantGroupIndex > 0, - lastAssistantPart: assistantGroupIndex === assistantGroupCount - 1, }), ) assistantGroupIndex += 1 diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index a39df7122..c014cb5d9 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -1,4 +1,16 @@ -import { createEffect, createMemo, createSignal, For, Index, on, onCleanup, Show, mapArray, type JSX } from "solid-js" +import { + createEffect, + createMemo, + createSignal, + For, + Index, + on, + onCleanup, + Show, + mapArray, + type Accessor, + type JSX, +} from "solid-js" import { createStore, produce } from "solid-js/store" import { Dynamic } from "solid-js/web" import { useNavigate } from "@solidjs/router" @@ -48,7 +60,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { createResizeObserver } from "@solid-primitives/resize-observer" import { useLanguage } from "@/context/language" import { useSessionKey } from "@/pages/session/session-layout" -import { useGlobalSDK } from "@/context/global-sdk" +import { useServerSDK } from "@/context/server-sdk" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" import { useSDK } from "@/context/sdk" @@ -245,7 +257,7 @@ function TimelineDiffView(props: { diff: SummaryDiff }) { return (
- +
) } @@ -273,7 +285,7 @@ export function MessageTimeline(props: { let touchGesture: number | undefined const navigate = useNavigate() - const globalSDK = useGlobalSDK() + const serverSDK = useServerSDK() const sdk = useSDK() const sync = useSync() const settings = useSettings() @@ -415,8 +427,7 @@ export function MessageTimeline(props: { if (rows.length === 0) return rows return reuseTimelineRows(previous, [...rows, new TimelineRow.BottomSpacer()]) }) - const timelineRowByKey = createMemo(() => new Map(timelineRows().map((row) => [TimelineRow.key(row), row] as const))) - const timelineRowKeys = createMemo(() => [...timelineRowByKey().keys()], [] as string[], { equals: sameKeys }) + const timelineRowKeys = createMemo(() => timelineRows().map(TimelineRow.key), [] as string[], { equals: sameKeys }) const virtualCache = createMemo(() => readTimelineCache(sessionKey(), timelineRowKeys())) const messageRowIndex = createMemo(() => { const result = new Map() @@ -427,6 +438,14 @@ export function MessageTimeline(props: { }) return result }) + const lastAssistantGroupKey = createMemo(() => { + const result = new Map() + timelineRows().forEach((row) => { + if (row._tag !== "AssistantPart") return + result.set(row.userMessageID, row.group.key) + }) + return result + }) const keepMounted = createMemo(() => { const id = activeMessageID() if (!id) return @@ -540,6 +559,7 @@ export function MessageTimeline(props: { const [bar, setBar] = createStore({ ms: pace(640), }) + const [toolOpen, setToolOpen] = createStore>({}) let more: HTMLButtonElement | undefined let head: HTMLDivElement | undefined @@ -560,6 +580,11 @@ export function MessageTimeline(props: { const isMeasuredBottom = (root: HTMLDivElement) => root.scrollHeight - root.clientHeight - root.scrollTop <= 4 + const measureTimeline = () => { + virtualizer?.measure() + anchorMeasuredBottom() + } + function anchorMeasuredBottom() { if (!listRoot) return false if (!measuredBottomAnchored) return false @@ -704,14 +729,14 @@ export function MessageTimeline(props: { } const shareMutation = useMutation(() => ({ - mutationFn: (id: string) => globalSDK.client.session.share({ sessionID: id, directory: sdk.directory }), + mutationFn: (id: string) => serverSDK.client.session.share({ sessionID: id, directory: sdk.directory }), onError: (err) => { console.error("Failed to share session", err) }, })) const unshareMutation = useMutation(() => ({ - mutationFn: (id: string) => globalSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }), + mutationFn: (id: string) => serverSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }), onError: (err) => { console.error("Failed to unshare session", err) }, @@ -980,29 +1005,42 @@ export function MessageTimeline(props: { const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID) - const renderAssistantPartGroup = (row: TimelineRowMap["AssistantPart"]) => { - if (row.group.type === "context") { + const renderAssistantPartGroup = (row: Accessor) => { + if (row().group.type === "context") { const parts = createMemo(() => { - const group = row.group + const group = row().group if (group.type !== "context") return emptyTools return group.refs .map((ref) => getMsgPart(ref.messageID, ref.partID)) .filter((part): part is ToolPart => part?.type === "tool") }) - return + return ( + + ) } const message = createMemo(() => { - const group = row.group + const group = row().group if (group.type !== "part") return return messageByID().get(group.ref.messageID) }) const part = createMemo(() => { - const group = row.group + const group = row().group if (group.type !== "part") return return getMsgPart(group.ref.messageID, group.ref.partID) }) + const defaultOpen = createMemo(() => { + const item = part() + if (!item) return + return partDefaultOpen(item, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded()) + }) return ( @@ -1012,14 +1050,13 @@ export function MessageTimeline(props: { setToolOpen(part().id, open)} deferToolContent={false} + virtualizeDiff={false} /> )} @@ -1028,25 +1065,25 @@ export function MessageTimeline(props: { ) } - function TimelineRowFrame(input: { row: FramedTimelineRow; children: JSX.Element }) { + function TimelineRowFrame(input: { row: Accessor; children: JSX.Element }) { const anchor = () => { - const row = input.row + const row = input.row() return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor) } const previousUserMessage = () => { - const row = input.row + const row = input.row() return (row._tag === "CommentStrip" || row._tag === "UserMessage") && row.previousUserMessage } const previousAssistantPart = () => { - const row = input.row + const row = input.row() return row._tag === "AssistantPart" && row.previousAssistantPart } return (
{ - switch (row._tag) { + const renderTimelineRow = (row: Accessor) => { + switch (row()._tag) { case "CommentStrip": { + const commentStripRow = row as Accessor> const comments = createMemo(() => - getMsgParts(row.userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []), + getMsgParts(commentStripRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []), ) return ( - +
@@ -1102,17 +1140,22 @@ export function MessageTimeline(props: { ) } case "UserMessage": { + const userMessageRow = row as Accessor> const message = createMemo(() => { - const m = messageByID().get(row.userMessageID) + const m = messageByID().get(userMessageRow().userMessageID) if (m?.role === "user") return m }) return ( - + {(message) => (
- +
)} @@ -1121,13 +1164,14 @@ export function MessageTimeline(props: { ) } case "TurnDivider": { + const turnDividerRow = row as Accessor> return ( - +
@@ -1136,22 +1180,27 @@ export function MessageTimeline(props: { ) } case "AssistantPart": { + const assistantPartRow = row as Accessor> return ( - +
-
- {renderAssistantPartGroup(row)} +
+ {renderAssistantPartGroup(assistantPartRow)}
) } case "Thinking": { + const thinkingRow = row as Accessor> return ( - +
@@ -1159,29 +1208,32 @@ export function MessageTimeline(props: { ) } case "Retry": { + const retryRow = row as Accessor> return ( - +
- +
) } case "DiffSummary": { + const diffSummaryRow = row as Accessor> return ( - +
- +
) } case "Error": { + const errorRow = row as Accessor> return ( - +
- {row.text} + {errorRow().text}
@@ -1192,12 +1244,8 @@ export function MessageTimeline(props: { } } - function TimelineRowView(props: { rowKey: string }) { - return ( - - {(item) => renderTimelineRow(item)} - - ) + function TimelineRowView(props: { row: TimelineRow.TimelineRow }) { + return renderTimelineRow(() => props.row) } return ( @@ -1529,7 +1577,7 @@ export function MessageTimeline(props: { {(root) => ( - {(key) => } + {(row) => } )} diff --git a/packages/app/src/utils/refcount.ts b/packages/app/src/utils/refcount.ts new file mode 100644 index 000000000..6877d82f2 --- /dev/null +++ b/packages/app/src/utils/refcount.ts @@ -0,0 +1,26 @@ +import { onCleanup } from "solid-js" + +export function createRefCountMap(create: (key: string) => T) { + const items = new Map() + const refCounts = new Map() + + return (key: string) => { + onCleanup(() => { + refCounts.set(key, (refCounts.get(key) ?? 0) - 1) + if (refCounts.get(key) === 0) { + items.delete(key) + refCounts.delete(key) + } + }) + + const cached = items.get(key) + if (cached) { + refCounts.set(key, (refCounts.get(key) ?? 0) + 1) + return cached + } + const item = create(key) + items.set(key, item) + refCounts.set(key, 1) + return item + } +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index e87035568..0566e07b3 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -509,7 +509,11 @@ export function Prompt(props: PromptProps) { const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text") const value = text - const content = await Editor.open({ value, renderer }) + const content = await Editor.open({ + value, + renderer, + cwd: project.instance.path().worktree || project.instance.directory() || process.cwd(), + }) if (!content) return input.setText(content) diff --git a/packages/opencode/src/cli/cmd/tui/context/thinking.ts b/packages/opencode/src/cli/cmd/tui/context/thinking.ts index 55e995df1..bb1c2a692 100644 --- a/packages/opencode/src/cli/cmd/tui/context/thinking.ts +++ b/packages/opencode/src/cli/cmd/tui/context/thinking.ts @@ -6,13 +6,14 @@ export type ThinkingMode = "show" | "hide" const MODES: readonly ThinkingMode[] = ["show", "hide"] as const // OpenAI's Responses API surfaces reasoning summaries that start with a bolded -// title line: "**Inspecting PR workflow**\n\n". GitHub Copilot routes -// through the same shape, and the opencode provider relays it too. Pull the -// title out for a nicer label; return null for providers that don't follow -// this convention so the caller can fall back to a generic "Thinking" string. -export function reasoningTitle(text: string): string | null { - const match = text.trimStart().match(/^\*\*([^*\n]+)\*\*/) - return match ? match[1].trim() : null +// title block: "**Inspecting PR workflow**\n\n". Treat that first block, +// or a complete title still awaiting its body while streaming, as disclosure +// metadata so the TUI can style its header independently from the markdown body. +export function reasoningSummary(text: string) { + const content = text.trim() + const match = content.match(/^\*\*([^*\n]+)\*\*(?:\r?\n\r?\n|$)/) + if (!match) return { title: null, body: content } + return { title: match[1].trim(), body: content.slice(match[0].length).trimEnd() } } export function isThinkingMode(value: unknown): value is ThinkingMode { diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx index 465b490fc..8b9b08880 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx @@ -5,9 +5,9 @@ import { SplitBorder } from "@tui/component/border" import { Spinner } from "@tui/component/spinner" import { useTheme } from "@tui/context/theme" import { useLocal } from "@tui/context/local" -import { reasoningTitle, useThinkingMode } from "@tui/context/thinking" +import { reasoningSummary, useThinkingMode } from "@tui/context/thinking" import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" -import { TextAttributes, type BoxRenderable, type SyntaxStyle } from "@opentui/core" +import { RGBA, TextAttributes, type BoxRenderable, type SyntaxStyle } from "@opentui/core" import { useBindings } from "../../keymap" import { Locale } from "@/util/locale" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" @@ -398,7 +398,7 @@ function AssistantReasoning(props: { // v2 reasoning parts have no per-part `time.end` (see SessionMessageAssistantReasoning // in the v2 SDK); we settle on parent-message completion instead. const isDone = createMemo(() => props.completedAt() !== undefined) - const title = createMemo(() => reasoningTitle(content())) + const summary = createMemo(() => reasoningSummary(content())) const toggle = () => { if (!inMinimal()) return @@ -407,41 +407,50 @@ function AssistantReasoning(props: { return ( - - - + + + + + + - - - - - - - - - {title() ? "Thinking: " + title() : "Thinking"} - - - + + ) } -function CollapsedReasoningText(props: { title: string | null }) { +function ReasoningHeader(props: { toggleable: boolean; open: boolean; done: boolean; title: string | null }) { const { theme } = useTheme() + const fg = () => + props.open + ? RGBA.fromValues(theme.warning.r, theme.warning.g, theme.warning.b, theme.thinkingOpacity) + : theme.warning return ( - - {props.title ? "+ Thought: " + props.title : "+ Thought"} + + + {props.open ? "- " : "+ "} + + {props.done ? "Thought" : "Thinking"} + + : + {props.title} + ) } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index ae23d58bc..270c11049 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -81,7 +81,7 @@ import * as Model from "../../util/model" import { formatTranscript } from "../../util/transcript" import { UI } from "@/cli/ui.ts" import { useTuiConfig } from "../../context/tui-config" -import { nextThinkingMode, reasoningTitle, useThinkingMode, type ThinkingMode } from "../../context/thinking" +import { nextThinkingMode, reasoningSummary, useThinkingMode, type ThinkingMode } from "../../context/thinking" import { getScrollAcceleration } from "../../util/scroll" import { collapseToolOutput } from "../../util/collapse-tool-output" import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime" @@ -965,7 +965,11 @@ export function Session() { if (options.openWithoutSaving) { // Just open in editor without saving - await Editor.open({ value: transcript, renderer }) + await Editor.open({ + value: transcript, + renderer, + cwd: project.instance.path().worktree || project.instance.directory() || process.cwd(), + }) } else { const exportDir = process.cwd() const filename = options.filename.trim() @@ -974,7 +978,11 @@ export function Session() { await Filesystem.write(filepath, transcript) // Open with EDITOR if available - const result = await Editor.open({ value: transcript, renderer }) + const result = await Editor.open({ + value: transcript, + renderer, + cwd: project.instance.path().worktree || project.instance.directory() || process.cwd(), + }) if (result !== undefined) { await Filesystem.write(filepath, result) } @@ -1515,12 +1523,8 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass const end = props.part.time.end return end === undefined ? 0 : Math.max(0, end - props.part.time.start) }) - // OpenAI / Copilot / opencode-via-OpenAI emit `**Title**\n\n` summary - // blocks. Surface the title both while streaming and after settling so the - // collapsed line carries real signal, not just a duration. - const title = createMemo(() => reasoningTitle(content())) - // Keep markdown emphasis for the existing thinking color/concealment, but render it without italics. - const syntax = createMemo(() => generateSubtleSyntax(theme, { "markup.italic": { italic: false } })) + const summary = createMemo(() => reasoningSummary(content())) + const syntax = createMemo(() => generateSubtleSyntax(theme)) const toggle = () => { if (!inMinimal()) return @@ -1529,47 +1533,65 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass return ( - - - {/* Full markdown block: `show` mode, or `hide` after the user opens it. */} - + + + + + + - - - - - - - - - {title() ? "Thinking: " + title() : "Thinking"} - - - + + ) } -function CollapsedReasoningText(props: { title: string | null; duration: number }) { +function ReasoningHeader(props: { + toggleable: boolean + open: boolean + done: boolean + title: string | null + duration?: string +}) { const { theme } = useTheme() - const duration = () => Locale.duration(props.duration) + const fg = () => + props.open + ? RGBA.fromValues(theme.warning.r, theme.warning.g, theme.warning.b, theme.thinkingOpacity) + : theme.warning return ( - - - {props.title ? "+ Thought: " + props.title + " · " + duration() : "+ Thought: " + duration()} - + + + {props.open ? "- " : "+ "} + + {props.done ? "Thought" : "Thinking"} + + : + + + {props.title} + + + + {props.title ? " · " : ""} + {props.duration} + + ) } diff --git a/packages/opencode/src/cli/cmd/tui/util/editor.ts b/packages/opencode/src/cli/cmd/tui/util/editor.ts index 45a9ffee9..03d91cf6d 100644 --- a/packages/opencode/src/cli/cmd/tui/util/editor.ts +++ b/packages/opencode/src/cli/cmd/tui/util/editor.ts @@ -6,7 +6,7 @@ import { CliRenderer } from "@opentui/core" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" -export async function open(opts: { value: string; renderer: CliRenderer }): Promise { +export async function open(opts: { value: string; renderer: CliRenderer; cwd?: string }): Promise { const editor = process.env["VISUAL"] || process.env["EDITOR"] if (!editor) return @@ -19,6 +19,7 @@ export async function open(opts: { value: string; renderer: CliRenderer }): Prom try { const parts = editor.split(" ") const proc = Process.spawn([...parts, filepath], { + cwd: opts.cwd, stdin: "inherit", stdout: "inherit", stderr: "inherit", diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 88d8b342b..6967d374b 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -48,7 +48,7 @@ export class Service extends ConfigService.Service()("@opencode/Runtime experimentalIconDiscovery: enabledByExperimental("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"), outputTokenMax: positiveInteger("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"), bashDefaultTimeoutMs: positiveInteger("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"), - experimentalNativeLlm: enabledByExperimental("OPENCODE_EXPERIMENTAL_NATIVE_LLM"), + experimentalNativeLlm: bool("OPENCODE_EXPERIMENTAL_NATIVE_LLM"), client: Config.string("OPENCODE_CLIENT").pipe(Config.withDefault("cli")), }) {} diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index e88657082..454d32143 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -1,6 +1,5 @@ -import { and } from "drizzle-orm" +import { and, eq, sql } from "drizzle-orm" import { Database } from "@/storage/db" -import { eq } from "drizzle-orm" import { ProjectTable } from "@opencode-ai/core/project/sql" import { PermissionTable, SessionTable } from "@opencode-ai/core/session/sql" import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql" @@ -219,7 +218,10 @@ export const layer = Layer.effect( d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run() } - d.update(SessionTable).set({ project_id: newID }).where(eq(SessionTable.project_id, oldID)).run() + d.update(SessionTable) + .set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` }) + .where(eq(SessionTable.project_id, oldID)) + .run() d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run() if (oldProject) d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run() diff --git a/packages/opencode/test/cli/tui/thinking.test.ts b/packages/opencode/test/cli/tui/thinking.test.ts new file mode 100644 index 000000000..7f8af1a5f --- /dev/null +++ b/packages/opencode/test/cli/tui/thinking.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "bun:test" +import { reasoningSummary } from "../../../src/cli/cmd/tui/context/thinking" + +describe("reasoningSummary", () => { + test("extracts a leading summary title and leaves markdown body", () => { + expect(reasoningSummary("**Continuing Quality Review**\n\nDetails.\n\n**Next section**\n\nMore.")).toEqual({ + title: "Continuing Quality Review", + body: "Details.\n\n**Next section**\n\nMore.", + }) + }) + + test("extracts a completed title before its streamed body arrives", () => { + expect(reasoningSummary("**Continuing Quality Review**")).toEqual({ + title: "Continuing Quality Review", + body: "", + }) + }) + + test("preserves markdown-significant indentation in the extracted body", () => { + expect(reasoningSummary("**Continuing Quality Review**\n\n const value = true\n")).toEqual({ + title: "Continuing Quality Review", + body: " const value = true", + }) + }) + + test("does not consume ordinary leading bold content", () => { + expect(reasoningSummary("**Important:** keep this in the body.")).toEqual({ + title: null, + body: "**Important:** keep this in the body.", + }) + }) + + test("leaves content without a leading title in its body", () => { + expect(reasoningSummary("Details only.")).toEqual({ title: null, body: "Details only." }) + }) +}) diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index bf7c7ae9c..e7515b37c 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -58,7 +58,7 @@ describe("RuntimeFlags", () => { expect(flags.experimentalEventSystem).toBe(true) expect(flags.experimentalWorkspaces).toBe(true) expect(flags.experimentalIconDiscovery).toBe(true) - expect(flags.experimentalNativeLlm).toBe(true) + expect(flags.experimentalNativeLlm).toBe(false) expect(flags.client).toBe("desktop") }), ) @@ -77,13 +77,13 @@ describe("RuntimeFlags", () => { }), ) - it.effect("enables native LLM via dedicated or umbrella flag", () => + it.effect("enables native LLM via dedicated flag only", () => Effect.gen(function* () { const explicit = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_NATIVE_LLM: "true" }))) const umbrella = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" }))) expect(explicit.experimentalNativeLlm).toBe(true) - expect(umbrella.experimentalNativeLlm).toBe(true) + expect(umbrella.experimentalNativeLlm).toBe(false) }), ) diff --git a/packages/storybook/.storybook/mocks/app/context/global-sync.ts b/packages/storybook/.storybook/mocks/app/context/global-sync.ts index 92622c04a..c538168be 100644 --- a/packages/storybook/.storybook/mocks/app/context/global-sync.ts +++ b/packages/storybook/.storybook/mocks/app/context/global-sync.ts @@ -24,7 +24,7 @@ const [store, setStore] = createStore({ config: { permission: {} }, }) -export function useGlobalSync() { +export function useServerSync() { return { data: { provider, diff --git a/packages/ui/src/components/apply-patch-file.test.ts b/packages/ui/src/components/apply-patch-file.test.ts index 6c5858156..5176eb998 100644 --- a/packages/ui/src/components/apply-patch-file.test.ts +++ b/packages/ui/src/components/apply-patch-file.test.ts @@ -36,7 +36,6 @@ describe("apply patch file", () => { ])[0] expect(file).toBeDefined() - expect(file?.view.patch).toContain("@@ -1,1 +1,1 @@") expect(text(file!.view, "deletions")).toBe("one\n") expect(text(file!.view, "additions")).toBe("two\n") }) diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index cb9f9b17f..213a48e11 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -29,6 +29,8 @@ export interface BasicToolProps { status?: string hideDetails?: boolean defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void forceOpen?: boolean defer?: boolean locked?: boolean @@ -83,7 +85,7 @@ export function BasicTool(props: BasicToolProps) { open: props.defaultOpen ?? false, ready: !props.defer && (props.defaultOpen ?? false), }) - const open = () => state.open + const open = () => props.open ?? state.open const ready = () => state.ready const pending = () => props.status === "pending" || props.status === "running" const hasChildren = () => (props.defer ? "children" in props : props.children) @@ -110,8 +112,15 @@ export function BasicTool(props: BasicToolProps) { if (props.defer && open()) scheduleReady(true) }) + const setOpen = (value: boolean) => { + if (props.open === undefined) setState("open", value) + props.onOpenChange?.(value) + } + createEffect(() => { - if (props.forceOpen) setState("open", true) + if (!props.forceOpen) return + if (open()) return + setOpen(true) }) createEffect( @@ -166,7 +175,7 @@ export function BasicTool(props: BasicToolProps) { const handleOpenChange = (value: boolean) => { if (pending()) return if (props.locked && !value) return - setState("open", value) + setOpen(value) } const trigger = () => ( diff --git a/packages/ui/src/components/file.tsx b/packages/ui/src/components/file.tsx index 97d4d69f7..bfcc05b40 100644 --- a/packages/ui/src/components/file.tsx +++ b/packages/ui/src/components/file.tsx @@ -1,5 +1,7 @@ import { sampledChecksum } from "@opencode-ai/core/util/encode" import { + areFilesEqual, + areOptionsEqual, DEFAULT_VIRTUAL_FILE_METRICS, type DiffLineAnnotation, type FileContents, @@ -88,6 +90,7 @@ type DiffBaseProps = FileDiffOptions & mode: "diff" annotations?: DiffLineAnnotation[] preloadedDiff?: DiffPreload + virtualize?: boolean } type DiffPairProps = DiffBaseProps & { @@ -123,7 +126,7 @@ const sharedKeys = [ ] as const const textKeys = ["file", ...sharedKeys] as const -const diffKeys = ["fileDiff", "before", "after", ...sharedKeys] as const +const diffKeys = ["fileDiff", "before", "after", "virtualize", ...sharedKeys] as const // --------------------------------------------------------------------------- // Shared viewer hook @@ -482,17 +485,24 @@ function notifyRendered(opts: { function renderViewer(opts: { viewer: Viewer current: I | undefined + reset?: boolean create: () => I + update?: (value: I) => void assign: (value: I) => void draw: (value: I) => void onReady: () => void }) { clearReadyWatcher(opts.viewer.ready) - opts.current?.cleanUp() - const next = opts.create() - opts.assign(next) + const reset = opts.reset === true && opts.current !== undefined + if (reset) opts.current?.cleanUp() + const next = reset || !opts.current ? opts.create() : opts.current + if (reset || !opts.current) { + opts.viewer.container.innerHTML = "" + opts.assign(next) + } else { + opts.update?.(next) + } - opts.viewer.container.innerHTML = "" opts.draw(next) applyViewerScheme(opts.viewer.getHost()) @@ -566,7 +576,7 @@ function createLocalVirtualStrategy(host: () => HTMLDivElement | undefined, enab } } -function createSharedVirtualStrategy(host: () => HTMLDivElement | undefined): VirtualStrategy { +function createSharedVirtualStrategy(host: () => HTMLDivElement | undefined, enabled: () => boolean): VirtualStrategy { let shared: NonNullable> | undefined const release = () => { @@ -576,6 +586,10 @@ function createSharedVirtualStrategy(host: () => HTMLDivElement | undefined): Vi return { get: () => { + if (!enabled()) { + release() + return + } if (shared) return shared.virtualizer const container = host() @@ -858,15 +872,14 @@ function TextViewer(props: TextFileProps) { createEffect(() => { const opts = options() const workerPool = getWorkerPool("unified") - const isVirtual = virtual() - const virtualizer = virtuals.get() renderViewer({ viewer, current: instance, + reset: instance !== undefined, create: () => - isVirtual && virtualizer + virtualizer ? new VirtualizedFile(opts, virtualizer, codeMetrics, workerPool) : new PierreFile(opts, workerPool), assign: (value) => { @@ -907,6 +920,12 @@ function TextViewer(props: TextFileProps) { function DiffViewer(props: DiffFileProps) { let instance: FileDiff | undefined + let instanceVirtualizer: Virtualizer | undefined + let instanceWorkerPool: ReturnType + let instanceVirtualHunkSeparators: FileDiffOptions["hunkSeparators"] | undefined + let instanceFileDiff: FileDiffMetadata | undefined + let instanceBefore: FileContents | undefined + let instanceAfter: FileContents | undefined let dragSide: DiffSelectionSide | undefined let dragEndSide: DiffSelectionSide | undefined let viewer!: Viewer @@ -991,7 +1010,10 @@ function DiffViewer(props: DiffFileProps) { adapter, ) - const virtuals = createSharedVirtualStrategy(() => viewer.container) + const virtuals = createSharedVirtualStrategy( + () => viewer.container, + () => local.virtualize !== false, + ) const large = createMemo(() => { if (local.fileDiff) { @@ -1067,31 +1089,66 @@ function DiffViewer(props: DiffFileProps) { return sampledChecksum(contents) } + const before = local.before + ? { ...local.before, contents: beforeContents, cacheKey: cacheKey(beforeContents) } + : undefined + const after = local.after + ? { ...local.after, contents: afterContents, cacheKey: cacheKey(afterContents) } + : undefined + const targetChanged = + local.fileDiff !== undefined + ? instanceFileDiff !== local.fileDiff + : instanceFileDiff !== undefined || + before === undefined || + after === undefined || + instanceBefore === undefined || + instanceAfter === undefined || + !areFilesEqual(instanceBefore, before) || + !areFilesEqual(instanceAfter, after) + // Pierre beta virtualized instances retain their first diff target and resolve separator metrics at construction. + // Plain timeline diffs can retain the instance as content streams; virtualized viewers reset only when that is unsafe. + const reset = + instance !== undefined && + (instanceVirtualizer !== virtualizer || + instanceWorkerPool !== workerPool || + (virtualizer !== undefined && (instanceVirtualHunkSeparators !== opts.hunkSeparators || targetChanged))) + const forceRender = !reset && instance !== undefined && !areOptionsEqual(instance.options, opts) + renderViewer({ viewer, current: instance, + reset, create: () => virtualizer ? new VirtualizedFileDiff(opts, virtualizer, virtualMetrics, workerPool) : new FileDiff(opts, workerPool), + update: (value) => value.setOptions(opts), assign: (value) => { instance = value + instanceVirtualizer = virtualizer + instanceWorkerPool = workerPool + instanceVirtualHunkSeparators = virtualizer ? opts.hunkSeparators : undefined + instanceFileDiff = local.fileDiff + instanceBefore = before + instanceAfter = after }, draw: (value) => { if (local.fileDiff) { value.render({ fileDiff: local.fileDiff, + forceRender, lineAnnotations: [], containerWrapper: viewer.container, }) return } - if (!local.before || !local.after) return + if (!before || !after) return value.render({ - oldFile: { ...local.before, contents: beforeContents, cacheKey: cacheKey(beforeContents) }, - newFile: { ...local.after, contents: afterContents, cacheKey: cacheKey(afterContents) }, + oldFile: before, + newFile: after, + forceRender, lineAnnotations: [], containerWrapper: viewer.container, }) @@ -1111,6 +1168,12 @@ function DiffViewer(props: DiffFileProps) { onCleanup(() => { instance?.cleanUp() instance = undefined + instanceVirtualizer = undefined + instanceWorkerPool = undefined + instanceVirtualHunkSeparators = undefined + instanceFileDiff = undefined + instanceBefore = undefined + instanceAfter = undefined virtuals.cleanup() dragSide = undefined dragEndSide = undefined diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index f309ca5f6..35912e119 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -175,7 +175,10 @@ export interface MessagePartProps { message: MessageType hideDetails?: boolean defaultOpen?: boolean + toolOpen?: boolean + onToolOpenChange?: (open: boolean) => void deferToolContent?: boolean + virtualizeDiff?: boolean showAssistantCopyPartID?: string | null turnDurationMs?: number } @@ -290,7 +293,7 @@ function getDirectory(path: string | undefined) { } import type { IconProps } from "./icon" -import { normalize } from "./session-diff" +import { normalize, resolveFileDiff } from "./session-diff" export type ToolInfo = { icon: IconProps["name"] @@ -930,7 +933,7 @@ export function AssistantMessageDisplay(props: { ) } -export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) { +export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean; onSizeChange?: () => void }) { const i18n = useI18n() const [open, setOpen] = createSignal(false) const pending = createMemo( @@ -938,11 +941,15 @@ export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) { !!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"), ) const summary = createMemo(() => contextToolSummary(props.parts)) + const handleOpenChange = (value: boolean) => { + setOpen(value) + props.onSizeChange?.() + } return ( part.id).join(",")} @@ -1261,7 +1268,10 @@ export function Part(props: MessagePartProps) { message={props.message} hideDetails={props.hideDetails} defaultOpen={props.defaultOpen} + toolOpen={props.toolOpen} + onToolOpenChange={props.onToolOpenChange} deferToolContent={props.deferToolContent} + virtualizeDiff={props.virtualizeDiff} showAssistantCopyPartID={props.showAssistantCopyPartID} turnDurationMs={props.turnDurationMs} /> @@ -1278,7 +1288,10 @@ export interface ToolProps { status?: string hideDetails?: boolean defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void deferContent?: boolean + virtualizeDiff?: boolean forceOpen?: boolean locked?: boolean } @@ -1376,6 +1389,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { }) const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool) + const controlledOpen = () => (props.onToolOpenChange ? (props.toolOpen ?? props.defaultOpen) : undefined) + const handleToolOpenChange = (open: boolean) => props.onToolOpenChange?.(open) return ( @@ -1399,6 +1414,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { error={error()} title={part().tool === "websearch" ? webSearchProviderLabel(partMetadata().provider) : undefined} defaultOpen={props.defaultOpen} + open={controlledOpen()} + onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined} subtitle={taskSubtitle()} href={taskHref()} /> @@ -1417,7 +1434,10 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { status={part().state.status} hideDetails={props.hideDetails} defaultOpen={props.defaultOpen} + open={controlledOpen()} + onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined} deferContent={props.deferToolContent} + virtualizeDiff={props.virtualizeDiff} /> @@ -1921,15 +1941,29 @@ ToolRegistry.register({ const path = createMemo(() => props.metadata?.filediff?.file || props.input.filePath || "") const filename = () => getFilename(props.input.filePath ?? "") const pending = () => props.status === "pending" || props.status === "running" + const diffSource = createMemo( + () => { + const filediff = props.metadata?.filediff + if (!filediff) return + return { + file: filediff.file || props.input.filePath || "", + patch: typeof filediff.patch === "string" ? filediff.patch : undefined, + before: typeof filediff.before === "string" ? filediff.before : undefined, + after: typeof filediff.after === "string" ? filediff.after : undefined, + } + }, + undefined, + { + equals: (a, b) => + a?.file === b?.file && a?.patch === b?.patch && a?.before === b?.before && a?.after === b?.after, + }, + ) const fileCompProps = createMemo(() => { try { - if (props.metadata?.filediff) { - const diff = normalize({ - ...props.metadata?.filediff, - status: "modified", - }) - const fileDiff = diff.fileDiff + const source = diffSource() + if (source) { + const fileDiff = resolveFileDiff(source) if (fileDiff) return { fileDiff, hunkSeparators: fileDiff.isPartial ? "simple" : "line-info-basic" } } } catch {} @@ -1987,7 +2021,7 @@ ToolRegistry.register({ } >
- +
@@ -2171,6 +2205,7 @@ ToolRegistry.register({ @@ -2243,7 +2278,12 @@ ToolRegistry.register({ } >
- +
diff --git a/packages/ui/src/components/session-diff.test.ts b/packages/ui/src/components/session-diff.test.ts index 172fe8d6c..ba8fd395e 100644 --- a/packages/ui/src/components/session-diff.test.ts +++ b/packages/ui/src/components/session-diff.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { normalize, text } from "./session-diff" +import { normalize, resolveFileDiff, text } from "./session-diff" describe("session diff", () => { test("keeps unified patch content", () => { @@ -13,8 +13,8 @@ describe("session diff", () => { } const view = normalize(diff) - expect(view.patch).toBe(diff.patch) expect(view.fileDiff.name).toBe("a.ts") + expect(view.fileDiff.isPartial).toBe(true) expect(text(view, "deletions")).toBe("one\ntwo\n") expect(text(view, "additions")).toBe("one\nthree\n") }) @@ -34,7 +34,53 @@ describe("session diff", () => { expect(text(view, "additions")).toBe("one\nthree") }) - test("converts legacy content into a patch", () => { + test("keeps separated patch hunks partial without complete file contents", () => { + const fileDiff = resolveFileDiff({ + file: "project.ts", + patch: + 'Index: project.ts\n===================================================================\n--- project.ts\t\n+++ project.ts\t\n@@ -1,3 +1,2 @@\n import { and } from "drizzle-orm"\n-import { sql } from "drizzle-orm"\n import { ProjectTable } from "./project.sql"\n@@ -346,3 +345,3 @@\n import { Database } from "@/storage/db"\n-import { ProjectTable } from "./project.sql"\n+import { ProjectTable } from "../project/project.sql"\n import { SessionTable } from "../session/session.sql"\n', + }) + + expect(fileDiff.isPartial).toBe(true) + expect(fileDiff.hunks).toHaveLength(2) + expect(fileDiff.hunks[1]?.collapsedBefore).toBeGreaterThan(0) + }) + + test("renders headerless persisted patches", () => { + const view = normalize({ + file: "a.ts", + patch: "@@ -1 +1 @@\n-old\n+new\n", + additions: 1, + deletions: 1, + status: "modified" as const, + }) + + expect(view.fileDiff.name).toBe("a.ts") + expect(view.fileDiff.isPartial).toBe(true) + expect(text(view, "deletions")).toBe("old\n") + expect(text(view, "additions")).toBe("new\n") + }) + + test("does not share headerless patch metadata between files", () => { + const patch = "@@ -1 +1 @@\n-old\n+new\n" + + expect(resolveFileDiff({ file: "a.ts", patch }).name).toBe("a.ts") + expect(resolveFileDiff({ file: "b.ts", patch }).name).toBe("b.ts") + }) + + test("keeps capped header-only patches partial", () => { + const fileDiff = resolveFileDiff({ + file: "a.ts", + patch: + "Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n", + }) + + expect(fileDiff.name).toBe("a.ts") + expect(fileDiff.isPartial).toBe(true) + expect(fileDiff.hunks).toEqual([]) + }) + + test("keeps full legacy content as a complete diff", () => { const diff = { file: "a.ts", before: "one\n", @@ -45,7 +91,7 @@ describe("session diff", () => { } const view = normalize(diff) - expect(view.patch).toContain("@@ -1,1 +1,1 @@") + expect(view.fileDiff.isPartial).toBe(false) expect(text(view, "deletions")).toBe("one\n") expect(text(view, "additions")).toBe("two\n") }) @@ -61,7 +107,6 @@ describe("session diff", () => { } const view = normalize(diff) - expect(view.patch).toBe(diff.patch) expect(text(view, "deletions")).toBe("") expect(text(view, "additions")).toBe("") }) diff --git a/packages/ui/src/components/session-diff.ts b/packages/ui/src/components/session-diff.ts index 52ef0a4bb..01f0e2b9f 100644 --- a/packages/ui/src/components/session-diff.ts +++ b/packages/ui/src/components/session-diff.ts @@ -1,5 +1,5 @@ import { parseDiffFromFile, parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs" -import { formatPatch, parsePatch, structuredPatch } from "diff" +import { parsePatch } from "diff" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" type LegacyDiff = { @@ -14,107 +14,35 @@ type LegacyDiff = { type SnapshotDiff = SnapshotFileDiff & { file: string } type ReviewDiff = SnapshotDiff | VcsFileDiff | LegacyDiff +export type DiffSource = Pick export type ViewDiff = { file: string - patch: string additions: number deletions: number status?: "added" | "deleted" | "modified" fileDiff: FileDiffMetadata } -const cache = new Map() +const diffCacheLimit = 16 +const patchFileDiffCache = new Map() -function patch(diff: ReviewDiff) { - if (typeof diff.patch === "string") { - try { - const [patch] = parsePatch(diff.patch) - const beforeLines: Array<{ text: string; newline: boolean }> = [] - const afterLines: Array<{ text: string; newline: boolean }> = [] - let previous: "-" | "+" | " " | undefined - - const patchIsPartial = patch.hunks.every((h) => h.oldStart > 1) - - for (const hunk of patch.hunks) { - for (const line of hunk.lines) { - if (line.startsWith("\\")) { - if (previous === "-" || previous === " ") { - const before = beforeLines.at(-1) - if (before) before.newline = false - } - if (previous === "+" || previous === " ") { - const after = afterLines.at(-1) - if (after) after.newline = false - } - continue - } - - if (line.startsWith("-")) { - beforeLines.push({ text: line.slice(1), newline: true }) - previous = "-" - } else if (line.startsWith("+")) { - afterLines.push({ text: line.slice(1), newline: true }) - previous = "+" - } else { - // context line (starts with ' ') - beforeLines.push({ text: line.slice(1), newline: true }) - afterLines.push({ text: line.slice(1), newline: true }) - previous = " " - } - } - } - - return { - before: beforeLines.map((line) => line.text + (line.newline ? "\n" : "")).join(""), - after: afterLines.map((line) => line.text + (line.newline ? "\n" : "")).join(""), - patch: diff.patch, - patchIsPartial, - } - } catch { - return { before: "", after: "", patch: diff.patch, patchIsPartial: false } - } - } - return { - before: "before" in diff && typeof diff.before === "string" ? diff.before : "", - after: "after" in diff && typeof diff.after === "string" ? diff.after : "", - patch: formatPatch( - structuredPatch( - diff.file, - diff.file, - "before" in diff && typeof diff.before === "string" ? diff.before : "", - "after" in diff && typeof diff.after === "string" ? diff.after : "", - "", - "", - { context: Number.MAX_SAFE_INTEGER }, - ), - ), - patchIsPartial: false, - } -} - -function file(file: string, patch: string, before: string, after: string, partial = false) { - const hit = cache.get(patch) - if (hit) return hit - - let value: FileDiffMetadata | undefined - if (partial) value = parsePatchFiles(patch)[0]?.files[0] - if (value === undefined) value = parseDiffFromFile({ name: file, contents: before }, { name: file, contents: after }) - - cache.set(patch, value) - return value +export function resolveFileDiff(diff: DiffSource) { + if (typeof diff.patch === "string") return fileDiffFromPatch(diff.file, diff.patch) + return fileDiffFromContent( + diff.file, + typeof diff.before === "string" ? diff.before : "", + typeof diff.after === "string" ? diff.after : "", + ) } export function normalize(diff: ReviewDiff): ViewDiff { - const next = patch(diff) - const fileDiff = file(diff.file, next.patch, next.before, next.after, next.patchIsPartial) return { file: diff.file, - patch: next.patch, additions: diff.additions, deletions: diff.deletions, status: diff.status, - fileDiff, + fileDiff: resolveFileDiff(diff), } } @@ -122,3 +50,40 @@ export function text(diff: ViewDiff, side: "deletions" | "additions") { if (side === "deletions") return diff.fileDiff.deletionLines.join("") return diff.fileDiff.additionLines.join("") } + +function fileDiffFromPatch(file: string, patch: string) { + const key = `${file}\0${patch}` + const hit = patchFileDiffCache.get(key) + if (hit) { + patchFileDiffCache.delete(key) + patchFileDiffCache.set(key, hit) + return hit + } + + const input = patchInput(file, patch) + const value = (input ? parsePatchFiles(input)[0]?.files[0] : undefined) ?? emptyFileDiff(file) + patchFileDiffCache.set(key, value) + while (patchFileDiffCache.size > diffCacheLimit) patchFileDiffCache.delete(patchFileDiffCache.keys().next().value!) + return value +} + +function patchInput(file: string, patch: string) { + try { + const parsed = parsePatch(patch)[0] + if (!parsed) return + if (parsed.index || parsed.oldFileName || parsed.newFileName) return patch + if (!parsed.hunks.length) return + return `Index: ${file}\n===================================================================\n--- ${file}\t\n+++ ${file}\t\n${patch}` + } catch { + return + } +} + +function fileDiffFromContent(file: string, before: string, after: string) { + if (!before && !after) return emptyFileDiff(file) + return parseDiffFromFile({ name: file, contents: before }, { name: file, contents: after }) +} + +function emptyFileDiff(file: string) { + return parseDiffFromFile({ name: file, contents: "" }, { name: file, contents: "" }) +} diff --git a/packages/ui/src/components/todo-panel-motion.stories.tsx b/packages/ui/src/components/todo-panel-motion.stories.tsx index 798fe080f..4bf8fe74b 100644 --- a/packages/ui/src/components/todo-panel-motion.stories.tsx +++ b/packages/ui/src/components/todo-panel-motion.stories.tsx @@ -2,7 +2,7 @@ import { createEffect, createMemo, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import type { Todo } from "@opencode-ai/sdk/v2" -import { useGlobalSync } from "@/context/global-sync" +import { useServerSync } from "@/context/global-sync" import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer" export default { @@ -129,7 +129,7 @@ const css = ` export const Playground = { render: () => { - const global = useGlobalSync() + const global = useServerSync() const [cfg, setCfg] = createStore({ open: true, step: 1, diff --git a/packages/ui/src/components/tool-error-card.tsx b/packages/ui/src/components/tool-error-card.tsx index c85e7d55c..ade7cc4b0 100644 --- a/packages/ui/src/components/tool-error-card.tsx +++ b/packages/ui/src/components/tool-error-card.tsx @@ -12,6 +12,8 @@ export interface ToolErrorCardProps extends Omit, "c error: string title?: string defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void subtitle?: string href?: string } @@ -22,9 +24,22 @@ export function ToolErrorCard(props: ToolErrorCardProps) { open: props.defaultOpen ?? false, copied: false, }) - const open = () => state.open + const open = () => props.open ?? state.open const copied = () => state.copied - const [split, rest] = splitProps(props, ["tool", "error", "title", "defaultOpen", "subtitle", "href"]) + const [split, rest] = splitProps(props, [ + "tool", + "error", + "title", + "defaultOpen", + "open", + "onOpenChange", + "subtitle", + "href", + ]) + const setOpen = (value: boolean) => { + if (props.open === undefined) setState("open", value) + props.onOpenChange?.(value) + } const name = createMemo(() => { if (split.title) return split.title const map: Record = { @@ -77,12 +92,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) { return ( - setState("open", value)} - > +
diff --git a/packages/web/src/content/docs/ar/cli.mdx b/packages/web/src/content/docs/ar/cli.mdx index 6fd419b69..5aa9b781f 100644 --- a/packages/web/src/content/docs/ar/cli.mdx +++ b/packages/web/src/content/docs/ar/cli.mdx @@ -594,17 +594,23 @@ opencode upgrade v0.1.48 تقوم متغيرات البيئة التالية بتفعيل ميزات تجريبية قد تتغير أو تتم إزالتها. -| المتغير | النوع | الوصف | -| ----------------------------------------------- | ------- | ------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | تفعيل جميع الميزات التجريبية | -| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | تفعيل اكتشاف الأيقونات | -| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | تعطيل النسخ عند التحديد في TUI | -| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | المهلة الافتراضية لأوامر bash بالميلي ثانية | -| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | الحد الأقصى لرموز المخرجات لاستجابات LLM | -| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | تفعيل مراقب الملفات لكامل الدليل | -| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | تفعيل مُنسّق oxfmt | -| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | تفعيل أداة LSP تجريبية | -| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | تعطيل مراقب الملفات | -| `OPENCODE_EXPERIMENTAL_EXA` | boolean | تفعيل ميزات Exa التجريبية | -| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | تمكين TY LSP لملفات python | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | تفعيل وضع الخطة | +| المتغير | النوع | الوصف | +| ----------------------------------------------- | ------- | -------------------------------------------- | +| `OPENCODE_EXPERIMENTAL` | boolean | تفعيل الميزات التجريبية الخاضعة للعلم الشامل | +| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | تفعيل اكتشاف الأيقونات | +| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | تعطيل النسخ عند التحديد في TUI | +| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | المهلة الافتراضية لأوامر bash بالميلي ثانية | +| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | الحد الأقصى لرموز المخرجات لاستجابات LLM | +| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | تفعيل مراقب الملفات لكامل الدليل | +| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | تفعيل مُنسّق oxfmt | +| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | تفعيل أداة LSP تجريبية | +| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | تعطيل مراقب الملفات | +| `OPENCODE_EXPERIMENTAL_EXA` | boolean | تفعيل ميزات Exa التجريبية | +| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | تمكين TY LSP لملفات python | +| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | تفعيل وضع الخطة | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | تفعيل مهام subagent في الخلفية | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | تفعيل نظام الأحداث التجريبي | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | تفعيل مسار طلبات LLM الأصلي | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | تفعيل تنفيذ بحث الويب بالتوازي | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | تفعيل Scout subagent | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | تفعيل دعم مساحات العمل | diff --git a/packages/web/src/content/docs/bs/cli.mdx b/packages/web/src/content/docs/bs/cli.mdx index b40e9865f..8883e6889 100644 --- a/packages/web/src/content/docs/bs/cli.mdx +++ b/packages/web/src/content/docs/bs/cli.mdx @@ -592,17 +592,23 @@ OpenCode se može konfigurirati pomoću varijabli okruženja. Ove varijable okruženja omogućavaju eksperimentalne karakteristike koje se mogu promijeniti ili ukloniti. -| Varijabla | Tip | Opis | -| ----------------------------------------------- | ------- | ------------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | Omogući sve eksperimentalne funkcije | -| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona | -| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI | -| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Zadano vremensko ograničenje za bash naredbe u ms | -| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksimalni izlazni tokeni za LLM odgovore | -| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij | -| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter | -| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat | -| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka | -| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije | -| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući TY LSP za python datoteke | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući Plan mod | +| Varijabla | Tip | Opis | +| ----------------------------------------------- | ------- | ------------------------------------------------------- | +| `OPENCODE_EXPERIMENTAL` | boolean | Omogući eksperimentalne funkcije pod zbirnom zastavicom | +| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona | +| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI | +| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Zadano vremensko ograničenje za bash naredbe u ms | +| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksimalni izlazni tokeni za LLM odgovore | +| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij | +| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter | +| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat | +| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka | +| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije | +| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući TY LSP za python datoteke | +| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući Plan mod | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Omogući pozadinske zadatke subagenata | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Omogući eksperimentalni sistem događaja | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Omogući nativnu putanju LLM zahtjeva | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Omogući paralelno izvršavanje web pretrage | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Omogući Scout subagenta | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Omogući podršku za radne prostore | diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 980ac7d05..0f9e5e3b1 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -712,7 +712,7 @@ These environment variables enable experimental features that may change or be r | Variable | Type | Description | | ----------------------------------------------- | ------- | --------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | Enable all experimental features | +| `OPENCODE_EXPERIMENTAL` | boolean | Enable the experimental umbrella flag | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Enable icon discovery | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disable copy on select in TUI | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Default timeout for bash commands in ms | @@ -724,3 +724,9 @@ These environment variables enable experimental features that may change or be r | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Enable experimental Exa features | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Enable TY LSP for python files | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Enable plan mode | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Enable background subagent tasks | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Enable experimental event system | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Enable native LLM request path | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Enable parallel web search execution | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Enable Scout subagent | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Enable workspace support | diff --git a/packages/web/src/content/docs/da/cli.mdx b/packages/web/src/content/docs/da/cli.mdx index d903be55b..814d2b819 100644 --- a/packages/web/src/content/docs/da/cli.mdx +++ b/packages/web/src/content/docs/da/cli.mdx @@ -595,17 +595,23 @@ OpenCode kan konfigureres ved hjælp af miljøvariabler. Disse miljøvariabler muliggør eksperimentelle funktioner, der kan ændres eller fjernes. -| Variabel | Skriv | Beskrivelse | -| ----------------------------------------------- | ------- | ----------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | Aktiver alle eksperimentelle funktioner | -| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Aktiver ikonopdagelse | -| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Deaktiver kopi ved valg i TUI | -| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | nummer | Standard timeout for bash-kommandoer i ms | -| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | nummer | Maks. output-tokens for LLM-svar | -| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Aktiver filovervågning for hele dir | -| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Aktiver oxfmt formatter | -| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Aktive eksperimenter LSP værktøj | -| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Deaktiver filovervågning | -| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Aktive eksperimenter Exa-funktioner | -| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Aktiver TY LSP for python-filer | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Aktiver plantilstand | +| Variabel | Skriv | Beskrivelse | +| ----------------------------------------------- | ------- | ---------------------------------------------------- | +| `OPENCODE_EXPERIMENTAL` | boolean | Aktiver eksperimentelle funktioner under samleflaget | +| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Aktiver ikonopdagelse | +| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Deaktiver kopi ved valg i TUI | +| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | nummer | Standard timeout for bash-kommandoer i ms | +| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | nummer | Maks. output-tokens for LLM-svar | +| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Aktiver filovervågning for hele dir | +| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Aktiver oxfmt formatter | +| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Aktive eksperimenter LSP værktøj | +| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Deaktiver filovervågning | +| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Aktive eksperimenter Exa-funktioner | +| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Aktiver TY LSP for python-filer | +| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Aktiver plantilstand | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Aktiver baggrundsopgaver for subagenter | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Aktiver eksperimentelt hændelsessystem | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Aktiver native LLM-anmodningssti | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Aktiver parallel udførelse af websøgning | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Aktiver Scout-subagent | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Aktiver workspace-understøttelse | diff --git a/packages/web/src/content/docs/de/cli.mdx b/packages/web/src/content/docs/de/cli.mdx index 1151e5146..2c100e7c7 100644 --- a/packages/web/src/content/docs/de/cli.mdx +++ b/packages/web/src/content/docs/de/cli.mdx @@ -594,17 +594,23 @@ OpenCode kann mithilfe von Umgebungsvariablen konfiguriert werden. Diese Umgebungsvariablen ermöglichen experimentelle Funktionen, die sich ändern oder entfernt werden können. -| Variable | Typ | Beschreibung | -| ----------------------------------------------- | --------------- | ------------------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolescher Wert | Alle experimentellen Funktionen aktivieren | -| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolescher Wert | Symbolerkennung aktivieren | -| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolescher Wert | Kopieren bei Auswahl in TUI deaktivieren | -| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Zahl | Standard-Timeout für Bash-Befehle in ms | -| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Zahl | Maximale Ausgabetokens für LLM-Antworten | -| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolescher Wert | Dateiüberwachung für das gesamte Verzeichnis aktivieren | -| `OPENCODE_EXPERIMENTAL_OXFMT` | boolescher Wert | Oxfmt-Formatierer aktivieren | -| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolescher Wert | Experimentelles LSP-Tool aktivieren | -| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolescher Wert | Dateiüberwachung deaktivieren | -| `OPENCODE_EXPERIMENTAL_EXA` | boolescher Wert | Experimentelle Exa-Funktionen aktivieren | -| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolescher Wert | TY LSP für Python-Dateien aktivieren | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolescher Wert | Planmodus aktivieren | +| Variable | Typ | Beschreibung | +| ----------------------------------------------- | --------------- | --------------------------------------------------------- | +| `OPENCODE_EXPERIMENTAL` | boolescher Wert | Experimentelle Funktionen unter dem Sammelflag aktivieren | +| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolescher Wert | Symbolerkennung aktivieren | +| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolescher Wert | Kopieren bei Auswahl in TUI deaktivieren | +| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Zahl | Standard-Timeout für Bash-Befehle in ms | +| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Zahl | Maximale Ausgabetokens für LLM-Antworten | +| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolescher Wert | Dateiüberwachung für das gesamte Verzeichnis aktivieren | +| `OPENCODE_EXPERIMENTAL_OXFMT` | boolescher Wert | Oxfmt-Formatierer aktivieren | +| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolescher Wert | Experimentelles LSP-Tool aktivieren | +| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolescher Wert | Dateiüberwachung deaktivieren | +| `OPENCODE_EXPERIMENTAL_EXA` | boolescher Wert | Experimentelle Exa-Funktionen aktivieren | +| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolescher Wert | TY LSP für Python-Dateien aktivieren | +| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolescher Wert | Planmodus aktivieren | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolescher Wert | Hintergrundaufgaben für Subagenten aktivieren | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolescher Wert | Experimentelles Ereignissystem aktivieren | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolescher Wert | Nativen LLM-Anfragepfad aktivieren | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolescher Wert | Parallele Websuche aktivieren | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolescher Wert | Scout-Subagent aktivieren | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolescher Wert | Workspace-Unterstützung aktivieren | diff --git a/packages/web/src/content/docs/es/cli.mdx b/packages/web/src/content/docs/es/cli.mdx index cff38c175..b925385cf 100644 --- a/packages/web/src/content/docs/es/cli.mdx +++ b/packages/web/src/content/docs/es/cli.mdx @@ -596,7 +596,7 @@ Estas variables de entorno habilitan funciones experimentales que pueden cambiar | Variable | Type | Description | | ----------------------------------------------- | -------- | ---------------------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | booleano | Habilitar todas las funciones experimentales | +| `OPENCODE_EXPERIMENTAL` | booleano | Habilitar funciones experimentales de la bandera global | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | booleano | Habilitar descubrimiento de íconos | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | booleano | Deshabilitar copia al seleccionar en TUI | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | número | Tiempo de espera predeterminado para comandos bash en ms | @@ -608,3 +608,9 @@ Estas variables de entorno habilitan funciones experimentales que pueden cambiar | `OPENCODE_EXPERIMENTAL_EXA` | booleano | Habilitar funciones experimentales de Exa | | `OPENCODE_EXPERIMENTAL_LSP_TY` | booleano | Habilitar Habilitar TY LSP para archivos python | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | booleano | Habilitar modo de plan | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | booleano | Habilitar tareas de subagentes en segundo plano | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | booleano | Habilitar sistema de eventos experimental | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | booleano | Habilitar ruta nativa de solicitud LLM | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | booleano | Habilitar ejecución paralela de búsqueda web | +| `OPENCODE_EXPERIMENTAL_SCOUT` | booleano | Habilitar subagente Scout | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | booleano | Habilitar soporte de espacios de trabajo | diff --git a/packages/web/src/content/docs/fr/cli.mdx b/packages/web/src/content/docs/fr/cli.mdx index d5fb6112e..bc8b550cc 100644 --- a/packages/web/src/content/docs/fr/cli.mdx +++ b/packages/web/src/content/docs/fr/cli.mdx @@ -597,7 +597,7 @@ Ces variables d'environnement activent des fonctionnalités expérimentales qui | Variables | Type | Description | | ----------------------------------------------- | ------- | --------------------------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | booléen | Activer toutes les fonctionnalités expérimentales | +| `OPENCODE_EXPERIMENTAL` | booléen | Activer les fonctionnalités expérimentales du drapeau global | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | booléen | Activer la découverte d'icônes | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | booléen | Désactiver la copie lors de la sélection dans TUI | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | numéro | Délai d'expiration par défaut pour les commandes bash en ms | @@ -609,3 +609,9 @@ Ces variables d'environnement activent des fonctionnalités expérimentales qui | `OPENCODE_EXPERIMENTAL_EXA` | booléen | Activer les fonctionnalités Exa expérimentales | | `OPENCODE_EXPERIMENTAL_LSP_TY` | booléen | Activer TY LSP pour les fichiers python | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | booléen | Activer le mode plan | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | booléen | Activer les tâches de sous-agents en arrière-plan | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | booléen | Activer le système d'événements expérimental | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | booléen | Activer le chemin de requête LLM natif | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | booléen | Activer l'exécution parallèle de la recherche web | +| `OPENCODE_EXPERIMENTAL_SCOUT` | booléen | Activer le sous-agent Scout | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | booléen | Activer la prise en charge des espaces de travail | diff --git a/packages/web/src/content/docs/it/cli.mdx b/packages/web/src/content/docs/it/cli.mdx index a07a41925..67cd703a9 100644 --- a/packages/web/src/content/docs/it/cli.mdx +++ b/packages/web/src/content/docs/it/cli.mdx @@ -595,17 +595,23 @@ OpenCode può essere configurato tramite variabili d'ambiente. Queste variabili d'ambiente abilitano funzionalità sperimentali che potrebbero cambiare o essere rimosse. -| Variabile | Tipo | Descrizione | -| ----------------------------------------------- | ------- | ------------------------------------------ | -| `OPENCODE_EXPERIMENTAL` | boolean | Abilita tutte le funzionalità sperimentali | -| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Abilita icon discovery | -| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disabilita copy on select nella TUI | -| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Timeout di default per comandi bash in ms | -| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Massimo token di output per risposte LLM | -| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Abilita file watcher per l'intera dir | -| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Abilita formatter oxfmt | -| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Abilita strumento LSP sperimentale | -| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Disabilita file watcher | -| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Abilita funzionalità Exa sperimentali | -| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Abilita TY LSP per i file python | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Abilita plan mode | +| Variabile | Tipo | Descrizione | +| ----------------------------------------------- | ------- | ------------------------------------------------------- | +| `OPENCODE_EXPERIMENTAL` | boolean | Abilita funzionalità sperimentali sotto il flag globale | +| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Abilita icon discovery | +| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disabilita copy on select nella TUI | +| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Timeout di default per comandi bash in ms | +| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Massimo token di output per risposte LLM | +| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Abilita file watcher per l'intera dir | +| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Abilita formatter oxfmt | +| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Abilita strumento LSP sperimentale | +| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Disabilita file watcher | +| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Abilita funzionalità Exa sperimentali | +| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Abilita TY LSP per i file python | +| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Abilita plan mode | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Abilita task subagent in background | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Abilita sistema eventi sperimentale | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Abilita percorso nativo di richiesta LLM | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Abilita esecuzione parallela della ricerca web | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Abilita subagent Scout | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Abilita supporto workspace | diff --git a/packages/web/src/content/docs/ja/cli.mdx b/packages/web/src/content/docs/ja/cli.mdx index 715aebb02..755b856fc 100644 --- a/packages/web/src/content/docs/ja/cli.mdx +++ b/packages/web/src/content/docs/ja/cli.mdx @@ -596,7 +596,7 @@ OpenCode は環境変数を使用して構成できます。 | 変数 | タイプ | 説明 | | ----------------------------------------------- | -------- | ------------------------------------------------ | -| `OPENCODE_EXPERIMENTAL` | ブール値 | すべての実験的機能を有効にする | +| `OPENCODE_EXPERIMENTAL` | ブール値 | アンブレラフラグ配下の実験的機能を有効にする | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | ブール値 | アイコン検出を有効にする | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | ブール値 | TUI で選択時のコピーを無効にする | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | 数値 | bash コマンドのデフォルトのタイムアウト (ミリ秒) | @@ -608,3 +608,9 @@ OpenCode は環境変数を使用して構成できます。 | `OPENCODE_EXPERIMENTAL_EXA` | ブール値 | 実験的な Exa 機能を有効にする | | `OPENCODE_EXPERIMENTAL_LSP_TY` | ブール値 | python ファイルの TY LSP を有効にする | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | ブール値 | プランモードを有効にする | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | ブール値 | バックグラウンド subagent タスクを有効にする | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | ブール値 | 実験的なイベントシステムを有効にする | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | ブール値 | ネイティブ LLM リクエスト経路を有効にする | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | ブール値 | 並列 Web 検索実行を有効にする | +| `OPENCODE_EXPERIMENTAL_SCOUT` | ブール値 | Scout subagent を有効にする | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | ブール値 | ワークスペースサポートを有効にする | diff --git a/packages/web/src/content/docs/ko/cli.mdx b/packages/web/src/content/docs/ko/cli.mdx index 0ee6aa576..e6ee7e1f0 100644 --- a/packages/web/src/content/docs/ko/cli.mdx +++ b/packages/web/src/content/docs/ko/cli.mdx @@ -594,17 +594,23 @@ OpenCode는 환경 변수로도 구성할 수 있습니다. 아래 환경 변수는 변경되거나 제거될 수 있는 실험 기능을 활성화합니다. -| 변수 | 타입 | 설명 | -| ----------------------------------------------- | ------- | -------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | 모든 실험 기능 활성화 | -| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 아이콘 탐색 활성화 | -| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI에서 선택 시 복사 비활성화 | -| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 명령 기본 타임아웃(ms) | -| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | LLM 응답 최대 출력 토큰 수 | -| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | 전체 디렉터리 파일 감시 활성화 | -| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | oxfmt 포매터 활성화 | -| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | 실험적 LSP 도구 활성화 | -| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | 파일 감시 비활성화 | -| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 실험적 Exa 기능 활성화 | -| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python 파일에 대해 TY LSP 활성화 | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan mode 활성화 | +| 변수 | 타입 | 설명 | +| ----------------------------------------------- | ------- | ------------------------------------------- | +| `OPENCODE_EXPERIMENTAL` | boolean | 엄브렐라 플래그로 제어되는 실험 기능 활성화 | +| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 아이콘 탐색 활성화 | +| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI에서 선택 시 복사 비활성화 | +| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 명령 기본 타임아웃(ms) | +| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | LLM 응답 최대 출력 토큰 수 | +| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | 전체 디렉터리 파일 감시 활성화 | +| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | oxfmt 포매터 활성화 | +| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | 실험적 LSP 도구 활성화 | +| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | 파일 감시 비활성화 | +| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 실험적 Exa 기능 활성화 | +| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python 파일에 대해 TY LSP 활성화 | +| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan mode 활성화 | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | 백그라운드 subagent 작업 활성화 | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | 실험적 이벤트 시스템 활성화 | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | 네이티브 LLM 요청 경로 활성화 | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | 병렬 웹 검색 실행 활성화 | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Scout subagent 활성화 | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | workspace 지원 활성화 | diff --git a/packages/web/src/content/docs/nb/cli.mdx b/packages/web/src/content/docs/nb/cli.mdx index 982f53906..36e9485e9 100644 --- a/packages/web/src/content/docs/nb/cli.mdx +++ b/packages/web/src/content/docs/nb/cli.mdx @@ -595,17 +595,23 @@ OpenCode kan konfigureres ved hjelp av miljøvariabler. Disse miljøvariablene muliggjør eksperimentelle funksjoner som kan endres eller fjernes. -| Variabel | Type | Beskrivelse | -| ----------------------------------------------- | ------ | --------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolsk | Aktiver alle eksperimentelle funksjoner | -| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolsk | Aktiver ikonoppdagelse | -| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolsk | Deaktiver kopi ved valg i TUI | -| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | tall | Standard tidsavbrudd for bash-kommandoer i ms | -| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | tall | Maks Output Tokens for LLM-svar | -| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolsk | Aktiver filovervåker for hele dir | -| `OPENCODE_EXPERIMENTAL_OXFMT` | boolsk | Aktiver oxfmt formatter | -| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolsk | Aktiver eksperimentelt LSP-verktøy | -| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolsk | Deaktiver filovervåking | -| `OPENCODE_EXPERIMENTAL_EXA` | boolsk | Aktiver eksperimentelle Exa-funksjoner | -| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolsk | Aktiver TY LSP for python-filer | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolsk | Aktiver planmodus | +| Variabel | Type | Beskrivelse | +| ----------------------------------------------- | ------ | ----------------------------------------------------- | +| `OPENCODE_EXPERIMENTAL` | boolsk | Aktiver eksperimentelle funksjoner under samleflagget | +| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolsk | Aktiver ikonoppdagelse | +| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolsk | Deaktiver kopi ved valg i TUI | +| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | tall | Standard tidsavbrudd for bash-kommandoer i ms | +| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | tall | Maks Output Tokens for LLM-svar | +| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolsk | Aktiver filovervåker for hele dir | +| `OPENCODE_EXPERIMENTAL_OXFMT` | boolsk | Aktiver oxfmt formatter | +| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolsk | Aktiver eksperimentelt LSP-verktøy | +| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolsk | Deaktiver filovervåking | +| `OPENCODE_EXPERIMENTAL_EXA` | boolsk | Aktiver eksperimentelle Exa-funksjoner | +| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolsk | Aktiver TY LSP for python-filer | +| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolsk | Aktiver planmodus | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolsk | Aktiver bakgrunnsoppgaver for subagenter | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolsk | Aktiver eksperimentelt hendelsessystem | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolsk | Aktiver innebygd LLM-forespørselsvei | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolsk | Aktiver parallell kjøring av websøk | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolsk | Aktiver Scout-subagent | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolsk | Aktiver arbeidsområde-støtte | diff --git a/packages/web/src/content/docs/pl/cli.mdx b/packages/web/src/content/docs/pl/cli.mdx index 90e861168..eee6d3e16 100644 --- a/packages/web/src/content/docs/pl/cli.mdx +++ b/packages/web/src/content/docs/pl/cli.mdx @@ -595,17 +595,23 @@ OpenCode można skonfigurować za pomocą zmiennych środowiskowych. Te zmienne włączają funkcje eksperymentalne, które mogą ulec zmianie lub zostać usunięte. -| Zmienna | Typ | Opis | -| ----------------------------------------------- | ------- | -------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | Włącz wszystkie funkcje eksperymentalne | -| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Włącz wykrywanie ikon | -| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Wyłącz kopiowanie przy zaznaczaniu w TUI | -| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Domyślny limit czasu dla narzędzia bash w ms | -| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksymalne tokeny wyjściowe dla LLM | -| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Włącz obserwatora plików dla całego katalogu | -| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Włącz formater oxfmt | -| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Włącz eksperymentalne narzędzie LSP | -| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Wyłącz obserwatora plików | -| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Włącz funkcje eksperymentalne Exa | -| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Włącz TY LSP dla plików python | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Włącz tryb planowania | +| Zmienna | Typ | Opis | +| ----------------------------------------------- | ------- | --------------------------------------------------- | +| `OPENCODE_EXPERIMENTAL` | boolean | Włącz funkcje eksperymentalne objęte flagą zbiorczą | +| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Włącz wykrywanie ikon | +| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Wyłącz kopiowanie przy zaznaczaniu w TUI | +| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Domyślny limit czasu dla narzędzia bash w ms | +| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksymalne tokeny wyjściowe dla LLM | +| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Włącz obserwatora plików dla całego katalogu | +| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Włącz formater oxfmt | +| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Włącz eksperymentalne narzędzie LSP | +| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Wyłącz obserwatora plików | +| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Włącz funkcje eksperymentalne Exa | +| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Włącz TY LSP dla plików python | +| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Włącz tryb planowania | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Włącz zadania subagentów w tle | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Włącz eksperymentalny system zdarzeń | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Włącz natywną ścieżkę żądań LLM | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Włącz równoległe wykonywanie wyszukiwania web | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Włącz subagenta Scout | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Włącz obsługę obszarów roboczych | diff --git a/packages/web/src/content/docs/pt-br/cli.mdx b/packages/web/src/content/docs/pt-br/cli.mdx index a238aec9e..69b69e1f2 100644 --- a/packages/web/src/content/docs/pt-br/cli.mdx +++ b/packages/web/src/content/docs/pt-br/cli.mdx @@ -596,7 +596,7 @@ Essas variáveis de ambiente habilitam recursos experimentais que podem mudar ou | Variável | Tipo | Descrição | | ----------------------------------------------- | ------- | --------------------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | Habilitar todos os recursos experimentais | +| `OPENCODE_EXPERIMENTAL` | boolean | Habilitar recursos experimentais sob o sinalizador global | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Habilitar descoberta de ícones | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Desabilitar cópia ao selecionar no TUI | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Tempo limite padrão para comandos bash em ms | @@ -608,3 +608,9 @@ Essas variáveis de ambiente habilitam recursos experimentais que podem mudar ou | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Habilitar recursos experimentais do Exa | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Habilitar TY LSP para arquivos python | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Habilitar modo de plano | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Habilitar tarefas de subagentes em segundo plano | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Habilitar sistema de eventos experimental | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Habilitar caminho nativo de requisição LLM | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Habilitar execução paralela de busca web | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Habilitar subagente Scout | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Habilitar suporte a espaços de trabalho | diff --git a/packages/web/src/content/docs/ru/cli.mdx b/packages/web/src/content/docs/ru/cli.mdx index be6c2cd02..ac48039aa 100644 --- a/packages/web/src/content/docs/ru/cli.mdx +++ b/packages/web/src/content/docs/ru/cli.mdx @@ -597,7 +597,7 @@ opencode можно настроить с помощью переменных с | Переменная | Тип | Описание | | ----------------------------------------------- | ------------------- | ------------------------------------------------------ | -| `OPENCODE_EXPERIMENTAL` | логическое значение | Включить все экспериментальные функции | +| `OPENCODE_EXPERIMENTAL` | логическое значение | Включить экспериментальные функции под общим флагом | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | логическое значение | Включить обнаружение значков | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | логическое значение | Отключить копирование при выборе в TUI | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | число | Таймаут по умолчанию для команд bash в мс | @@ -609,3 +609,9 @@ opencode можно настроить с помощью переменных с | `OPENCODE_EXPERIMENTAL_EXA` | логическое значение | Включить экспериментальные функции Exa | | `OPENCODE_EXPERIMENTAL_LSP_TY` | логическое значение | Включить TY LSP для файлов python | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | логическое значение | Включить режим плана | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | логическое значение | Включить фоновые задачи субагентов | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | логическое значение | Включить экспериментальную систему событий | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | логическое значение | Включить нативный путь запросов LLM | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | логическое значение | Включить параллельное выполнение веб-поиска | +| `OPENCODE_EXPERIMENTAL_SCOUT` | логическое значение | Включить субагент Scout | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | логическое значение | Включить поддержку рабочих областей | diff --git a/packages/web/src/content/docs/th/cli.mdx b/packages/web/src/content/docs/th/cli.mdx index 2c63bf6e8..49df515c8 100644 --- a/packages/web/src/content/docs/th/cli.mdx +++ b/packages/web/src/content/docs/th/cli.mdx @@ -598,7 +598,7 @@ OpenCode สามารถกำหนดค่าโดยใช้ตัว | ตัวแปร | ชนิด | คำอธิบาย | | ----------------------------------------------- | ------- | ---------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | Boolean | เปิดใช้งานฟีเจอร์ทดลองทั้งหมด | +| `OPENCODE_EXPERIMENTAL` | Boolean | เปิดใช้งานฟีเจอร์ทดลองภายใต้แฟล็กรวม | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | Boolean | การค้นหาไอคอนทดลอง | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | Boolean | ปิดใช้งานการคัดลอกเมื่อเลือกใน TUI | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Number | การหมดเวลาเริ่มต้นสำหรับคำสั่ง bash ในหน่วย ms | @@ -610,3 +610,9 @@ OpenCode สามารถกำหนดค่าโดยใช้ตัว | `OPENCODE_EXPERIMENTAL_EXA` | Boolean | ฟีเจอร์ Exa ทดลอง | | `OPENCODE_EXPERIMENTAL_LSP_TY` | Boolean | เปิดใช้งาน TY LSP สำหรับไฟล์ python | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | Boolean | เปิดใช้งาน Plan mode | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | Boolean | เปิดใช้งานงาน subagent เบื้องหลัง | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | Boolean | เปิดใช้งานระบบเหตุการณ์ทดลอง | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | Boolean | เปิดใช้งานเส้นทางคำขอ LLM แบบ native | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | Boolean | เปิดใช้งานการค้นหาเว็บแบบขนาน | +| `OPENCODE_EXPERIMENTAL_SCOUT` | Boolean | เปิดใช้งาน Scout subagent | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | Boolean | เปิดใช้งานการรองรับ workspace | diff --git a/packages/web/src/content/docs/tr/cli.mdx b/packages/web/src/content/docs/tr/cli.mdx index 571d4058f..4f28122bd 100644 --- a/packages/web/src/content/docs/tr/cli.mdx +++ b/packages/web/src/content/docs/tr/cli.mdx @@ -597,7 +597,7 @@ Bu ortam değişkenleri değişebilecek veya kaldırılabilecek deneysel özelli | Değişken | Tip | Tanım | | ----------------------------------------------- | ------- | ------------------------------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | Tüm deneysel özellikleri etkinleştir | +| `OPENCODE_EXPERIMENTAL` | boolean | Şemsiye bayrağa bağlı deneysel özellikleri etkinleştir | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Simge bulmayı etkinleştir | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI'da seçim yapıldığında kopyalamayı devre dışı bırak | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | MS cinsinden bash komutları için varsayılan zaman aşımı | @@ -609,3 +609,9 @@ Bu ortam değişkenleri değişebilecek veya kaldırılabilecek deneysel özelli | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Deneysel Exa özelliklerini etkinleştirin | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python dosyaları için TY LSP'yi etkinleştir | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan modunu etkinleştir | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Arka plan alt ajan görevlerini etkinleştir | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Deneysel olay sistemini etkinleştir | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Yerel LLM istek yolunu etkinleştir | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Paralel web araması yürütmesini etkinleştir | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Scout alt ajanını etkinleştir | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Çalışma alanı desteğini etkinleştir | diff --git a/packages/web/src/content/docs/zh-cn/cli.mdx b/packages/web/src/content/docs/zh-cn/cli.mdx index 883a8904c..46f090bb7 100644 --- a/packages/web/src/content/docs/zh-cn/cli.mdx +++ b/packages/web/src/content/docs/zh-cn/cli.mdx @@ -597,7 +597,7 @@ OpenCode 可以通过环境变量进行配置。 | 变量 | 类型 | 描述 | | ----------------------------------------------- | ------- | ------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | 启用所有实验性功能 | +| `OPENCODE_EXPERIMENTAL` | boolean | 启用受总开关控制的实验性功能 | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 启用图标发现 | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 禁用 TUI 中的选中即复制 | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 命令的默认超时时间(毫秒) | @@ -609,3 +609,9 @@ OpenCode 可以通过环境变量进行配置。 | `OPENCODE_EXPERIMENTAL_EXA` | boolean | 启用实验性 Exa 功能 | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 为 python 文件启用 TY LSP | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | 启用计划模式 | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | 启用后台子代理任务 | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | 启用实验性事件系统 | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | 启用原生 LLM 请求路径 | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | 启用并行 Web 搜索执行 | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | 启用 Scout 子代理 | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | 启用工作区支持 | diff --git a/packages/web/src/content/docs/zh-tw/cli.mdx b/packages/web/src/content/docs/zh-tw/cli.mdx index cf0189845..25e7bce88 100644 --- a/packages/web/src/content/docs/zh-tw/cli.mdx +++ b/packages/web/src/content/docs/zh-tw/cli.mdx @@ -597,7 +597,7 @@ OpenCode 可以透過環境變數進行設定。 | 變數 | 類型 | 說明 | | ----------------------------------------------- | ------- | ------------------------------- | -| `OPENCODE_EXPERIMENTAL` | boolean | 啟用所有實驗性功能 | +| `OPENCODE_EXPERIMENTAL` | boolean | 啟用受總開關控制的實驗性功能 | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 啟用圖示探索 | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 停用 TUI 中的選取即複製 | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 指令的預設逾時時間(毫秒) | @@ -609,3 +609,9 @@ OpenCode 可以透過環境變數進行設定。 | `OPENCODE_EXPERIMENTAL_EXA` | boolean | 啟用實驗性 Exa 功能 | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 為 python 檔案啟用 TY LSP | | `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | 啟用計畫模式 | +| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | 啟用背景子代理任務 | +| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | 啟用實驗性事件系統 | +| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | 啟用原生 LLM 請求路徑 | +| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | 啟用平行 Web 搜尋執行 | +| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | 啟用 Scout 子代理 | +| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | 啟用工作區支援 | diff --git a/patches/virtua@0.49.1.patch b/patches/virtua@0.49.1.patch new file mode 100644 index 000000000..d8064d3d0 --- /dev/null +++ b/patches/virtua@0.49.1.patch @@ -0,0 +1,93 @@ +diff --git a/lib/solid/Virtualizer.d.ts b/lib/solid/Virtualizer.d.ts +index 144dd7f..819aab9 100644 +--- a/lib/solid/Virtualizer.d.ts ++++ b/lib/solid/Virtualizer.d.ts +@@ -38,6 +38,10 @@ export interface VirtualizerHandle { + * @param index index of item + */ + getItemSize(index: number): number; ++ /** ++ * Synchronously measure currently mounted items and update cached item sizes. ++ */ ++ measure(): void; + /** + * Scroll to the item specified by index. + * @param index index of item +diff --git a/lib/solid/index.jsx b/lib/solid/index.jsx +index 029201a..3949cd4 100644 +--- a/lib/solid/index.jsx ++++ b/lib/solid/index.jsx +@@ -1085,6 +1085,7 @@ const createResizer = (store, isHorizontal) => { + let viewportElement; + const sizeKey = isHorizontal ? "width" : "height"; + const mountedIndexes = new WeakMap(); ++ const mountedItems = new Map(); + const resizeObserver = createResizeObserver((entries) => { + const resizes = []; + for (const { target, contentRect } of entries) { +@@ -1111,12 +1112,27 @@ const createResizer = (store, isHorizontal) => { + }, + $observeItem: (el, i) => { + mountedIndexes.set(el, i); ++ mountedItems.set(i, el); + resizeObserver._observe(el); + return () => { + mountedIndexes.delete(el); ++ if (mountedItems.get(i) === el) { ++ mountedItems.delete(i); ++ } + resizeObserver._unobserve(el); + }; + }, ++ $measureItems: () => { ++ const resizes = []; ++ mountedItems.forEach((el, index) => { ++ if (!el.offsetParent) ++ return; ++ resizes.push([index, el.getBoundingClientRect()[sizeKey]]); ++ }); ++ if (resizes.length) { ++ store.$update(ACTION_ITEM_RESIZE, resizes); ++ } ++ }, + $dispose: resizeObserver._dispose, + }; + }; +@@ -1354,6 +1370,8 @@ const Virtualizer = (props) => { + const range = createMemo((prev) => { + stateVersion(); + const next = store.$getRange(props.bufferSize); ++ next[0] = Math.max(0, next[0]); ++ next[1] = Math.min(props.data.length - 1, next[1]); + if (prev && isSameRange(prev, next)) { + return prev; + } +@@ -1380,6 +1398,7 @@ const Virtualizer = (props) => { + findItemIndex: store.$findItemIndex, + getItemOffset: store.$getItemOffset, + getItemSize: store.$getItemSize, ++ measure: resizer.$measureItems, + scrollToIndex: scroller.$scrollToIndex, + scrollTo: scroller.$scrollTo, + scrollBy: scroller.$scrollBy, +@@ -1417,6 +1436,11 @@ const Virtualizer = (props) => { + const indexes = []; + if (props.keepMounted) { + const mounted = new Set(props.keepMounted); ++ mounted.forEach((index) => { ++ if (index < 0 || index >= count) { ++ mounted.delete(index); ++ } ++ }); + for (let [i, j] = range(); i <= j; i++) { + mounted.add(i); + } +@@ -1528,6 +1552,8 @@ const WindowVirtualizer = (props) => { + const range = createMemo((prev) => { + stateVersion(); + const next = store.$getRange(props.bufferSize); ++ next[0] = Math.max(0, next[0]); ++ next[1] = Math.min(props.data.length - 1, next[1]); + if (prev && isSameRange(prev, next)) { + return prev; + }