From 67047fa7669e17670ae40595cee648a1ad8f0ad8 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 4 May 2026 13:26:08 +0000 Subject: [PATCH 01/27] chore: generate --- packages/app/src/context/terminal.test.ts | 5 ++++- packages/app/src/context/terminal.tsx | 14 +++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts index 623303fbf4..5bca1b4b7e 100644 --- a/packages/app/src/context/terminal.test.ts +++ b/packages/app/src/context/terminal.test.ts @@ -62,7 +62,10 @@ describe("getTerminalServerScope", () => { ), ).toBe("wsl:Debian" as ServerKey) expect( - getTerminalServerScope({ type: "http", http: { url: "https://example.com" } }, "https://example.com" as ServerKey), + getTerminalServerScope( + { type: "http", http: { url: "https://example.com" } }, + "https://example.com" as ServerKey, + ), ).toBe("https://example.com" as ServerKey) }) }) diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 0dcebd567d..f6751c3f0e 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -94,7 +94,12 @@ export function getTerminalServerScope(conn: ServerConnection.Any | undefined, k if (conn.type === "http") { try { const url = new URL(conn.http.url) - if (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]") + if ( + url.hostname === "localhost" || + url.hostname === "127.0.0.1" || + url.hostname === "::1" || + url.hostname === "[::1]" + ) return } catch { return key @@ -127,12 +132,7 @@ const trimTerminal = (pty: LocalPTY) => { } } -export function clearWorkspaceTerminals( - dir: string, - sessionIDs?: string[], - platform?: Platform, - scope?: string, -) { +export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform, scope?: string) { const key = getWorkspaceTerminalCacheKey(dir, scope) for (const cache of caches) { const entry = cache.get(key) From 1251a870cb384543c150c4a72fb101b55eec971b Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Mon, 4 May 2026 15:43:03 +0200 Subject: [PATCH 02/27] fix(opencode): strip transfer-encoding in UI proxy and allow public manifest assets (#25698) Co-authored-by: Kit Langton --- packages/app/src/components/terminal.tsx | 2 +- packages/opencode/src/server/middleware.ts | 2 ++ .../instance/httpapi/middleware/authorization.ts | 2 ++ packages/opencode/src/server/shared/public-ui.ts | 12 ++++++++++++ packages/opencode/src/server/shared/ui.ts | 1 + 5 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/src/server/shared/public-ui.ts diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index d8ed63b8d2..6dae9de955 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -482,7 +482,7 @@ export const Terminal = (props: TerminalProps) => { const connectToken = async () => { const result = await client.pty .connectToken( - { ptyID: id }, + { ptyID: id, directory }, { throwOnError: false, headers: { "x-opencode-ticket": "1" }, diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index 898acaf089..160d258796 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -13,6 +13,7 @@ import { compress } from "hono/compress" import * as ServerBackend from "./backend" import { isAllowedCorsOrigin, type CorsOptions } from "./cors" import { isPtyConnectPath, PTY_CONNECT_TICKET_QUERY } from "./shared/pty-ticket" +import { isPublicUIPath } from "./shared/public-ui" const log = Log.create({ service: "server" }) @@ -45,6 +46,7 @@ export const AuthMiddleware: MiddlewareHandler = (c, next) => { if (c.req.method === "OPTIONS") return next() const password = Flag.OPENCODE_SERVER_PASSWORD if (!password) return next() + if (isPublicUIPath(c.req.method, c.req.path)) return next() if (isPtyConnectPath(c.req.path) && c.req.query(PTY_CONNECT_TICKET_QUERY)) return next() const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts index 6c6d0cd1f1..6f5648f30a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts @@ -3,6 +3,7 @@ import { Effect, Encoding, Layer, Redacted } from "effect" import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { HttpApiError, HttpApiMiddleware } from "effect/unstable/httpapi" import { hasPtyConnectTicketURL } from "@/server/shared/pty-ticket" +import { isPublicUIPath } from "@/server/shared/public-ui" const AUTH_TOKEN_QUERY = "auth_token" const UNAUTHORIZED = 401 @@ -92,6 +93,7 @@ export const authorizationRouterMiddleware = HttpRouter.middleware()( Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest const url = new URL(request.url, "http://localhost") + if (isPublicUIPath(request.method, url.pathname)) return yield* effect if (hasPtyConnectTicketURL(url)) return yield* effect return yield* credentialFromURL(url, request).pipe( Effect.flatMap((credential) => validateRawCredential(effect, credential, config)), diff --git a/packages/opencode/src/server/shared/public-ui.ts b/packages/opencode/src/server/shared/public-ui.ts new file mode 100644 index 0000000000..fece09592f --- /dev/null +++ b/packages/opencode/src/server/shared/public-ui.ts @@ -0,0 +1,12 @@ +// Static UI assets the browser fetches without app-managed credentials, e.g. +// the manifest link in . These bypass auth so the page can install/render +// the manifest icons even when a server password is configured. +export const PUBLIC_UI_PATHS = new Set([ + "/site.webmanifest", + "/web-app-manifest-192x192.png", + "/web-app-manifest-512x512.png", +]) + +export function isPublicUIPath(method: string, pathname: string) { + return method === "GET" && PUBLIC_UI_PATHS.has(pathname) +} diff --git a/packages/opencode/src/server/shared/ui.ts b/packages/opencode/src/server/shared/ui.ts index c1558a1a4e..40d8aa7afb 100644 --- a/packages/opencode/src/server/shared/ui.ts +++ b/packages/opencode/src/server/shared/ui.ts @@ -33,6 +33,7 @@ function proxyResponseHeaders(headers: Record) { // transfer metadata makes browsers decode already-decoded assets again. result.delete("content-encoding") result.delete("content-length") + result.delete("transfer-encoding") return result } From 6e9f10ad3fbace5df1e3955404c8210528918349 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 4 May 2026 09:54:19 -0400 Subject: [PATCH 03/27] test(server): regression reproducers for #25698 (#25714) --- .../test/server/httpapi-listen.test.ts | 40 ++++++++++++ .../opencode/test/server/httpapi-ui.test.ts | 65 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/packages/opencode/test/server/httpapi-listen.test.ts b/packages/opencode/test/server/httpapi-listen.test.ts index af4c0a01ce..7258b32a92 100644 --- a/packages/opencode/test/server/httpapi-listen.test.ts +++ b/packages/opencode/test/server/httpapi-listen.test.ts @@ -257,6 +257,46 @@ describe("HttpApi Server.listen", () => { } }) + // Regression for #25698 (Ope): the app's SDK call to + // `client.pty.connectToken({ ptyID })` originally omitted `directory`, so + // the server resolved the PTY in its own cwd context — where the project + // PTY isn't registered — and returned 404. The fix is to always pass + // `directory` from the app side; this test locks in two contracts: + // 1. Mint without directory cannot find a PTY registered in another dir. + // 2. Mint with the project directory succeeds; the resulting ticket + // consumes cleanly when the WS upgrade carries the same directory. + testPty("PTY connect token requires matching directory across mint and connect", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const listener = await startListener() + try { + const info = await createCat(listener, tmp.path) + + // Mint without directory — server uses its own cwd, can't find the PTY. + const ambiguous = await fetch(new URL(PtyPaths.connectToken.replace(":ptyID", info.id), listener.url), { + method: "POST", + headers: { authorization: authorization(), "x-opencode-ticket": "1" }, + }) + expect(ambiguous.status).toBe(404) + + // Mint with the project directory — succeeds, ticket binds to that scope. + const scoped = await fetch( + new URL(`${PtyPaths.connectToken.replace(":ptyID", info.id)}?directory=${encodeURIComponent(tmp.path)}`, listener.url), + { + method: "POST", + headers: { authorization: authorization(), "x-opencode-ticket": "1" }, + }, + ) + expect(scoped.status).toBe(200) + const mint = (await scoped.json()) as { ticket: string } + + // Same directory on the WS upgrade → consume succeeds. + const ws = await openSocket(socketURL(listener, info.id, tmp.path, mint.ticket)) + ws.close(1000) + } finally { + await stop(listener, "timed out cleaning up directory-scope listener").catch(() => undefined) + } + }) + testPty("keeps PTY websocket tickets optional when server auth is disabled", async () => { await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) const listener = await startNoAuthListener() diff --git a/packages/opencode/test/server/httpapi-ui.test.ts b/packages/opencode/test/server/httpapi-ui.test.ts index f364491ace..85162f6a92 100644 --- a/packages/opencode/test/server/httpapi-ui.test.ts +++ b/packages/opencode/test/server/httpapi-ui.test.ts @@ -184,6 +184,52 @@ describe("HttpApi UI fallback", () => { expect(await response.text()).toBe("console.log('ok')") }) + // Regression for #25698 (Ope): upstream `transfer-encoding: chunked` was + // forwarded through the proxy while the proxy itself re-frames the body, + // causing browsers to fail with `ERR_INVALID_CHUNKED_ENCODING`. + test("strips upstream transfer-encoding header from proxied assets", async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true + + const response = await Effect.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const client = yield* HttpClient.HttpClient + return yield* serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/")), { + fs, + client, + }) + }).pipe( + Effect.provide( + Layer.mergeAll( + AppFileSystem.defaultLayer, + Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response("opencode", { + headers: { + "transfer-encoding": "chunked", + "content-type": "text/html", + }, + }), + ), + ), + ), + ), + ), + ), + Effect.map(HttpServerResponse.toWeb), + ), + ) + + expect(response.status).toBe(200) + expect(response.headers.get("transfer-encoding")).toBeNull() + expect(await response.text()).toBe("opencode") + }) + test("serves embedded UI assets when Bun can read them but access reports missing", async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true let readPath: string | undefined @@ -257,6 +303,25 @@ describe("HttpApi UI fallback", () => { expect(response.status).toBe(200) }) + // Regression for #25698 (Ope): the browser fetches the PWA manifest and + // its icons via flows that don't carry app-managed credentials (the + // `` request is not under page-auth control), so the + // server returning 401 breaks PWA install. These specific public assets + // should bypass auth. + test("serves the PWA manifest without auth even when a server password is set", async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true + + for (const path of ["/site.webmanifest", "/web-app-manifest-192x192.png", "/web-app-manifest-512x512.png"]) { + const response = await uiApp({ + password: "secret", + username: "opencode", + client: httpClient(new Response("ok")), + }).request(path) + expect(response.status).not.toBe(401) + } + }) + test("allows web UI preflight without auth", async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true From 2c819f290fcb3db83ec12638749959cdc973b5ad Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 4 May 2026 13:55:28 +0000 Subject: [PATCH 04/27] chore: generate --- packages/opencode/test/server/httpapi-listen.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/server/httpapi-listen.test.ts b/packages/opencode/test/server/httpapi-listen.test.ts index 7258b32a92..98ae30e8a7 100644 --- a/packages/opencode/test/server/httpapi-listen.test.ts +++ b/packages/opencode/test/server/httpapi-listen.test.ts @@ -280,7 +280,10 @@ describe("HttpApi Server.listen", () => { // Mint with the project directory — succeeds, ticket binds to that scope. const scoped = await fetch( - new URL(`${PtyPaths.connectToken.replace(":ptyID", info.id)}?directory=${encodeURIComponent(tmp.path)}`, listener.url), + new URL( + `${PtyPaths.connectToken.replace(":ptyID", info.id)}?directory=${encodeURIComponent(tmp.path)}`, + listener.url, + ), { method: "POST", headers: { authorization: authorization(), "x-opencode-ticket": "1" }, From c1f607d206e7d723d8093650559fffb8a144738e Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 4 May 2026 09:58:21 -0500 Subject: [PATCH 05/27] fix: ensure anthropic sdk properly resolves when using azure (#25721) --- packages/opencode/src/provider/provider.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 939110e044..4013dcee36 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -138,6 +138,14 @@ function useLanguageModel(sdk: any) { return sdk.responses === undefined && sdk.chat === undefined } +function selectAzureLanguageModel(sdk: any, modelID: string, useChat: boolean) { + if (useChat && sdk.chat) return sdk.chat(modelID) + if (sdk.responses) return sdk.responses(modelID) + if (sdk.messages) return sdk.messages(modelID) + if (sdk.chat) return sdk.chat(modelID) + return sdk.languageModel(modelID) +} + function custom(dep: CustomDep): Record { return { anthropic: () => @@ -222,12 +230,7 @@ function custom(dep: CustomDep): Record { return { autoload: false, async getModel(sdk: any, modelID: string, options?: Record) { - if (useLanguageModel(sdk)) return sdk.languageModel(modelID) - if (options?.["useCompletionUrls"]) { - return sdk.chat(modelID) - } else { - return sdk.responses(modelID) - } + return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"])) }, options: { resourceName: resource, @@ -247,12 +250,7 @@ function custom(dep: CustomDep): Record { return { autoload: false, async getModel(sdk: any, modelID: string, options?: Record) { - if (useLanguageModel(sdk)) return sdk.languageModel(modelID) - if (options?.["useCompletionUrls"]) { - return sdk.chat(modelID) - } else { - return sdk.responses(modelID) - } + return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"])) }, options: { baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined, From 1aed6b1d8bfa5502cdc6997234a0d5be9933ec52 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 May 2026 11:16:23 -0400 Subject: [PATCH 06/27] sync --- packages/console/app/src/routes/zen/util/handler.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 2f75668e67..8bab495b72 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -158,11 +158,13 @@ export async function handler( Object.entries(obj).flatMap(([k, v]) => { if (Array.isArray(v)) return [[k, v]] if (typeof v === "object") return [[k, replacer(v)]] - if (v === "$ip") return [[k, ip]] - if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : [] - if (v.startsWith("$header.")) { - const headerValue = input.request.headers.get(v.slice(8)) - return headerValue ? [[k, headerValue]] : [] + if (typeof v === "string") { + if (v === "$ip") return [[k, ip]] + if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : [] + if (v.startsWith("$header.")) { + const headerValue = input.request.headers.get(v.slice(8)) + return headerValue ? [[k, headerValue]] : [] + } } return [[k, v]] }), From b70e2700ef38c166730d8af26ac97e36baa660c1 Mon Sep 17 00:00:00 2001 From: Colby Gilbert Date: Mon, 4 May 2026 08:27:03 -0700 Subject: [PATCH 07/27] chore(docs): rename firmware provider to frogbot (#25453) --- packages/web/src/content/docs/ar/providers.mdx | 8 ++++---- packages/web/src/content/docs/bs/providers.mdx | 8 ++++---- packages/web/src/content/docs/da/providers.mdx | 8 ++++---- packages/web/src/content/docs/de/providers.mdx | 8 ++++---- packages/web/src/content/docs/es/providers.mdx | 8 ++++---- packages/web/src/content/docs/fr/providers.mdx | 6 +++--- packages/web/src/content/docs/it/providers.mdx | 8 ++++---- packages/web/src/content/docs/ja/providers.mdx | 4 ++-- packages/web/src/content/docs/ko/providers.mdx | 8 ++++---- packages/web/src/content/docs/nb/providers.mdx | 8 ++++---- packages/web/src/content/docs/pl/providers.mdx | 8 ++++---- packages/web/src/content/docs/providers.mdx | 8 ++++---- packages/web/src/content/docs/pt-br/providers.mdx | 8 ++++---- packages/web/src/content/docs/ru/providers.mdx | 8 ++++---- packages/web/src/content/docs/th/providers.mdx | 8 ++++---- packages/web/src/content/docs/tr/providers.mdx | 8 ++++---- packages/web/src/content/docs/zh-cn/providers.mdx | 8 ++++---- packages/web/src/content/docs/zh-tw/providers.mdx | 8 ++++---- 18 files changed, 69 insertions(+), 69 deletions(-) diff --git a/packages/web/src/content/docs/ar/providers.mdx b/packages/web/src/content/docs/ar/providers.mdx index 07a19b8ad2..c4812fe5d5 100644 --- a/packages/web/src/content/docs/ar/providers.mdx +++ b/packages/web/src/content/docs/ar/providers.mdx @@ -648,17 +648,17 @@ OpenCode Go هي خطة اشتراك منخفضة التكلفة توفّر وص --- -### Firmware +### FrogBot -1. توجّه إلى [Firmware dashboard](https://app.firmware.ai/signup)، وأنشئ حسابا، ثم أنشئ مفتاح API. +1. توجّه إلى [FrogBot dashboard](https://app.frogbot.ai/signup)، وأنشئ حسابا، ثم أنشئ مفتاح API. -2. شغّل الأمر `/connect` وابحث عن **Firmware**. +2. شغّل الأمر `/connect` وابحث عن **FrogBot**. ```txt /connect ``` -3. أدخل مفتاح API الخاص بـ Firmware. +3. أدخل مفتاح API الخاص بـ FrogBot. ```txt ┌ API key diff --git a/packages/web/src/content/docs/bs/providers.mdx b/packages/web/src/content/docs/bs/providers.mdx index 4087db8cde..f6e54fc6ad 100644 --- a/packages/web/src/content/docs/bs/providers.mdx +++ b/packages/web/src/content/docs/bs/providers.mdx @@ -653,17 +653,17 @@ Također možete dodati modele kroz svoju opencode konfiguraciju. --- -### Firmware +### FrogBot -1. Idite na [kontrolnu tablu firmvera](https://app.firmware.ai/signup), kreirajte nalog i generišite API ključ. +1. Idite na [kontrolnu tablu firmvera](https://app.frogbot.ai/signup), kreirajte nalog i generišite API ključ. -2. Pokrenite naredbu `/connect` i potražite **Firmware**. +2. Pokrenite naredbu `/connect` i potražite **FrogBot**. ```txt /connect ``` -3. Unesite svoj Firmware API ključ. +3. Unesite svoj FrogBot API ključ. ```txt ┌ API key diff --git a/packages/web/src/content/docs/da/providers.mdx b/packages/web/src/content/docs/da/providers.mdx index 8817d23192..9b04d6be82 100644 --- a/packages/web/src/content/docs/da/providers.mdx +++ b/packages/web/src/content/docs/da/providers.mdx @@ -644,17 +644,17 @@ Cloudflare AI Gateway lader dig få adgang til modeller fra OpenAI, Anthropic, W --- -### Firmware +### FrogBot -1. Gå til [Firmware dashboard](https://app.firmware.ai/signup), opret en konto og generer en API-nøgle. +1. Gå til [FrogBot dashboard](https://app.frogbot.ai/signup), opret en konto og generer en API-nøgle. -2. Kør kommandoen `/connect` og søg efter **Firmware**. +2. Kør kommandoen `/connect` og søg efter **FrogBot**. ```txt /connect ``` -3. Indtast firmware API-nøglen. +3. Indtast frogbot API-nøglen. ```txt ┌ API key diff --git a/packages/web/src/content/docs/de/providers.mdx b/packages/web/src/content/docs/de/providers.mdx index 87f78c9d22..9298146930 100644 --- a/packages/web/src/content/docs/de/providers.mdx +++ b/packages/web/src/content/docs/de/providers.mdx @@ -650,17 +650,17 @@ Mit dem Cloudflare AI Gateway können Sie über einen einheitlichen Endpunkt auf --- -### Firmware +### FrogBot -1. Gehen Sie zu [Firmware dashboard](https://app.firmware.ai/signup), erstellen Sie ein Konto und generieren Sie einen API-Schlüssel. +1. Gehen Sie zu [FrogBot dashboard](https://app.frogbot.ai/signup), erstellen Sie ein Konto und generieren Sie einen API-Schlüssel. -2. Führen Sie den Befehl `/connect` aus und suchen Sie nach **Firmware**. +2. Führen Sie den Befehl `/connect` aus und suchen Sie nach **FrogBot**. ```txt /connect ``` -3. Geben Sie Ihren Firmware API-Schlüssel ein. +3. Geben Sie Ihren FrogBot API-Schlüssel ein. ```txt ┌ API key diff --git a/packages/web/src/content/docs/es/providers.mdx b/packages/web/src/content/docs/es/providers.mdx index b44ce9ee99..11489609bc 100644 --- a/packages/web/src/content/docs/es/providers.mdx +++ b/packages/web/src/content/docs/es/providers.mdx @@ -651,17 +651,17 @@ Cloudflare AI Gateway le permite acceder a modelos de OpenAI, Anthropic, Workers --- -### Firmware +### FrogBot -1. Dirígete al [Panel de firmware](https://app.firmware.ai/signup), crea una cuenta y genera una clave API. +1. Dirígete al [Panel de frogbot](https://app.frogbot.ai/signup), crea una cuenta y genera una clave API. -2. Ejecute el comando `/connect` y busque **Firmware**. +2. Ejecute el comando `/connect` y busque **FrogBot**. ```txt /connect ``` -3. Ingrese su clave de firmware API. +3. Ingrese su clave de frogbot API. ```txt ┌ API key diff --git a/packages/web/src/content/docs/fr/providers.mdx b/packages/web/src/content/docs/fr/providers.mdx index 6a902ab02f..90bdb1fbc3 100644 --- a/packages/web/src/content/docs/fr/providers.mdx +++ b/packages/web/src/content/docs/fr/providers.mdx @@ -654,11 +654,11 @@ Vous pouvez également ajouter des modèles via votre configuration opencode. --- -### Firmware +### FrogBot -1. Rendez-vous sur le [Tableau de bord du micrologiciel](https://app.firmware.ai/signup), créez un compte et générez une clé API. +1. Rendez-vous sur le [Tableau de bord du micrologiciel](https://app.frogbot.ai/signup), créez un compte et générez une clé API. -2. Exécutez la commande `/connect` et recherchez **Firmware**. +2. Exécutez la commande `/connect` et recherchez **FrogBot**. ```txt /connect diff --git a/packages/web/src/content/docs/it/providers.mdx b/packages/web/src/content/docs/it/providers.mdx index 96da8c4df1..f2d195d721 100644 --- a/packages/web/src/content/docs/it/providers.mdx +++ b/packages/web/src/content/docs/it/providers.mdx @@ -628,17 +628,17 @@ Cloudflare AI Gateway ti permette di accedere a modelli di OpenAI, Anthropic, Wo --- -### Firmware +### FrogBot -1. Vai alla [dashboard di Firmware](https://app.firmware.ai/signup), crea un account e genera una chiave API. +1. Vai alla [dashboard di FrogBot](https://app.frogbot.ai/signup), crea un account e genera una chiave API. -2. Esegui il comando `/connect` e cerca **Firmware**. +2. Esegui il comando `/connect` e cerca **FrogBot**. ```txt /connect ``` -3. Inserisci la tua chiave API di Firmware. +3. Inserisci la tua chiave API di FrogBot. ```txt ┌ API key diff --git a/packages/web/src/content/docs/ja/providers.mdx b/packages/web/src/content/docs/ja/providers.mdx index 8017d0882e..c969c6d4a0 100644 --- a/packages/web/src/content/docs/ja/providers.mdx +++ b/packages/web/src/content/docs/ja/providers.mdx @@ -658,9 +658,9 @@ OpenCode 設定を通じてモデルを追加することもできます。 --- -### Firmware +### FrogBot -1. [ファームウェアダッシュボード](https://app.firmware.ai/signup) に移動し、アカウントを作成し、API キーを生成します。 +1. [ファームウェアダッシュボード](https://app.frogbot.ai/signup) に移動し、アカウントを作成し、API キーを生成します。 2. `/connect` コマンドを実行し、**ファームウェア**を検索します。 diff --git a/packages/web/src/content/docs/ko/providers.mdx b/packages/web/src/content/docs/ko/providers.mdx index 6ca3afccc3..87278bef23 100644 --- a/packages/web/src/content/docs/ko/providers.mdx +++ b/packages/web/src/content/docs/ko/providers.mdx @@ -654,17 +654,17 @@ Cloudflare AI Gateway는 OpenAI, Anthropic, Workers AI 등의 모델에 액세 --- -### Firmware +### FrogBot -1. [Firmware 대시보드](https://app.firmware.ai/signup)로 이동하여 계정을 만들고 API 키를 생성합니다. +1. [FrogBot 대시보드](https://app.frogbot.ai/signup)로 이동하여 계정을 만들고 API 키를 생성합니다. -2. `/connect` 명령을 실행하고 **Firmware**를 검색하십시오. +2. `/connect` 명령을 실행하고 **FrogBot**를 검색하십시오. ```txt /connect ``` -3. Firmware API 키를 입력하십시오. +3. FrogBot API 키를 입력하십시오. ```txt ┌ API key diff --git a/packages/web/src/content/docs/nb/providers.mdx b/packages/web/src/content/docs/nb/providers.mdx index 1fe8812e67..bf276918a9 100644 --- a/packages/web/src/content/docs/nb/providers.mdx +++ b/packages/web/src/content/docs/nb/providers.mdx @@ -652,17 +652,17 @@ Cloudflare AI Gateway lar deg få tilgang til modeller fra OpenAI, Anthropic, Wo --- -### Firmware +### FrogBot -1. Gå over til [Firmware dashboard](https://app.firmware.ai/signup), opprett en konto og generer en API nøkkel. +1. Gå over til [FrogBot dashboard](https://app.frogbot.ai/signup), opprett en konto og generer en API nøkkel. -2. Kjør kommandoen `/connect` og søk etter **Firmware**. +2. Kjør kommandoen `/connect` og søk etter **FrogBot**. ```txt /connect ``` -3. Skriv inn firmware API nøkkelen. +3. Skriv inn frogbot API nøkkelen. ```txt ┌ API key diff --git a/packages/web/src/content/docs/pl/providers.mdx b/packages/web/src/content/docs/pl/providers.mdx index deadd07d6a..0e722d5fde 100644 --- a/packages/web/src/content/docs/pl/providers.mdx +++ b/packages/web/src/content/docs/pl/providers.mdx @@ -650,17 +650,17 @@ Cloudflare AI Gateway umożliwia dostęp do modeli z OpenAI, Anthropic, Workers --- -### Firmware +### FrogBot -1. Przejdź do [Firmware dashboard](https://app.firmware.ai/signup), utwórz konto i wygeneruj klucz API. +1. Przejdź do [FrogBot dashboard](https://app.frogbot.ai/signup), utwórz konto i wygeneruj klucz API. -2. Uruchom polecenie `/connect` i wyszukaj **Firmware**. +2. Uruchom polecenie `/connect` i wyszukaj **FrogBot**. ```txt /connect ``` -3. Wprowadź klucz API Firmware. +3. Wprowadź klucz API FrogBot. ```txt ┌ API key diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 7c395022c1..8410c549f2 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -721,17 +721,17 @@ Cloudflare Workers AI lets you run AI models on Cloudflare's global network dire --- -### Firmware +### FrogBot -1. Head over to the [Firmware dashboard](https://app.firmware.ai/signup), create an account, and generate an API key. +1. Head over to the [FrogBot dashboard](https://app.frogbot.ai/signup), create an account, and generate an API key. -2. Run the `/connect` command and search for **Firmware**. +2. Run the `/connect` command and search for **FrogBot**. ```txt /connect ``` -3. Enter your Firmware API key. +3. Enter your FrogBot API key. ```txt ┌ API key diff --git a/packages/web/src/content/docs/pt-br/providers.mdx b/packages/web/src/content/docs/pt-br/providers.mdx index 50f841cf36..174bc1679b 100644 --- a/packages/web/src/content/docs/pt-br/providers.mdx +++ b/packages/web/src/content/docs/pt-br/providers.mdx @@ -654,17 +654,17 @@ O Cloudflare AI Gateway permite que você acesse modelos do OpenAI, Anthropic, W --- -### Firmware +### FrogBot -1. Acesse o [painel Firmware](https://app.firmware.ai/signup), crie uma conta e gere uma chave da API. +1. Acesse o [painel FrogBot](https://app.frogbot.ai/signup), crie uma conta e gere uma chave da API. -2. Execute o comando `/connect` e procure por **Firmware**. +2. Execute o comando `/connect` e procure por **FrogBot**. ```txt /connect ``` -3. Insira sua chave da API Firmware. +3. Insira sua chave da API FrogBot. ```txt ┌ API key diff --git a/packages/web/src/content/docs/ru/providers.mdx b/packages/web/src/content/docs/ru/providers.mdx index f5868ceaa0..39aae9e096 100644 --- a/packages/web/src/content/docs/ru/providers.mdx +++ b/packages/web/src/content/docs/ru/providers.mdx @@ -650,17 +650,17 @@ Cloudflare AI Gateway позволяет вам получать доступ к --- -### Firmware +### FrogBot -1. Перейдите на [панель Firmware](https://app.firmware.ai/signup), создайте учетную запись и сгенерируйте ключ API. +1. Перейдите на [панель FrogBot](https://app.frogbot.ai/signup), создайте учетную запись и сгенерируйте ключ API. -2. Запустите команду `/connect` и найдите **Firmware**. +2. Запустите команду `/connect` и найдите **FrogBot**. ```txt /connect ``` -3. Введите ключ API Firmware. +3. Введите ключ API FrogBot. ```txt ┌ API key diff --git a/packages/web/src/content/docs/th/providers.mdx b/packages/web/src/content/docs/th/providers.mdx index 818f39213c..07008de218 100644 --- a/packages/web/src/content/docs/th/providers.mdx +++ b/packages/web/src/content/docs/th/providers.mdx @@ -650,17 +650,17 @@ Cloudflare AI Gateway ช่วยให้คุณเข้าถึงโม --- -### Firmware +### FrogBot -1. ไปที่ [แดชบอร์ด Firmware](https://app.firmware.ai/signup) สร้างบัญชี และสร้างคีย์ API +1. ไปที่ [แดชบอร์ด FrogBot](https://app.frogbot.ai/signup) สร้างบัญชี และสร้างคีย์ API -2. เรียกใช้คำสั่ง `/connect` และค้นหา **Firmware** +2. เรียกใช้คำสั่ง `/connect` และค้นหา **FrogBot** ```txt /connect ``` -3. ป้อนคีย์ Firmware API ของคุณ +3. ป้อนคีย์ FrogBot API ของคุณ ```txt ┌ API key diff --git a/packages/web/src/content/docs/tr/providers.mdx b/packages/web/src/content/docs/tr/providers.mdx index 527c20e15e..8c6ef23fee 100644 --- a/packages/web/src/content/docs/tr/providers.mdx +++ b/packages/web/src/content/docs/tr/providers.mdx @@ -652,17 +652,17 @@ Cloudflare AI Gateway, OpenAI, Anthropic, Workers AI ve daha fazlasındaki model --- -### Firmware +### FrogBot -1. [Firmware dashboard](https://app.firmware.ai/signup) adresine gidin, bir hesap oluşturun ve bir API anahtarı oluşturun. +1. [FrogBot dashboard](https://app.frogbot.ai/signup) adresine gidin, bir hesap oluşturun ve bir API anahtarı oluşturun. -2. `/connect` komutunu çalıştırın ve **Firmware**'i arayın. +2. `/connect` komutunu çalıştırın ve **FrogBot**'i arayın. ```txt /connect ``` -3. Firmware API anahtarınızı girin. +3. FrogBot API anahtarınızı girin. ```txt ┌ API key diff --git a/packages/web/src/content/docs/zh-cn/providers.mdx b/packages/web/src/content/docs/zh-cn/providers.mdx index 80dfe1e93d..9c0a5d8a3b 100644 --- a/packages/web/src/content/docs/zh-cn/providers.mdx +++ b/packages/web/src/content/docs/zh-cn/providers.mdx @@ -624,17 +624,17 @@ Cloudflare AI Gateway 允许你通过统一端点访问来自 OpenAI、Anthropic --- -### Firmware +### FrogBot -1. 前往 [Firmware 仪表盘](https://app.firmware.ai/signup),创建账户并生成 API 密钥。 +1. 前往 [FrogBot 仪表盘](https://app.frogbot.ai/signup),创建账户并生成 API 密钥。 -2. 执行 `/connect` 命令并搜索 **Firmware**。 +2. 执行 `/connect` 命令并搜索 **FrogBot**。 ```txt /connect ``` -3. 输入你的 Firmware API 密钥。 +3. 输入你的 FrogBot API 密钥。 ```txt ┌ API key diff --git a/packages/web/src/content/docs/zh-tw/providers.mdx b/packages/web/src/content/docs/zh-tw/providers.mdx index c874170959..d4e55ed712 100644 --- a/packages/web/src/content/docs/zh-tw/providers.mdx +++ b/packages/web/src/content/docs/zh-tw/providers.mdx @@ -645,17 +645,17 @@ Cloudflare AI Gateway 允許您透過統一端點存取來自 OpenAI、Anthropic --- -### Firmware +### FrogBot -1. 前往 [Firmware 儀表板](https://app.firmware.ai/signup),建立帳號並產生 API 金鑰。 +1. 前往 [FrogBot 儀表板](https://app.frogbot.ai/signup),建立帳號並產生 API 金鑰。 -2. 執行 `/connect` 指令並搜尋 **Firmware**。 +2. 執行 `/connect` 指令並搜尋 **FrogBot**。 ```txt /connect ``` -3. 輸入您的 Firmware API 金鑰。 +3. 輸入您的 FrogBot API 金鑰。 ```txt ┌ API key From 25dc6f09bca2f9b90b7594e0a696f451f22f1254 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 4 May 2026 12:01:13 -0400 Subject: [PATCH 08/27] fix(worktree): fork workspace worktree boot (#25723) --- packages/opencode/src/worktree/index.ts | 11 +- .../opencode/test/project/worktree.test.ts | 4 +- .../server/worktree-endpoint-repro.test.ts | 148 ++++++++++++++++++ 3 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 packages/opencode/test/server/worktree-endpoint-repro.test.ts diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 43453b561a..f4e4d2721c 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -291,16 +291,15 @@ export const layer: Layer.Layer< const createFromInfo = Effect.fn("Worktree.createFromInfo")(function* (info: Info, startCommand?: string) { yield* setup(info) - yield* boot(info, startCommand) + yield* boot(info, startCommand).pipe( + Effect.catchCause((cause) => Effect.sync(() => log.error("worktree bootstrap failed", { cause }))), + Effect.forkIn(scope), + ) }) const create = Effect.fn("Worktree.create")(function* (input?: CreateInput) { const info = yield* makeWorktreeInfo(input?.name) - yield* setup(info) - yield* boot(info, input?.startCommand).pipe( - Effect.catchCause((cause) => Effect.sync(() => log.error("worktree bootstrap failed", { cause }))), - Effect.forkIn(scope), - ) + yield* createFromInfo(info, input?.startCommand) return info }) diff --git a/packages/opencode/test/project/worktree.test.ts b/packages/opencode/test/project/worktree.test.ts index a89fda6ca5..b191a3c952 100644 --- a/packages/opencode/test/project/worktree.test.ts +++ b/packages/opencode/test/project/worktree.test.ts @@ -178,12 +178,13 @@ describe("Worktree", () => { }) describe("createFromInfo", () => { - wintest("creates and bootstraps git worktree", () => + wintest("creates git worktree and boots asynchronously", () => provideTmpdirInstance( (dir) => Effect.gen(function* () { const svc = yield* Worktree.Service const info = yield* svc.makeWorktreeInfo("from-info-test") + const ready = waitReady() yield* svc.createFromInfo(info) const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(dir).quiet().text()) @@ -191,6 +192,7 @@ describe("Worktree", () => { const normalizedDir = info.directory.replace(/\\/g, "/") expect(normalizedList).toContain(normalizedDir) + yield* Effect.promise(() => ready) yield* svc.remove({ directory: info.directory }) }), { git: true }, diff --git a/packages/opencode/test/server/worktree-endpoint-repro.test.ts b/packages/opencode/test/server/worktree-endpoint-repro.test.ts new file mode 100644 index 0000000000..768a261a00 --- /dev/null +++ b/packages/opencode/test/server/worktree-endpoint-repro.test.ts @@ -0,0 +1,148 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { Flag } from "@opencode-ai/core/flag/flag" +import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server" +import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/groups/experimental" +import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace" +import { withTimeout } from "../../src/util/timeout" +import { resetDatabase } from "../fixture/db" +import { TestInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" + +const stateLayer = Layer.effectDiscard( + Effect.gen(function* () { + const original = { + OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI, + OPENCODE_EXPERIMENTAL_WORKSPACES: Flag.OPENCODE_EXPERIMENTAL_WORKSPACES, + } + + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true + + yield* Effect.addFinalizer(() => + Effect.promise(async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI + Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = original.OPENCODE_EXPERIMENTAL_WORKSPACES + await resetDatabase() + }), + ) + }), +) + +const it = testEffect(stateLayer) +type TestServer = ReturnType + +function serverScoped() { + return Effect.acquireRelease( + Effect.sync(() => HttpRouter.toWebHandler(ExperimentalHttpApiServer.routes, { disableLogger: true })), + (server) => Effect.promise(() => server.dispose()).pipe(Effect.ignore), + ) +} + +function request(server: TestServer, input: string, init?: RequestInit) { + return Effect.promise(() => + server.handler(new Request(new URL(input, "http://localhost"), init), ExperimentalHttpApiServer.context), + ) +} + +function withRequestTimeout(effect: Effect.Effect, label: string, ms = 5_000) { + return Effect.promise(() => withTimeout(Effect.runPromise(effect), ms, label)) +} + +function setProjectStartCommand(input: { server: TestServer; directory: string; command: string }) { + return Effect.gen(function* () { + const current = yield* request(input.server, `/project/current?directory=${encodeURIComponent(input.directory)}`) + expect(current.status).toBe(200) + const project = (yield* Effect.promise(() => current.json())) as { id: string } + const updated = yield* request( + input.server, + `/project/${project.id}?directory=${encodeURIComponent(input.directory)}`, + { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ commands: { start: input.command } }), + }, + ) + expect(updated.status).toBe(200) + }) +} + +describe("worktree endpoint reproduction", () => { + it.instance( + "direct HttpApi worktree create returns without waiting for boot", + () => + Effect.gen(function* () { + const test = yield* TestInstance + const server = yield* serverScoped() + + const response = yield* withRequestTimeout( + request(server, `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({}), + }), + "direct worktree create", + ) + + expect(response.status).toBe(200) + expect(yield* Effect.promise(() => response.json())).toMatchObject({ directory: expect.any(String) }) + }), + { git: true }, + ) + + it.instance( + "workspace worktree create does not hang", + () => + Effect.gen(function* () { + const test = yield* TestInstance + const server = yield* serverScoped() + + const response = yield* withRequestTimeout( + request(server, `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ type: "worktree", branch: null }), + }), + "workspace worktree create", + 8_000, + ) + + expect(response.status).toBe(200) + expect(yield* Effect.promise(() => response.json())).toMatchObject({ + type: "worktree", + directory: expect.any(String), + }) + }), + { git: true }, + ) + + it.instance( + "workspace worktree create returns without waiting for project start command", + () => + Effect.gen(function* () { + const test = yield* TestInstance + const server = yield* serverScoped() + yield* setProjectStartCommand({ + server, + directory: test.directory, + command: 'bun -e "setTimeout(() => {}, 2000)"', + }) + + const started = Date.now() + const response = yield* withRequestTimeout( + request(server, `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ type: "worktree", branch: null }), + }), + "workspace worktree create with project start command", + 6_000, + ) + + expect(response.status).toBe(200) + expect(Date.now() - started).toBeLessThan(1_500) + }), + { git: true }, + ) +}) From fb07c2070cba705bf0e9766a5a7ce6a3452797fb Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 4 May 2026 13:06:29 -0400 Subject: [PATCH 09/27] fix(server): provide fresh ConfigProvider per HttpApi listener (#25726) --- packages/opencode/src/server/server.ts | 8 ++++- .../test/server/httpapi-listen.test.ts | 34 ++++++++++--------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 3971214f3d..ca86599955 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -5,7 +5,7 @@ import { lazy } from "@/util/lazy" import * as Log from "@opencode-ai/core/util/log" import { Flag } from "@opencode-ai/core/flag/flag" import { WorkspaceID } from "@/control-plane/schema" -import { Context, Effect, Exit, Layer, Scope } from "effect" +import { ConfigProvider, Context, Effect, Exit, Layer, Scope } from "effect" import { HttpRouter, HttpServer } from "effect/unstable/http" import { OpenApi } from "effect/unstable/httpapi" import * as HttpApiServer from "#httpapi-server" @@ -259,6 +259,12 @@ async function listenHttpApi(opts: ListenOptions, selection: ServerBackend.Selec }).pipe( Layer.provideMerge(WebSocketTracker.layer), Layer.provideMerge(HttpApiServer.layer({ port, hostname: opts.hostname })), + // Install a fresh `ConfigProvider` per listener so `Config.string(...)` + // reads reflect the current `process.env`. Effect's default + // `ConfigProvider` snapshots `process.env` on first read and caches the + // result on a module-singleton Reference; without overriding it here, + // every later `Server.listen()` keeps observing that initial snapshot. + Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv())), ) const start = async (port: number) => { diff --git a/packages/opencode/test/server/httpapi-listen.test.ts b/packages/opencode/test/server/httpapi-listen.test.ts index 98ae30e8a7..b49fbe98b5 100644 --- a/packages/opencode/test/server/httpapi-listen.test.ts +++ b/packages/opencode/test/server/httpapi-listen.test.ts @@ -40,8 +40,8 @@ async function startListener(backend: "effect-httpapi" | "hono" = "effect-httpap return Server.listen({ hostname: "127.0.0.1", port: 0 }) } -async function startNoAuthListener() { - Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false +async function startNoAuthListener(backend: "effect-httpapi" | "hono" = "effect-httpapi") { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "effect-httpapi" Flag.OPENCODE_SERVER_PASSWORD = undefined Flag.OPENCODE_SERVER_USERNAME = auth.username delete process.env.OPENCODE_SERVER_PASSWORD @@ -300,18 +300,20 @@ describe("HttpApi Server.listen", () => { } }) - testPty("keeps PTY websocket tickets optional when server auth is disabled", async () => { - await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) - const listener = await startNoAuthListener() - try { - const info = await createCat(listener, tmp.path) - const ws = await openSocket(socketURL(listener, info.id, tmp.path)) - const message = waitForMessage(ws, (message) => message.includes("ping-no-auth")) - ws.send("ping-no-auth\n") - expect(await message).toContain("ping-no-auth") - ws.close(1000) - } finally { - await stop(listener, "timed out cleaning up no-auth listener").catch(() => undefined) - } - }) + for (const backend of ["effect-httpapi", "hono"] as const) { + testPty(`keeps PTY websocket tickets optional when server auth is disabled (${backend})`, async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const listener = await startNoAuthListener(backend) + try { + const info = await createCat(listener, tmp.path) + const ws = await openSocket(socketURL(listener, info.id, tmp.path)) + const message = waitForMessage(ws, (message) => message.includes(`ping-no-auth-${backend}`)) + ws.send(`ping-no-auth-${backend}\n`) + expect(await message).toContain(`ping-no-auth-${backend}`) + ws.close(1000) + } finally { + await stop(listener, "timed out cleaning up no-auth listener").catch(() => undefined) + } + }) + } }) From 007b57f0788b129a993228b5f1c340c640e94ea9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 4 May 2026 13:11:33 -0400 Subject: [PATCH 10/27] test(agent): skip InstanceBootstrap in plugin-agent regression test (#25737) --- .../agent/plugin-agent-regression.test.ts | 73 +++++-------------- .../test/fixture/agent-plugin.constants.ts | 6 ++ .../opencode/test/fixture/agent-plugin.ts | 12 +++ 3 files changed, 36 insertions(+), 55 deletions(-) create mode 100644 packages/opencode/test/fixture/agent-plugin.constants.ts create mode 100644 packages/opencode/test/fixture/agent-plugin.ts diff --git a/packages/opencode/test/agent/plugin-agent-regression.test.ts b/packages/opencode/test/agent/plugin-agent-regression.test.ts index 3ac923c435..dff972d100 100644 --- a/packages/opencode/test/agent/plugin-agent-regression.test.ts +++ b/packages/opencode/test/agent/plugin-agent-regression.test.ts @@ -1,65 +1,28 @@ import { expect } from "bun:test" -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Effect, Layer } from "effect" import path from "path" import { pathToFileURL } from "url" import { Agent } from "../../src/agent/agent" -import { InstanceRef } from "../../src/effect/instance-ref" -import { InstanceLayer } from "../../src/project/instance-layer" -import { InstanceStore } from "../../src/project/instance-store" -import { tmpdirScoped } from "../fixture/fixture" +import { Plugin } from "../../src/plugin" import { testEffect } from "../lib/effect" +import { PLUGIN_AGENT } from "../fixture/agent-plugin.constants" -const pluginAgent = { - name: "plugin_added", - description: "Added by a plugin via the config hook", - mode: "subagent", -} as const +// `it.instance` skips InstanceBootstrap so FileWatcher / LSP / MCP don't spin +// up — those services hang during scope teardown on Windows and aren't needed +// to verify plugin → config hook → Agent.list. +const pluginUrl = pathToFileURL(path.join(import.meta.dir, "..", "fixture", "agent-plugin.ts")).href -const it = testEffect(Layer.mergeAll(Agent.defaultLayer, InstanceLayer.layer, CrossSpawnSpawner.defaultLayer)) +const it = testEffect(Layer.mergeAll(Agent.defaultLayer, Plugin.defaultLayer)) -it.live("plugin-registered agents appear in Agent.list", () => - Effect.gen(function* () { - const dir = yield* tmpdirScoped() - const pluginFile = path.join(dir, "plugin.ts") - - yield* Effect.promise(async () => { - await Promise.all([ - Bun.write( - pluginFile, - [ - "export default async () => ({", - " config: async (cfg) => {", - " cfg.agent = cfg.agent ?? {}", - ` cfg.agent[${JSON.stringify(pluginAgent.name)}] = {`, - ` description: ${JSON.stringify(pluginAgent.description)},`, - ` mode: ${JSON.stringify(pluginAgent.mode)},`, - " }", - " },", - "})", - "", - ].join("\n"), - ), - Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - plugin: [pathToFileURL(pluginFile).href], - }), - ), - ]) - }) - - const agents = yield* InstanceStore.Service.use((store) => - Effect.gen(function* () { - const ctx = yield* store.load({ directory: dir }) - yield* Effect.addFinalizer(() => store.dispose(ctx).pipe(Effect.ignore)) - return yield* Agent.Service.use((svc) => svc.list()).pipe(Effect.provideService(InstanceRef, ctx)) - }), - ) - const added = agents.find((agent) => agent.name === pluginAgent.name) - - expect(added?.description).toBe(pluginAgent.description) - expect(added?.mode).toBe(pluginAgent.mode) - }), +it.instance( + "plugin-registered agents appear in Agent.list", + () => + Effect.gen(function* () { + yield* Plugin.Service.use((p) => p.init()) + const agents = yield* Agent.Service.use((svc) => svc.list()) + const added = agents.find((agent) => agent.name === PLUGIN_AGENT.name) + expect(added?.description).toBe(PLUGIN_AGENT.description) + expect(added?.mode).toBe(PLUGIN_AGENT.mode) + }), + { config: { plugin: [pluginUrl] } }, ) diff --git a/packages/opencode/test/fixture/agent-plugin.constants.ts b/packages/opencode/test/fixture/agent-plugin.constants.ts new file mode 100644 index 0000000000..9dd5f3910e --- /dev/null +++ b/packages/opencode/test/fixture/agent-plugin.constants.ts @@ -0,0 +1,6 @@ +// Separate file because every export in `agent-plugin.ts` must be a function. +export const PLUGIN_AGENT = { + name: "plugin_added", + description: "Added by a plugin via the config hook", + mode: "subagent", +} as const diff --git a/packages/opencode/test/fixture/agent-plugin.ts b/packages/opencode/test/fixture/agent-plugin.ts new file mode 100644 index 0000000000..892f636466 --- /dev/null +++ b/packages/opencode/test/fixture/agent-plugin.ts @@ -0,0 +1,12 @@ +// Every export in this file must be a plugin function — `getLegacyPlugins` +// (src/plugin/index.ts) throws on anything else. Test constants live in +// `agent-plugin.constants.ts`. +export default async () => ({ + config: async (cfg: { agent?: Record }) => { + cfg.agent = cfg.agent ?? {} + cfg.agent["plugin_added"] = { + description: "Added by a plugin via the config hook", + mode: "subagent", + } + }, +}) From 5720883d5d8b2e823cb7a6c81350973f7b7f0b79 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 May 2026 15:51:29 -0400 Subject: [PATCH 11/27] sync --- packages/console/app/src/routes/zen/util/handler.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 8bab495b72..7f36246ee5 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -919,6 +919,13 @@ export async function handler( "tokens.cache_read": cacheReadTokens, "tokens.cache_write_5m": cacheWrite5mTokens, "tokens.cache_write_1h": cacheWrite1hTokens, + "cost.input.microcents": centsToMicroCents(inputCost), + "cost.output.microcents": centsToMicroCents(outputCost), + "cost.reasoning.microcents": reasoningCost ? centsToMicroCents(reasoningCost) : undefined, + "cost.cache_read.microcents": cacheReadCost ? centsToMicroCents(cacheReadCost) : undefined, + "cost.cache_write.microcents": cacheWrite5mCost ? centsToMicroCents(cacheWrite5mCost) : undefined, + "cost.total.microcents": centsToMicroCents(totalCostInCent), + // deprecated - remove after May 20, 2026 "cost.input": Math.round(inputCost), "cost.output": Math.round(outputCost), "cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined, From d431a0e4b47fbf586ad3d23390b3c5e36911fb37 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 4 May 2026 17:29:00 -0500 Subject: [PATCH 12/27] fix: ensure effect server middleware properly parses errors (#25717) --- .../instance/httpapi/middleware/error.ts | 58 +++++++++++++++++++ .../server/routes/instance/httpapi/server.ts | 2 + 2 files changed, 60 insertions(+) create mode 100644 packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts new file mode 100644 index 0000000000..6f3c33a647 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts @@ -0,0 +1,58 @@ +import { Provider } from "@/provider/provider" +import { Session } from "@/session/session" +import { NotFoundError } from "@/storage/storage" +import { iife } from "@/util/iife" +import { NamedError } from "@opencode-ai/core/util/error" +import * as Log from "@opencode-ai/core/util/log" +import { Cause, Effect } from "effect" +import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http" + +const log = Log.create({ service: "server" }) + +// Keep typed HttpApi failures on their declared error path; this boundary only replaces defect-only empty 500s. +export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) => + effect.pipe( + Effect.catchCause((cause) => { + const defect = cause.reasons.filter(Cause.isDieReason).find((reason) => { + if (HttpServerResponse.isHttpServerResponse(reason.defect)) return false + if (HttpServerError.isHttpServerError(reason.defect)) return false + if (HttpServerRespondable.isRespondable(reason.defect)) return false + return true + }) + if (!defect) return Effect.failCause(cause) + + const error = defect.defect + log.error("failed", { error, cause: Cause.pretty(cause) }) + + if (error instanceof NamedError) { + return Effect.succeed( + HttpServerResponse.jsonUnsafe(error.toObject(), { + status: iife(() => { + if (error instanceof NotFoundError) return 404 + if (error instanceof Provider.ModelNotFoundError) return 400 + if (error.name === "ProviderAuthValidationFailed") return 400 + if (error.name.startsWith("Worktree")) return 400 + return 500 + }), + }), + ) + } + if (error instanceof Session.BusyError) { + return Effect.succeed( + HttpServerResponse.jsonUnsafe(new NamedError.Unknown({ message: error.message }).toObject(), { + status: 400, + }), + ) + } + + return Effect.succeed( + HttpServerResponse.jsonUnsafe( + new NamedError.Unknown({ + message: error instanceof Error && error.stack ? error.stack : String(error), + }).toObject(), + { status: 500 }, + ), + ) + }), + ), +).layer diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index a3754c2e19..ef966036a9 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -73,6 +73,7 @@ import { workspaceRouterMiddleware, workspaceRoutingLayer } from "./middleware/w import { disposeMiddleware } from "./lifecycle" import { memoMap } from "@opencode-ai/core/effect/memo-map" import * as ServerBackend from "@/server/backend" +import { errorLayer } from "./middleware/error" export const context = Context.makeUnsafe(new Map()) @@ -144,6 +145,7 @@ const uiRoute = HttpRouter.use((router) => export function createRoutes(corsOptions?: CorsOptions) { return Layer.mergeAll(rootApiRoutes, eventApiRoutes, instanceRoutes, uiRoute).pipe( Layer.provide([ + errorLayer, cors(corsOptions), runtime, Account.defaultLayer, From 4b65b1e0532b6f6cab101f2aba0c26a318fb36d8 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 4 May 2026 23:26:02 +0000 Subject: [PATCH 13/27] sync release versions for v1.14.34 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 25068f3d9a..3cf2d9ce99 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -85,7 +85,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -119,7 +119,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -146,7 +146,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -170,7 +170,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -194,7 +194,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.14.33", + "version": "1.14.34", "bin": { "opencode": "./bin/opencode", }, @@ -228,7 +228,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -263,7 +263,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -309,7 +309,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -338,7 +338,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -354,7 +354,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.14.33", + "version": "1.14.34", "bin": { "opencode": "./bin/opencode", }, @@ -496,7 +496,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -531,7 +531,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "cross-spawn": "catalog:", }, @@ -546,7 +546,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -581,7 +581,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -630,7 +630,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 5f4d79e44f..ac9bfd5904 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.14.33", + "version": "1.14.34", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index cb5b4bf9a4..85e855c55f 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.14.33", + "version": "1.14.34", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index bfb7f7db8f..d5157a372c 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.14.33", + "version": "1.14.34", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index f6072bd379..0bb1265419 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.14.33", + "version": "1.14.34", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index d73a23e081..b685bb1aab 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.14.33", + "version": "1.14.34", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/core/package.json b/packages/core/package.json index 4ba8d1401b..5f3371b988 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.33", + "version": "1.14.34", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 7a26516a99..8a6fcf5786 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.14.33", + "version": "1.14.34", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 1327423e51..2ec5cd0594 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.14.33", + "version": "1.14.34", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 16e142b9cf..fe9de85848 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.14.33", + "version": "1.14.34", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index d9e71219f5..17b51a6257 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.14.33" +version = "1.14.34" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 1eb790cced..f9044078b7 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.14.33", + "version": "1.14.34", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index adb4a7db1b..08d3171510 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.33", + "version": "1.14.34", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d6bfdd844b..a8c17f19f4 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.14.33", + "version": "1.14.34", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index de69e685c5..b3e12fc253 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.14.33", + "version": "1.14.34", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 04b996aca7..8a2ba85b02 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.14.33", + "version": "1.14.34", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index cd210c4d61..0c86216238 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.14.33", + "version": "1.14.34", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index c346fe5e7e..8187602b09 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.14.33", + "version": "1.14.34", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 67617771f0..43f07930ef 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.14.33", + "version": "1.14.34", "publisher": "sst-dev", "repository": { "type": "git", From 6a5e329427458619749f9c83e5374b249f87322c Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 5 May 2026 10:34:06 +1000 Subject: [PATCH 14/27] fix(vcs): preserve batched patch boundaries (#25787) --- packages/opencode/src/project/vcs.ts | 4 ++- packages/opencode/test/project/vcs.test.ts | 27 ++++++++++++++ .../ui/src/components/session-diff.test.ts | 16 +++++++++ packages/ui/src/components/session-diff.ts | 35 ++++++++++--------- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index 28ac143eec..8b3bedbf5b 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -85,7 +85,9 @@ const fileFromPatchChunk = (chunk: string) => { } const splitGitPatch = (patch: Git.Patch) => { - const starts = [...patch.text.matchAll(/^diff --git /gm)].map((match) => match.index) + const starts = [...patch.text.matchAll(/(?:^|\n)diff --git /g)].map((match) => + match[0].startsWith("\n") ? match.index + 1 : match.index, + ) const chunks = starts.map((start, index) => patch.text.slice(start, starts[index + 1] ?? patch.text.length)) if (!patch.truncated) return chunks return chunks.slice(0, -1) diff --git a/packages/opencode/test/project/vcs.test.ts b/packages/opencode/test/project/vcs.test.ts index 53ff547ac1..06da6ccba1 100644 --- a/packages/opencode/test/project/vcs.test.ts +++ b/packages/opencode/test/project/vcs.test.ts @@ -1,5 +1,6 @@ import { $ } from "bun" import { afterEach, describe, expect, test } from "bun:test" +import { parsePatch } from "diff" import { Effect } from "effect" import fs from "fs/promises" import path from "path" @@ -288,6 +289,32 @@ describe("Vcs diff", () => { }) }) + test( + "diff('git') keeps carriage returns inside patch hunks", + async () => { + await using tmp = await tmpdir({ git: true }) + await fs.writeFile(path.join(tmp.path, "file.txt"), "keep\nsame\rdiff --git inside\ndelete\n", "utf-8") + await $`git add .`.cwd(tmp.path).quiet() + await $`git commit --no-gpg-sign -m "add file"`.cwd(tmp.path).quiet() + await fs.writeFile(path.join(tmp.path, "file.txt"), "keep\nadd\nsame\rdiff --git inside\n", "utf-8") + + await withVcsOnly(tmp.path, async () => { + const diff = await AppRuntime.runPromise( + Effect.gen(function* () { + const vcs = yield* Vcs.Service + return yield* vcs.diff("git") + }), + ) + const file = diff.find((item) => item.file === "file.txt") + + expect(file?.patch).toContain(" same\rdiff --git inside") + expect(file?.patch).toContain("-delete") + expect(() => parsePatch(file?.patch ?? "")).not.toThrow() + }) + }, + 20_000, + ) + test("diff('branch') returns changes against default branch", async () => { await using tmp = await tmpdir({ git: true }) await $`git branch -M main`.cwd(tmp.path).quiet() diff --git a/packages/ui/src/components/session-diff.test.ts b/packages/ui/src/components/session-diff.test.ts index 463a729778..edaa15b84b 100644 --- a/packages/ui/src/components/session-diff.test.ts +++ b/packages/ui/src/components/session-diff.test.ts @@ -34,4 +34,20 @@ describe("session diff", () => { expect(text(view, "deletions")).toBe("one\n") expect(text(view, "additions")).toBe("two\n") }) + + test("ignores malformed persisted patches", () => { + const diff = { + file: "a.ts", + patch: + "diff --git a/a.ts b/a.ts\nindex ff4ceb2..65a1de0 100644\n--- a/a.ts\n+++ b/a.ts\n@@ -1,3 +1,3 @@\n keep\n+add\n same\r", + additions: 1, + deletions: 1, + status: "modified" as const, + } + 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 a5fbdbc5c0..2da8c61a76 100644 --- a/packages/ui/src/components/session-diff.ts +++ b/packages/ui/src/components/session-diff.ts @@ -27,26 +27,29 @@ const cache = new Map() function patch(diff: ReviewDiff) { if (typeof diff.patch === "string") { - const [patch] = parsePatch(diff.patch) + try { + const [patch] = parsePatch(diff.patch) + const beforeLines = [] + const afterLines = [] - const beforeLines = [] - const afterLines = [] - - for (const hunk of patch.hunks) { - for (const line of hunk.lines) { - if (line.startsWith("-")) { - beforeLines.push(line.slice(1)) - } else if (line.startsWith("+")) { - afterLines.push(line.slice(1)) - } else { - // context line (starts with ' ') - beforeLines.push(line.slice(1)) - afterLines.push(line.slice(1)) + for (const hunk of patch.hunks) { + for (const line of hunk.lines) { + if (line.startsWith("-")) { + beforeLines.push(line.slice(1)) + } else if (line.startsWith("+")) { + afterLines.push(line.slice(1)) + } else { + // context line (starts with ' ') + beforeLines.push(line.slice(1)) + afterLines.push(line.slice(1)) + } } } - } - return { before: beforeLines.join("\n"), after: afterLines.join("\n"), patch: diff.patch } + return { before: beforeLines.join("\n"), after: afterLines.join("\n"), patch: diff.patch } + } catch { + return { before: "", after: "", patch: diff.patch } + } } return { before: "before" in diff && typeof diff.before === "string" ? diff.before : "", From f14784d5319c5fc4f6e298819d8112ee6aa5342c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 5 May 2026 00:35:18 +0000 Subject: [PATCH 15/27] chore: generate --- packages/opencode/test/project/vcs.test.ts | 42 ++++++++++------------ 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/opencode/test/project/vcs.test.ts b/packages/opencode/test/project/vcs.test.ts index 06da6ccba1..82eacfb6df 100644 --- a/packages/opencode/test/project/vcs.test.ts +++ b/packages/opencode/test/project/vcs.test.ts @@ -289,31 +289,27 @@ describe("Vcs diff", () => { }) }) - test( - "diff('git') keeps carriage returns inside patch hunks", - async () => { - await using tmp = await tmpdir({ git: true }) - await fs.writeFile(path.join(tmp.path, "file.txt"), "keep\nsame\rdiff --git inside\ndelete\n", "utf-8") - await $`git add .`.cwd(tmp.path).quiet() - await $`git commit --no-gpg-sign -m "add file"`.cwd(tmp.path).quiet() - await fs.writeFile(path.join(tmp.path, "file.txt"), "keep\nadd\nsame\rdiff --git inside\n", "utf-8") + test("diff('git') keeps carriage returns inside patch hunks", async () => { + await using tmp = await tmpdir({ git: true }) + await fs.writeFile(path.join(tmp.path, "file.txt"), "keep\nsame\rdiff --git inside\ndelete\n", "utf-8") + await $`git add .`.cwd(tmp.path).quiet() + await $`git commit --no-gpg-sign -m "add file"`.cwd(tmp.path).quiet() + await fs.writeFile(path.join(tmp.path, "file.txt"), "keep\nadd\nsame\rdiff --git inside\n", "utf-8") - await withVcsOnly(tmp.path, async () => { - const diff = await AppRuntime.runPromise( - Effect.gen(function* () { - const vcs = yield* Vcs.Service - return yield* vcs.diff("git") - }), - ) - const file = diff.find((item) => item.file === "file.txt") + await withVcsOnly(tmp.path, async () => { + const diff = await AppRuntime.runPromise( + Effect.gen(function* () { + const vcs = yield* Vcs.Service + return yield* vcs.diff("git") + }), + ) + const file = diff.find((item) => item.file === "file.txt") - expect(file?.patch).toContain(" same\rdiff --git inside") - expect(file?.patch).toContain("-delete") - expect(() => parsePatch(file?.patch ?? "")).not.toThrow() - }) - }, - 20_000, - ) + expect(file?.patch).toContain(" same\rdiff --git inside") + expect(file?.patch).toContain("-delete") + expect(() => parsePatch(file?.patch ?? "")).not.toThrow() + }) + }, 20_000) test("diff('branch') returns changes against default branch", async () => { await using tmp = await tmpdir({ git: true }) From 6b852774e18c2bdabfd8754d3e1c506c7db76bff Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 5 May 2026 01:01:47 +0000 Subject: [PATCH 16/27] sync release versions for v1.14.35 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 3cf2d9ce99..07415dd79f 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -85,7 +85,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -119,7 +119,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -146,7 +146,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -170,7 +170,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -194,7 +194,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.14.34", + "version": "1.14.35", "bin": { "opencode": "./bin/opencode", }, @@ -228,7 +228,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -263,7 +263,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -309,7 +309,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -338,7 +338,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -354,7 +354,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.14.34", + "version": "1.14.35", "bin": { "opencode": "./bin/opencode", }, @@ -496,7 +496,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -531,7 +531,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "cross-spawn": "catalog:", }, @@ -546,7 +546,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -581,7 +581,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -630,7 +630,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index ac9bfd5904..cde4986d18 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.14.34", + "version": "1.14.35", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 85e855c55f..fb2e71d22d 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.14.34", + "version": "1.14.35", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index d5157a372c..7301b23e5c 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.14.34", + "version": "1.14.35", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 0bb1265419..06fb0affd0 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.14.34", + "version": "1.14.35", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index b685bb1aab..674fc55fd5 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.14.34", + "version": "1.14.35", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/core/package.json b/packages/core/package.json index 5f3371b988..e90ab7628a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.34", + "version": "1.14.35", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 8a6fcf5786..ba981e637a 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.14.34", + "version": "1.14.35", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 2ec5cd0594..e60320300a 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.14.34", + "version": "1.14.35", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index fe9de85848..dce25e204d 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.14.34", + "version": "1.14.35", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 17b51a6257..775f826d4c 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.14.34" +version = "1.14.35" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.34/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.35/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index f9044078b7..1039677b52 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.14.34", + "version": "1.14.35", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 08d3171510..bafa532de7 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.34", + "version": "1.14.35", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index a8c17f19f4..661201d2d9 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.14.34", + "version": "1.14.35", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index b3e12fc253..bef0fee141 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.14.34", + "version": "1.14.35", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 8a2ba85b02..448df66401 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.14.34", + "version": "1.14.35", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 0c86216238..dcf52499d6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.14.34", + "version": "1.14.35", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 8187602b09..a243a47078 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.14.34", + "version": "1.14.35", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 43f07930ef..22d8adc54b 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.14.34", + "version": "1.14.35", "publisher": "sst-dev", "repository": { "type": "git", From ca2411d332f4f7a98f44aa974a1b9d992d27dc8f Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 5 May 2026 11:05:53 +1000 Subject: [PATCH 17/27] Run UI unit tests in CI (#25792) --- packages/ui/package.json | 2 ++ .../ui/src/components/session-diff.test.ts | 15 ++++++++ packages/ui/src/components/session-diff.ts | 34 +++++++++++++++---- turbo.json | 9 +++++ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index dcf52499d6..1bc70c15ab 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -25,6 +25,8 @@ }, "scripts": { "typecheck": "tsgo --noEmit", + "test": "bun test src", + "test:ci": "mkdir -p .artifacts/unit && bun test src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "dev": "vite", "generate:tailwind": "bun run script/tailwind.ts" }, diff --git a/packages/ui/src/components/session-diff.test.ts b/packages/ui/src/components/session-diff.test.ts index edaa15b84b..172fe8d6c2 100644 --- a/packages/ui/src/components/session-diff.test.ts +++ b/packages/ui/src/components/session-diff.test.ts @@ -19,6 +19,21 @@ describe("session diff", () => { expect(text(view, "additions")).toBe("one\nthree\n") }) + test("keeps missing final newlines from unified patches", () => { + const diff = { + file: "a.ts", + patch: + "Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n@@ -1,2 +1,2 @@\n one\n-two\n\\ No newline at end of file\n+three\n\\ No newline at end of file\n", + additions: 1, + deletions: 1, + status: "modified" as const, + } + const view = normalize(diff) + + expect(text(view, "deletions")).toBe("one\ntwo") + expect(text(view, "additions")).toBe("one\nthree") + }) + test("converts legacy content into a patch", () => { const diff = { file: "a.ts", diff --git a/packages/ui/src/components/session-diff.ts b/packages/ui/src/components/session-diff.ts index 2da8c61a76..bd6bed88d8 100644 --- a/packages/ui/src/components/session-diff.ts +++ b/packages/ui/src/components/session-diff.ts @@ -29,24 +29,44 @@ function patch(diff: ReviewDiff) { if (typeof diff.patch === "string") { try { const [patch] = parsePatch(diff.patch) - const beforeLines = [] - const afterLines = [] + const beforeLines: Array<{ text: string; newline: boolean }> = [] + const afterLines: Array<{ text: string; newline: boolean }> = [] + let previous: "-" | "+" | " " | undefined 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(line.slice(1)) + beforeLines.push({ text: line.slice(1), newline: true }) + previous = "-" } else if (line.startsWith("+")) { - afterLines.push(line.slice(1)) + afterLines.push({ text: line.slice(1), newline: true }) + previous = "+" } else { // context line (starts with ' ') - beforeLines.push(line.slice(1)) - afterLines.push(line.slice(1)) + beforeLines.push({ text: line.slice(1), newline: true }) + afterLines.push({ text: line.slice(1), newline: true }) + previous = " " } } } - return { before: beforeLines.join("\n"), after: afterLines.join("\n"), patch: diff.patch } + return { + before: beforeLines.map((line) => line.text + (line.newline ? "\n" : "")).join(""), + after: afterLines.map((line) => line.text + (line.newline ? "\n" : "")).join(""), + patch: diff.patch, + } } catch { return { before: "", after: "", patch: diff.patch } } diff --git a/turbo.json b/turbo.json index 28c2fa2de0..0183fabca4 100644 --- a/turbo.json +++ b/turbo.json @@ -26,6 +26,15 @@ "dependsOn": ["^build"], "outputs": [".artifacts/unit/junit.xml"], "passThroughEnv": ["*"] + }, + "@opencode-ai/ui#test": { + "dependsOn": ["^build"], + "outputs": [] + }, + "@opencode-ai/ui#test:ci": { + "dependsOn": ["^build"], + "outputs": [".artifacts/unit/junit.xml"], + "passThroughEnv": ["*"] } } } From 84afd2bef8d114b41a6cb9b38074ea5cb4c6d4f9 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Tue, 5 May 2026 09:19:13 +0800 Subject: [PATCH 18/27] update: normalize download asset names to match new naming convention (#25796) --- .../app/src/routes/download/[channel]/[platform].ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/console/app/src/routes/download/[channel]/[platform].ts b/packages/console/app/src/routes/download/[channel]/[platform].ts index b486acb99d..4ae8e2465f 100644 --- a/packages/console/app/src/routes/download/[channel]/[platform].ts +++ b/packages/console/app/src/routes/download/[channel]/[platform].ts @@ -2,11 +2,11 @@ import type { APIEvent } from "@solidjs/start" import type { DownloadPlatform } from "../types" const prodAssetNames: Record = { - "darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg", - "darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg", - "windows-x64-nsis": "opencode-desktop-windows-x64.exe", + "darwin-aarch64-dmg": "opencode-desktop-mac-arm64.dmg", + "darwin-x64-dmg": "opencode-desktop-mac-x64.dmg", + "windows-x64-nsis": "opencode-desktop-win-x64.exe", "linux-x64-deb": "opencode-desktop-linux-amd64.deb", - "linux-x64-appimage": "opencode-desktop-linux-amd64.AppImage", + "linux-x64-appimage": "opencode-desktop-linux-x86_64.AppImage", "linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm", } satisfies Record From 22a4a9df8b98f998f526df983393df885388d569 Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 4 May 2026 21:28:38 -0400 Subject: [PATCH 19/27] feat(core): session warping (#25768) --- .../migration.sql | 1 + .../snapshot.json | 1429 +++++++++++++++++ packages/opencode/script/httpapi-exercise.ts | 4 +- .../cmd/tui/component/dialog-session-list.tsx | 120 +- .../tui/component/dialog-workspace-create.tsx | 288 ++-- .../cli/cmd/tui/component/prompt/index.tsx | 357 ++-- .../cli/cmd/tui/component/workspace-label.tsx | 19 + .../cli/cmd/tui/routes/session/sidebar.tsx | 29 +- .../src/cli/cmd/tui/ui/dialog-select.tsx | 53 +- .../opencode/src/control-plane/workspace.ts | 281 ++-- .../src/server/routes/control/workspace.ts | 74 +- .../routes/instance/httpapi/groups/sync.ts | 16 + .../instance/httpapi/groups/workspace.ts | 29 +- .../routes/instance/httpapi/handlers/sync.ts | 24 +- .../instance/httpapi/handlers/workspace.ts | 15 +- .../src/server/routes/instance/index.ts | 2 +- .../src/server/routes/instance/sync.ts | 47 + packages/opencode/src/sync/event.sql.ts | 1 + packages/opencode/src/sync/index.ts | 34 +- .../test/control-plane/workspace.test.ts | 506 ++---- .../test/server/httpapi-workspace.test.ts | 19 +- packages/opencode/test/sync/index.test.ts | 73 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 266 ++- packages/sdk/js/src/v2/gen/types.gen.ts | 631 +++++++- packages/sdk/openapi.json | 809 +++++++++- 25 files changed, 4032 insertions(+), 1095 deletions(-) create mode 100644 packages/opencode/migration/20260504145000_add_sync_owner/migration.sql create mode 100644 packages/opencode/migration/20260504145000_add_sync_owner/snapshot.json create mode 100644 packages/opencode/src/cli/cmd/tui/component/workspace-label.tsx diff --git a/packages/opencode/migration/20260504145000_add_sync_owner/migration.sql b/packages/opencode/migration/20260504145000_add_sync_owner/migration.sql new file mode 100644 index 0000000000..3bdf2b85e9 --- /dev/null +++ b/packages/opencode/migration/20260504145000_add_sync_owner/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `event_sequence` ADD `owner_id` text; \ No newline at end of file diff --git a/packages/opencode/migration/20260504145000_add_sync_owner/snapshot.json b/packages/opencode/migration/20260504145000_add_sync_owner/snapshot.json new file mode 100644 index 0000000000..4f6ebe00c0 --- /dev/null +++ b/packages/opencode/migration/20260504145000_add_sync_owner/snapshot.json @@ -0,0 +1,1429 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "27114226-085b-421a-9a40-29b88747e29a", + "prevIds": ["aaa2ebeb-caa4-478d-8365-4fc595d16856"], + "ddl": [ + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session_entry", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_entry" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_entry" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_entry" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_entry" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_entry" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_entry" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "columns": ["active_account_id"], + "tableTo": "account", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": ["message_id"], + "tableTo": "message", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_entry_session_id_session_id_fk", + "entityType": "fks", + "table": "session_entry" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": ["aggregate_id"], + "tableTo": "event_sequence", + "columnsTo": ["aggregate_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": ["email", "url"], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": ["session_id", "position"], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": ["project_id"], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_entry_pk", + "table": "session_entry", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["session_id"], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": ["aggregate_id"], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_entry_session_idx", + "entityType": "indexes", + "table": "session_entry" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_entry_session_type_idx", + "entityType": "indexes", + "table": "session_entry" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_entry_time_created_idx", + "entityType": "indexes", + "table": "session_entry" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} diff --git a/packages/opencode/script/httpapi-exercise.ts b/packages/opencode/script/httpapi-exercise.ts index 9755cf4017..771e1e417e 100644 --- a/packages/opencode/script/httpapi-exercise.ts +++ b/packages/opencode/script/httpapi-exercise.ts @@ -776,9 +776,9 @@ const scenarios: Scenario[] = [ })) .status(200), http - .post("/experimental/workspace/{id}/session-restore", "experimental.workspace.sessionRestore") + .post("/experimental/workspace/warp", "experimental.workspace.warp") .at((ctx) => ({ - path: route("/experimental/workspace/{id}/session-restore", { id: "wrk_httpapi_missing" }), + path: "/experimental/workspace/warp", headers: ctx.headers(), body: {}, })) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 04c6b9945c..09d952ef81 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -2,7 +2,7 @@ import { useDialog } from "@tui/ui/dialog" import { DialogSelect } from "@tui/ui/dialog-select" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" -import { createMemo, createResource, createSignal, onMount } from "solid-js" +import { createMemo, createResource, createSignal, onMount, type JSX } from "solid-js" import { Locale } from "@/util/locale" import { useProject } from "@tui/context/project" import { useKeybind } from "../context/keybind" @@ -10,15 +10,13 @@ import { useTheme } from "../context/theme" import { useSDK } from "../context/sdk" import { Flag } from "@opencode-ai/core/flag/flag" import { DialogSessionRename } from "./dialog-session-rename" -import { Keybind } from "@/util/keybind" import { createDebouncedSignal } from "../util/signal" import { useToast } from "../ui/toast" -import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create" +import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create" import { Spinner } from "./spinner" import { errorMessage } from "@/util/error" import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed" - -type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error" +import { WorkspaceLabel } from "./workspace-label" export function DialogSessionList() { const dialog = useDialog() @@ -44,26 +42,39 @@ export function DialogSessionList() { const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) const sessions = createMemo(() => searchResults() ?? sync.data.session) - function createWorkspace() { - dialog.replace(() => ( - - openWorkspaceSession({ - dialog, - route, - sdk, - sync, - toast, - workspaceID, - }) - } - /> - )) - } - function recover(session: NonNullable[number]>) { const workspace = project.workspace.get(session.workspaceID!) const list = () => dialog.replace(() => ) + const warp = async (selection: WorkspaceSelection) => { + const workspaceID = await (async () => { + if (selection.type === "none") return null + if (selection.type === "existing") return selection.workspaceID + const result = await sdk.client.experimental.workspace + .create({ type: selection.workspaceType, branch: null }) + .catch(() => undefined) + const workspace = result?.data + if (!workspace) { + toast.show({ + message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`, + variant: "error", + }) + return + } + await project.workspace.sync() + return workspace.id + })() + if (workspaceID === undefined) return + await warpWorkspaceSession({ + dialog, + sdk, + sync, + project, + toast, + workspaceID, + sessionID: session.id, + done: list, + }) + } dialog.replace(() => ( { - dialog.replace(() => ( - - restoreWorkspaceSession({ - dialog, - sdk, - sync, - project, - toast, - workspaceID, - sessionID: session.id, - done: list, - }) - } - /> - )) + void openWorkspaceSelect({ + dialog, + sdk, + sync, + toast, + onSelect: (selection) => { + void warp(selection) + }, + }) return false }} /> @@ -124,30 +128,17 @@ export function DialogSessionList() { .map((x) => { const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined - let workspaceStatus: WorkspaceStatus | null = null - if (x.workspaceID) { - workspaceStatus = project.workspace.status(x.workspaceID) || "error" - } - - let footer = "" + let footer: JSX.Element | string = "" if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { if (x.workspaceID) { - let desc = "unknown" - if (workspace) { - desc = `${workspace.type}: ${workspace.name}` - } - - footer = ( - <> - {desc}{" "} - - ● - - + footer = workspace ? ( + + ) : ( + ) } } else { @@ -250,15 +241,6 @@ export function DialogSessionList() { dialog.replace(() => ) }, }, - { - keybind: Keybind.parse("ctrl+w")[0], - title: "new workspace", - side: "right", - disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES, - onTrigger: () => { - createWorkspace() - }, - }, ]} /> ) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index 0aa61c313a..e2af0d63e1 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -1,11 +1,9 @@ -import { createOpencodeClient } from "@opencode-ai/sdk/v2" +import type { Workspace } from "@opencode-ai/sdk/v2" import { useDialog } from "@tui/ui/dialog" -import { DialogSelect } from "@tui/ui/dialog-select" -import { useRoute } from "@tui/context/route" +import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import { useSync } from "@tui/context/sync" import { useProject } from "@tui/context/project" import { createMemo, createSignal, onMount } from "solid-js" -import { setTimeout as sleep } from "node:timers/promises" import { errorMessage } from "@/util/error" import { useSDK } from "../context/sdk" import { useToast } from "../ui/toast" @@ -16,184 +14,212 @@ type Adapter = { description: string } -function scoped(sdk: ReturnType, sync: ReturnType, workspaceID: string) { - return createOpencodeClient({ - baseUrl: sdk.url, - fetch: sdk.fetch, - directory: sync.path.directory || sdk.directory, - experimental_workspaceID: workspaceID, - }) -} +export type WorkspaceSelection = + | { + type: "none" + } + | { + type: "new" + workspaceType: string + workspaceName: string + } + | { + type: "existing" + workspaceID: string + workspaceType: string + workspaceName: string + } -export async function openWorkspaceSession(input: { - dialog: ReturnType - route: ReturnType +type WorkspaceSelectValue = WorkspaceSelection | { type: "existing-list" } +type ExistingWorkspaceSelectValue = { workspace: Workspace } + +async function loadWorkspaceAdapters(input: { sdk: ReturnType sync: ReturnType toast: ReturnType - workspaceID: string }) { - const client = scoped(input.sdk, input.sync, input.workspaceID) - - while (true) { - const result = await client.session.create({ workspace: input.workspaceID }).catch(() => undefined) - if (!result) { - input.toast.show({ - message: "Failed to create workspace session", - variant: "error", - }) - return - } - if (result.response?.status && result.response.status >= 500 && result.response.status < 600) { - await sleep(1000) - continue - } - if (!result.data) { - input.toast.show({ - message: "Failed to create workspace session", - variant: "error", - }) - return - } - - input.route.navigate({ - type: "session", - sessionID: result.data.id, - }) - input.dialog.clear() - return - } + const dir = input.sync.path.directory || input.sdk.directory + const url = new URL("/experimental/workspace/adapter", input.sdk.url) + if (dir) url.searchParams.set("directory", dir) + const res = await input.sdk + .fetch(url) + .then((x) => x.json() as Promise) + .catch(() => undefined) + if (res) return res + input.toast.show({ + message: "Failed to load workspace adapters", + variant: "error", + }) } -export async function restoreWorkspaceSession(input: { +export async function openWorkspaceSelect(input: { + dialog: ReturnType + sdk: ReturnType + sync: ReturnType + toast: ReturnType + onSelect: (selection: WorkspaceSelection) => Promise | void +}) { + input.dialog.clear() + const adapters = await loadWorkspaceAdapters(input) + if (!adapters) return + input.dialog.replace(() => ) +} + +export async function warpWorkspaceSession(input: { dialog: ReturnType sdk: ReturnType sync: ReturnType project: ReturnType toast: ReturnType - workspaceID: string + workspaceID: string | null sessionID: string done?: () => void -}) { +}): Promise { const result = await input.sdk.client.experimental.workspace - .sessionRestore({ id: input.workspaceID, sessionID: input.sessionID }) + .warp({ + id: input.workspaceID, + sessionID: input.sessionID, + }) .catch(() => undefined) if (!result?.data) { input.toast.show({ - message: `Failed to restore session: ${errorMessage(result?.error ?? "no response")}`, + message: `Failed to warp session: ${errorMessage(result?.error ?? "no response")}`, variant: "error", }) - return + return false } input.project.workspace.set(input.workspaceID) await input.sync.bootstrap({ fatal: false }).catch(() => undefined) - await Promise.all([input.project.workspace.sync(), input.sync.session.sync(input.sessionID)]) + await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]) - input.toast.show({ - message: "Session restored into the new workspace", - variant: "success", - }) input.done?.() - if (input.done) return + if (input.done) return true input.dialog.clear() + return true } -export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise | void }) { +export function DialogWorkspaceSelect(props: { + adapters?: Adapter[] + onSelect: (selection: WorkspaceSelection) => Promise | void +}) { const dialog = useDialog() - const sync = useSync() const project = useProject() + const sync = useSync() const sdk = useSDK() const toast = useToast() - const [creating, setCreating] = createSignal() - const [adapters, setAdapters] = createSignal() + const [adapters, setAdapters] = createSignal(props.adapters) onMount(() => { dialog.setSize("medium") void (async () => { - const dir = sync.path.directory || sdk.directory - const url = new URL("/experimental/workspace/adapter", sdk.url) - if (dir) url.searchParams.set("directory", dir) - const res = await sdk - .fetch(url) - .then((x) => x.json() as Promise) - .catch(() => undefined) - if (!res) { - toast.show({ - message: "Failed to load workspace adapters", - variant: "error", - }) - return - } + if (adapters()) return + const res = await loadWorkspaceAdapters({ sdk, sync, toast }) + if (!res) return setAdapters(res) })() }) - const options = createMemo(() => { - const type = creating() - if (type) { - return [ - { - title: `Creating ${type} workspace...`, - value: "creating" as const, - description: "This can take a while for remote environments", - }, - ] - } + const options = createMemo[]>(() => { const list = adapters() - if (!list) { - return [ - { - title: "Loading workspaces...", - value: "loading" as const, - description: "Fetching available workspace adapters", + if (!list) return [] + const recent = sync.data.session + .toSorted((a, b) => b.time.updated - a.time.updated) + .flatMap((session) => (session.workspaceID ? [session.workspaceID] : [])) + .filter((workspaceID, index, list) => list.indexOf(workspaceID) === index) + .slice(0, 3) + .flatMap((workspaceID) => { + const workspace = project.workspace.get(workspaceID) + return workspace ? [workspace] : [] + }) + return [ + ...list.map((adapter) => ({ + title: adapter.name, + value: { type: "new" as const, workspaceType: adapter.type, workspaceName: adapter.name }, + description: adapter.description, + category: "New workspace", + })), + { + title: "None", + value: { type: "none" as const }, + description: "Use the local project", + category: "Choose workspace", + }, + ...recent.map((workspace: Workspace) => ({ + title: workspace.name, + description: `(${workspace.type})`, + value: { + type: "existing" as const, + workspaceID: workspace.id, + workspaceType: workspace.type, + workspaceName: workspace.name, }, - ] - } - return list.map((item) => ({ - title: item.name, - value: item.type, - description: item.description, - })) + category: "Choose workspace", + })), + { + title: "View all workspaces", + value: { type: "existing-list" as const }, + description: "Choose from all workspaces", + category: "Choose workspace", + }, + ] }) - const create = async (type: string) => { - if (creating()) return - setCreating(type) - - const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch(() => { - toast.show({ - message: "Creating workspace failed", - variant: "error", - }) - return undefined - }) - - const workspace = result?.data - if (!workspace) { - setCreating(undefined) - toast.show({ - message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`, - variant: "error", - }) - return - } - - await project.workspace.sync() - await props.onSelect(workspace.id) - setCreating(undefined) - } - + if (!adapters()) return null return ( - + title="Warp" skipFilter={true} + renderFilter={false} options={options()} onSelect={(option) => { - if (option.value === "creating" || option.value === "loading") return - void create(option.value) + if (!option.value) return + if (option.value.type === "none") { + void props.onSelect(option.value) + return + } + if (option.value.type === "new") { + void props.onSelect(option.value) + return + } + if (option.value.type === "existing") { + void props.onSelect(option.value) + return + } + + dialog.replace(() => ) + }} + /> + ) +} + +function DialogExistingWorkspaceSelect(props: { onSelect: (selection: WorkspaceSelection) => Promise | void }) { + const project = useProject() + + const options = createMemo[]>(() => + project.workspace + .list() + .filter((workspace) => project.workspace.status(workspace.id) === "connected") + .map((workspace: Workspace) => ({ + title: workspace.name, + description: `(${workspace.type})`, + value: { workspace }, + })), + ) + + return ( + + title="Existing Workspace" + options={options()} + onSelect={(option) => { + void props.onSelect({ + type: "existing", + workspaceID: option.value.workspace.id, + workspaceType: option.value.workspace.type, + workspaceName: option.value.workspace.name, + }) }} /> ) 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 a6ba797f33..74332c77be 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -7,6 +7,7 @@ import { Filesystem } from "@/util/filesystem" import { useLocal } from "@tui/context/local" import { tint, useTheme } from "@tui/context/theme" import { EmptyBorder, SplitBorder } from "@tui/component/border" +import { Spinner } from "@tui/component/spinner" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" import { useProject } from "@tui/context/project" @@ -41,9 +42,11 @@ import { useKV } from "../../context/kv" import { createFadeIn } from "../../util/signal" import { useTextareaKeybindings } from "../textarea-keybindings" import { DialogSkill } from "../dialog-skill" -import { DialogWorkspaceCreate, restoreWorkspaceSession } from "../dialog-workspace-create" +import { openWorkspaceSelect, warpWorkspaceSession, type WorkspaceSelection } from "../dialog-workspace-create" import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable" import { useArgs } from "@tui/context/args" +import { Flag } from "@opencode-ai/core/flag/flag" +import { WorkspaceLabel, type WorkspaceStatus } from "../workspace-label" export type PromptProps = { sessionID?: string @@ -173,9 +176,92 @@ export function Prompt(props: PromptProps) { const [editorContextHover, setEditorContextHover] = createSignal(false) let lastSubmittedEditorSelectionKey: string | undefined const [auto, setAuto] = createSignal() + const [workspaceSelection, setWorkspaceSelection] = createSignal() + const [workspaceCreating, setWorkspaceCreating] = createSignal(false) + const [workspaceCreatingDots, setWorkspaceCreatingDots] = createSignal(3) + const [warpNotice, setWarpNotice] = createSignal() const currentProviderLabel = createMemo(() => local.model.parsed().provider) const hasRightContent = createMemo(() => Boolean(props.right)) + function selectWorkspace(selection: WorkspaceSelection | undefined) { + setWorkspaceSelection(selection) + } + + function setCreatingWorkspace(creating: boolean) { + setWorkspaceCreating(creating) + } + + function showWarpNotice(name: string) { + setWarpNotice(`Warped to ${name}`) + setTimeout(() => setWarpNotice(undefined), 4000) + } + + async function createWorkspace(selection: Extract) { + setCreatingWorkspace(true) + const result = await sdk.client.experimental.workspace + .create({ type: selection.workspaceType, branch: null }) + .catch(() => undefined) + if (result == undefined || result.error || !result.data) { + selectWorkspace(undefined) + setCreatingWorkspace(false) + toast.show({ + message: "Creating workspace failed", + variant: "error", + }) + return + } + + await project.workspace.sync() + const workspace = result.data + selectWorkspace({ + type: "existing", + workspaceID: workspace.id, + workspaceType: workspace.type, + workspaceName: workspace.name, + }) + setCreatingWorkspace(false) + return workspace + } + + async function warpSession(selection: WorkspaceSelection) { + if (!props.sessionID) { + selectWorkspace(selection) + dialog.clear() + if (selection.type === "new") void createWorkspace(selection) + return + } + selectWorkspace(selection) + dialog.clear() + + const workspace = + selection.type === "none" + ? { id: null, name: "local project" } + : selection.type === "existing" + ? { id: selection.workspaceID, name: selection.workspaceName } + : await createWorkspace(selection) + if (!workspace) return + + const warped = await warpWorkspaceSession({ + dialog, + sdk, + sync, + project, + toast, + workspaceID: workspace.id, + sessionID: props.sessionID, + }) + if (warped) showWarpNotice(workspace.name) + } + + createEffect(() => { + if (!workspaceCreating()) { + setWorkspaceCreatingDots(3) + return + } + const timer = setInterval(() => setWorkspaceCreatingDots((dots) => (dots % 3) + 1), 1000) + onCleanup(() => clearInterval(timer)) + }) + function promptModelWarning() { toast.show({ variant: "warning", @@ -213,6 +299,7 @@ export function Prompt(props: PromptProps) { }) createEffect(() => { + if (!input || input.isDestroyed) return if (props.disabled) input.cursorColor = theme.backgroundElement if (!props.disabled) input.cursorColor = theme.text }) @@ -489,6 +576,27 @@ export function Prompt(props: PromptProps) { )) }, }, + { + title: "Warp", + description: "Change the workspace for the session", + value: "workspace.set", + category: "Session", + enabled: Flag.OPENCODE_EXPERIMENTAL_WORKSPACES, + slash: { + name: "warp", + }, + onSelect: (dialog) => { + void openWorkspaceSelect({ + dialog, + sdk, + sync, + toast, + onSelect: (selection) => { + void warpSession(selection) + }, + }) + }, + }, ] }) @@ -699,6 +807,8 @@ export function Prompt(props: PromptProps) { ]) async function submit() { + setWarpNotice(undefined) + // IME: double-defer may fire before onContentChange flushes the last // composed character (e.g. Korean hangul) to the store, so read // plainText directly and sync before any downstream reads. @@ -707,6 +817,7 @@ export function Prompt(props: PromptProps) { syncExtmarksWithPromptParts() } if (props.disabled) return false + if (workspaceCreating()) return false if (autocomplete?.visible) return false if (!store.prompt.input) return false const agent = local.agent.current() @@ -729,21 +840,16 @@ export function Prompt(props: PromptProps) { dialog.replace(() => ( { - dialog.replace(() => ( - - restoreWorkspaceSession({ - dialog, - sdk, - sync, - project, - toast, - workspaceID: nextWorkspaceID, - sessionID: props.sessionID!, - }) - } - /> - )) + void openWorkspaceSelect({ + dialog, + sdk, + sync, + toast, + onSelect: (selection) => { + void warpSession(selection) + }, + }) + return false }} /> )) @@ -753,6 +859,14 @@ export function Prompt(props: PromptProps) { const variant = local.model.variant.current() let sessionID = props.sessionID if (sessionID == null) { + const workspace = workspaceSelection() + const workspaceID = iife(() => { + if (!workspace) return undefined + if (workspace.type === "none") return undefined + if (workspace.type === "existing") return workspace.workspaceID + return undefined + }) + const res = await sdk.client.session.create({ workspace: props.workspaceID, agent: agent.name, @@ -1025,6 +1139,29 @@ export function Prompt(props: PromptProps) { return `Ask anything... "${list()[store.placeholder % list().length]}"` }) + const workspaceLabel = createMemo< + | { type: "new"; workspaceType: string } + | { type: "existing"; workspaceType: string; workspaceName: string; status?: WorkspaceStatus } + | undefined + >(() => { + const selected = workspaceSelection() + if (!selected) return + if (selected.type === "none") return + if (props.sessionID && !workspaceCreating()) return + if (selected.type === "new") { + return { + type: "new", + workspaceType: selected.workspaceType, + } + } + return { + type: "existing", + workspaceType: selected.workspaceType, + workspaceName: selected.workspaceName, + status: selected.type === "existing" ? "connected" : undefined, + } + }) + const spinnerDef = createMemo(() => { const agent = local.agent.current() const color = agent ? local.agent.color(agent.name) : theme.border @@ -1281,7 +1418,7 @@ export function Prompt(props: PromptProps) { }} onMouseDown={(r: MouseEvent) => r.target?.focus()} focusedBackgroundColor={theme.backgroundElement} - cursorColor={theme.text} + cursorColor={props.disabled ? theme.backgroundElement : theme.text} syntaxStyle={syntax()} /> @@ -1351,86 +1488,124 @@ export function Prompt(props: PromptProps) { /> - }> - - - - [⋯]}> - - - - - {(() => { - const retry = createMemo(() => { - const s = status() - if (s.type !== "retry") return - return s - }) - const message = createMemo(() => { - const r = retry() - if (!r) return - if (r.message.includes("exceeded your current quota") && r.message.includes("gemini")) - return "gemini is way too hot right now" - if (r.message.length > 80) return r.message.slice(0, 80) + "..." - return r.message - }) - const isTruncated = createMemo(() => { - const r = retry() - if (!r) return false - return r.message.length > 120 - }) - const [seconds, setSeconds] = createSignal(0) - onMount(() => { - const timer = setInterval(() => { - const next = retry()?.next - if (next) setSeconds(Math.round((next - Date.now()) / 1000)) - }, 1000) - - onCleanup(() => { - clearInterval(timer) + + + + + + [⋯]}> + + + + + {(() => { + const retry = createMemo(() => { + const s = status() + if (s.type !== "retry") return + return s }) - }) - const handleMessageClick = () => { - const r = retry() - if (!r) return - if (isTruncated()) { - void DialogAlert.show(dialog, "Retry Error", r.message) + const message = createMemo(() => { + const r = retry() + if (!r) return + if (r.message.includes("exceeded your current quota") && r.message.includes("gemini")) + return "gemini is way too hot right now" + if (r.message.length > 80) return r.message.slice(0, 80) + "..." + return r.message + }) + const isTruncated = createMemo(() => { + const r = retry() + if (!r) return false + return r.message.length > 120 + }) + const [seconds, setSeconds] = createSignal(0) + onMount(() => { + const timer = setInterval(() => { + const next = retry()?.next + if (next) setSeconds(Math.round((next - Date.now()) / 1000)) + }, 1000) + + onCleanup(() => { + clearInterval(timer) + }) + }) + const handleMessageClick = () => { + const r = retry() + if (!r) return + if (isTruncated()) { + void DialogAlert.show(dialog, "Retry Error", r.message) + } } - } - const retryText = () => { - const r = retry() - if (!r) return "" - const baseMessage = message() - const truncatedHint = isTruncated() ? " (click to expand)" : "" - const duration = formatDuration(seconds()) - const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]` - return baseMessage + truncatedHint + retryInfo - } + const retryText = () => { + const r = retry() + if (!r) return "" + const baseMessage = message() + const truncatedHint = isTruncated() ? " (click to expand)" : "" + const duration = formatDuration(seconds()) + const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]` + return baseMessage + truncatedHint + retryInfo + } - return ( - - - {retryText()} - - - ) - })()} + return ( + + + {retryText()} + + + ) + })()} + + 0 ? theme.primary : theme.text}> + esc{" "} + 0 ? theme.primary : theme.textMuted }}> + {store.interrupt > 0 ? "again to interrupt" : "interrupt"} + + - 0 ? theme.primary : theme.text}> - esc{" "} - 0 ? theme.primary : theme.textMuted }}> - {store.interrupt > 0 ? "again to interrupt" : "interrupt"} - - - - + + + {(notice) => ( + + {notice()} + + )} + + + {(workspace) => ( + + + + + + {(() => { + const item = workspace() + if (item.type === "new") { + if (workspaceCreating()) + return `Creating ${item.workspaceType}${".".repeat(workspaceCreatingDots())}` + return ( + <> + Workspace (new {item.workspaceType}) + + ) + } + return ( + <> + Workspace {item.workspaceName} + + ) + })()} + + + )} + + {props.hint ?? } + diff --git a/packages/opencode/src/cli/cmd/tui/component/workspace-label.tsx b/packages/opencode/src/cli/cmd/tui/component/workspace-label.tsx new file mode 100644 index 0000000000..efdbf71587 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/workspace-label.tsx @@ -0,0 +1,19 @@ +import { useTheme } from "@tui/context/theme" + +export type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error" + +export function WorkspaceLabel(props: { type: string; name: string; status?: WorkspaceStatus; icon?: boolean }) { + const { theme } = useTheme() + const color = () => { + if (props.status === "connected") return theme.success + if (props.status === "error") return theme.error + return theme.textMuted + } + + return ( + <> + {props.icon ? : undefined} + {props.name} ({props.type}) + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 7adc4c1db1..0f9214092e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -7,6 +7,7 @@ import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/inst import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime" import { getScrollAcceleration } from "../../util/scroll" +import { WorkspaceLabel } from "../../component/workspace-label" export function Sidebar(props: { sessionID: string; overlay?: boolean }) { const project = useProject() @@ -14,17 +15,10 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { const { theme } = useTheme() const tuiConfig = useTuiConfig() const session = createMemo(() => sync.session.get(props.sessionID)) - const workspaceStatus = () => { + const workspace = () => { const workspaceID = session()?.workspaceID - if (!workspaceID) return "error" - return project.workspace.status(workspaceID) ?? "error" - } - const workspaceLabel = () => { - const workspaceID = session()?.workspaceID - if (!workspaceID) return "unknown" - const info = project.workspace.get(workspaceID) - if (!info) return "unknown" - return `${info.type}: ${info.name}` + if (!workspaceID) return + return project.workspace.get(workspaceID) } const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig)) @@ -67,8 +61,19 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { - {" "} - {workspaceLabel()} + } + > + {(item) => ( + + )} + diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 4d68c44308..ef7d4bd3bb 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -23,6 +23,7 @@ export interface DialogSelectProps { onFilter?: (query: string) => void onSelect?: (option: DialogSelectOption) => void skipFilter?: boolean + renderFilter?: boolean keybind?: { keybind?: Keybind.Info title: string @@ -81,7 +82,7 @@ export function DialogSelect(props: DialogSelectProps) { let input: InputRenderable const filtered = createMemo(() => { - if (props.skipFilter) return props.options.filter((x) => x.disabled !== true) + if (props.skipFilter || props.renderFilter === false) return props.options.filter((x) => x.disabled !== true) const needle = store.filter.toLowerCase() const options = pipe( props.options, @@ -250,30 +251,32 @@ export function DialogSelect(props: DialogSelectProps) { esc - - { - batch(() => { - setStore("filter", e) - props.onFilter?.(e) - }) - }} - focusedBackgroundColor={theme.backgroundPanel} - cursorColor={theme.primary} - focusedTextColor={theme.textMuted} - ref={(r) => { - input = r - input.traits = { status: "FILTER" } - setTimeout(() => { - if (!input) return - if (input.isDestroyed) return - input.focus() - }, 1) - }} - placeholder={props.placeholder ?? "Search"} - placeholderColor={theme.textMuted} - /> - + + + { + batch(() => { + setStore("filter", e) + props.onFilter?.(e) + }) + }} + focusedBackgroundColor={theme.backgroundPanel} + cursorColor={theme.primary} + focusedTextColor={theme.textMuted} + ref={(r) => { + input = r + input.traits = { status: "FILTER" } + setTimeout(() => { + if (!input) return + if (input.isDestroyed) return + input.focus() + }, 1) + }} + placeholder={props.placeholder ?? "Search"} + placeholderColor={theme.textMuted} + /> + + 0} diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 485cb2e925..fe651fe3e3 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -1,10 +1,11 @@ -import { Context, Effect, FiberMap, Layer, Schema, Stream } from "effect" +import { Context, Effect, FiberMap, Iterable, Layer, Schema, Stream } from "effect" import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http" import { Database } from "@/storage/db" import { asc } from "drizzle-orm" import { eq } from "drizzle-orm" import { inArray } from "drizzle-orm" import { Project } from "@/project/project" +import { Instance } from "@/project/instance" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { Auth } from "@/auth" @@ -20,6 +21,7 @@ import { getAdapter } from "./adapters" import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types" import { WorkspaceID } from "./schema" import { Session } from "@/session/session" +import { SessionPrompt } from "@/session/prompt" import { SessionTable } from "@/session/session.sql" import { SessionID } from "@/session/schema" import { errorData } from "@/util/error" @@ -38,13 +40,6 @@ export const ConnectionStatus = Schema.Struct({ }) export type ConnectionStatus = Schema.Schema.Type -const Restore = Schema.Struct({ - workspaceID: WorkspaceID, - sessionID: SessionID, - total: NonNegativeInt, - step: NonNegativeInt, -}) - export const Event = { Ready: BusEvent.define( "workspace.ready", @@ -58,7 +53,6 @@ export const Event = { message: Schema.String, }), ), - Restore: BusEvent.define("workspace.restore", Restore), Status: BusEvent.define("workspace.status", ConnectionStatus), } @@ -84,15 +78,15 @@ export const CreateInput = Schema.Struct({ type: Info.fields.type, branch: Info.fields.branch, projectID: ProjectID, - extra: Info.fields.extra, + extra: Schema.optional(Info.fields.extra), }).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) }))) export type CreateInput = Schema.Schema.Type -export const SessionRestoreInput = Schema.Struct({ - workspaceID: WorkspaceID, +export const SessionWarpInput = Schema.Struct({ + workspaceID: Schema.NullOr(WorkspaceID), sessionID: SessionID, }).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) }))) -export type SessionRestoreInput = Schema.Schema.Type +export type SessionWarpInput = Schema.Schema.Type export class SyncHttpError extends Schema.TaggedErrorClass()("WorkspaceSyncHttpError", { message: Schema.String, @@ -116,8 +110,8 @@ export class SessionEventsNotFoundError extends Schema.TaggedErrorClass()( - "WorkspaceSessionRestoreHttpError", +export class SessionWarpHttpError extends Schema.TaggedErrorClass()( + "WorkspaceSessionWarpHttpError", { message: Schema.String, workspaceID: WorkspaceID, @@ -138,17 +132,17 @@ export class SyncAbortedError extends Schema.TaggedErrorClass( }) {} type CreateError = Auth.AuthError -type SessionRestoreError = +type SessionWarpError = | WorkspaceNotFoundError | SessionEventsNotFoundError - | SessionRestoreHttpError + | SessionWarpHttpError | HttpClientError.HttpClientError type WaitForSyncError = SyncTimeoutError | SyncAbortedError type SyncLoopError = SyncHttpError | HttpClientError.HttpClientError export interface Interface { readonly create: (input: CreateInput) => Effect.Effect - readonly sessionRestore: (input: SessionRestoreInput) => Effect.Effect<{ total: number }, SessionRestoreError> + readonly sessionWarp: (input: SessionWarpInput) => Effect.Effect readonly list: (project: Project.Info) => Effect.Effect readonly get: (id: WorkspaceID) => Effect.Effect readonly remove: (id: WorkspaceID) => Effect.Effect @@ -169,6 +163,7 @@ export const layer = Layer.effect( Effect.gen(function* () { const auth = yield* Auth.Service const session = yield* Session.Service + const prompt = yield* SessionPrompt.Service const http = yield* HttpClient.HttpClient const sync = yield* SyncEvent.Service const connections = new Map() @@ -461,7 +456,7 @@ export const layer = Layer.effect( const id = WorkspaceID.ascending(input.id) const adapter = getAdapter(input.projectID, input.type) const config = yield* EffectBridge.fromPromise(() => - adapter.configure({ ...input, id, name: Slug.create(), directory: null }), + adapter.configure({ ...input, id, name: Slug.create(), directory: null, extra: input.extra ?? null }), ) const info: Info = { @@ -518,29 +513,93 @@ export const layer = Layer.effect( return info }) - const sessionRestore = Effect.fn("Workspace.sessionRestore")(function* (input: SessionRestoreInput) { + const sessionWarp = Effect.fn("Workspace.sessionWarp")(function* (input: SessionWarpInput) { return yield* Effect.gen(function* () { - log.info("session restore requested", { + log.info("session warp requested", { workspaceID: input.workspaceID, sessionID: input.sessionID, }) - const space = yield* get(input.workspaceID) + const current = yield* db((db) => + db + .select({ workspaceID: SessionTable.workspace_id }) + .from(SessionTable) + .where(eq(SessionTable.id, input.sessionID)) + .get(), + ) + + if (current?.workspaceID) { + const previous = yield* get(current.workspaceID) + if (previous) { + const adapter = getAdapter(previous.projectID, previous.type) + const target = yield* EffectBridge.fromPromise(() => adapter.target(previous)) + + if (target.type === "remote") { + yield* syncHistory(previous, target.url, target.headers).pipe( + Effect.catch((error) => + Effect.sync(() => { + log.warn("session warp final source sync failed", { + workspaceID: previous.id, + sessionID: input.sessionID, + error: errorData(error), + }) + }), + ), + ) + } else { + yield* prompt.cancel(input.sessionID) + } + + // "claim" this session so any future events coming from + // the old workspace are ignored + SyncEvent.claim(input.sessionID, input.workspaceID ?? Instance.project.id) + } + } + + if (input.workspaceID === null) { + yield* Effect.sync(() => + SyncEvent.run(Session.Event.Updated, { + sessionID: input.sessionID, + info: { + workspaceID: null, + }, + }), + ) + + log.info("session warp complete", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + target: "local", + }) + return + } + + const workspaceID = input.workspaceID + const space = yield* get(workspaceID) if (!space) return yield* new WorkspaceNotFoundError({ - message: `Workspace not found: ${input.workspaceID}`, - workspaceID: input.workspaceID, + message: `Workspace not found: ${workspaceID}`, + workspaceID, }) const adapter = getAdapter(space.projectID, space.type) const target = yield* EffectBridge.fromPromise(() => adapter.target(space)) - yield* sync.run(Session.Event.Updated, { - sessionID: input.sessionID, - info: { + if (target.type === "local") { + yield* sync.run(Session.Event.Updated, { + sessionID: input.sessionID, + info: { + workspaceID: input.workspaceID, + }, + }) + + log.info("session warp complete", { workspaceID: input.workspaceID, - }, - }) + sessionID: input.sessionID, + target: target.directory, + }) + return + } const rows = yield* db((db) => db @@ -562,130 +621,95 @@ export const layer = Layer.effect( sessionID: input.sessionID, }) - const size = 10 - // TODO: look into using effect APIs to process this in chunks - const sets = Array.from({ length: Math.ceil(rows.length / size) }, (_, i) => - rows.slice(i * size, (i + 1) * size), - ) - const total = sets.length + const batches = Iterable.chunksOf(rows, 10) + const total = Iterable.size(batches) - log.info("session restore prepared", { + log.info("session warp prepared", { workspaceID: input.workspaceID, sessionID: input.sessionID, - workspaceType: space.type, - directory: space.directory, - target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, + target: String(route(target.url, "/sync/replay")), events: rows.length, batches: total, first: rows[0]?.seq, last: rows.at(-1)?.seq, }) - yield* Effect.sync(() => - GlobalBus.emit("event", { - directory: "global", - workspace: input.workspaceID, - payload: { - type: Event.Restore.type, - properties: { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - total, - step: 0, - }, - }, - }), - ) - - for (const [i, events] of sets.entries()) { - log.info("session restore batch starting", { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - step: i + 1, - total, - events: events.length, - first: events[0]?.seq, - last: events.at(-1)?.seq, - target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, - }) - - if (target.type === "local") { - yield* sync.replayAll(events) - log.info("session restore batch replayed locally", { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - step: i + 1, - total, - events: events.length, - }) - } else { - const url = route(target.url, "/sync/replay") - const res = yield* http.execute( - HttpClientRequest.post(url, { - headers: new Headers(target.headers), - body: HttpBody.jsonUnsafe({ - directory: space.directory ?? "", - events, + yield* Effect.forEach( + batches, + (events, i) => + Effect.gen(function* () { + const response = yield* http.execute( + HttpClientRequest.post(route(target.url, "/sync/replay"), { + headers: new Headers(target.headers), + body: HttpBody.jsonUnsafe({ + directory: space.directory ?? "", + events, + }), }), - }), - ) + ) - if (res.status < 200 || res.status >= 300) { - const body = yield* res.text - log.error("session restore batch failed", { + if (response.status < 200 || response.status >= 300) { + const body = yield* response.text + log.error("session warp batch failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + status: response.status, + body, + }) + return yield* new SessionWarpHttpError({ + message: `Failed to warp session ${input.sessionID} into workspace ${workspaceID}: HTTP ${response.status} ${body}`, + workspaceID, + sessionID: input.sessionID, + status: response.status, + body, + }) + } + + log.info("session warp batch posted", { workspaceID: input.workspaceID, sessionID: input.sessionID, step: i + 1, total, - status: res.status, - body, + status: response.status, }) - return yield* new SessionRestoreHttpError({ - message: `Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`, - workspaceID: input.workspaceID, - sessionID: input.sessionID, - status: res.status, - body, - }) - } - - log.info("session restore batch posted", { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - step: i + 1, - total, - status: res.status, - }) - } - - yield* Effect.sync(() => - GlobalBus.emit("event", { - directory: "global", - workspace: input.workspaceID, - payload: { - type: Event.Restore.type, - properties: { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - total, - step: i + 1, - }, - }, }), - ) + { discard: true }, + ) + + const response = yield* http.execute( + HttpClientRequest.post(route(target.url, "/sync/steal"), { + headers: new Headers(target.headers), + body: HttpBody.jsonUnsafe({ sessionID: input.sessionID }), + }), + ) + if (response.status < 200 || response.status >= 300) { + const body = yield* response.text + log.error("session warp steal failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + status: response.status, + body, + }) + return yield* new SessionWarpHttpError({ + message: `Failed to steal session ${input.sessionID} into workspace ${workspaceID}: HTTP ${response.status} ${body}`, + workspaceID, + sessionID: input.sessionID, + status: response.status, + body, + }) } - log.info("session restore complete", { + log.info("session warp complete", { workspaceID: input.workspaceID, sessionID: input.sessionID, batches: total, }) - - return { total } }).pipe( Effect.tapError((err) => Effect.sync(() => - log.error("session restore failed", { + log.error("session warp failed", { workspaceID: input.workspaceID, sessionID: input.sessionID, error: errorData(err), @@ -814,7 +838,7 @@ export const layer = Layer.effect( return Service.of({ create, - sessionRestore, + sessionWarp, list, get, remove, @@ -830,6 +854,7 @@ export const defaultLayer = layer.pipe( Layer.provide(Auth.defaultLayer), Layer.provide(Session.defaultLayer), Layer.provide(SyncEvent.defaultLayer), + Layer.provide(SessionPrompt.defaultLayer), Layer.provide(FetchHttpClient.layer), ) diff --git a/packages/opencode/src/server/routes/control/workspace.ts b/packages/opencode/src/server/routes/control/workspace.ts index 21a7810ce1..788aef3176 100644 --- a/packages/opencode/src/server/routes/control/workspace.ts +++ b/packages/opencode/src/server/routes/control/workspace.ts @@ -10,10 +10,6 @@ import { zodObject } from "@/util/effect-zod" import { Instance } from "@/project/instance" import { errors } from "../../error" import { lazy } from "@/util/lazy" -import * as Log from "@opencode-ai/core/util/log" -import { errorData } from "@/util/error" - -const log = Log.create({ service: "server.workspace" }) export const WorkspaceRoutes = lazy(() => new Hono() @@ -151,60 +147,36 @@ export const WorkspaceRoutes = lazy(() => }, ) .post( - "/:id/session-restore", + "/warp", describeRoute({ - summary: "Restore session into workspace", - description: "Replay a session's sync events into the target workspace in batches.", - operationId: "experimental.workspace.sessionRestore", + summary: "Warp session into workspace", + description: "Move a session's sync history into the target workspace, or detach it to the local project.", + operationId: "experimental.workspace.warp", responses: { - 200: { - description: "Session replay started", - content: { - "application/json": { - schema: resolver( - z.object({ - total: z.number().int().min(0), - }), - ), - }, - }, + 204: { + description: "Session warped", }, ...errors(400), }, }), - validator("param", z.object({ id: zodObject(Workspace.Info).shape.id })), - validator("json", Workspace.SessionRestoreInput.zodObject.omit({ workspaceID: true })), + validator( + "json", + z.object({ + id: zodObject(Workspace.Info).shape.id.nullable(), + sessionID: Workspace.SessionWarpInput.zodObject.shape.sessionID, + }), + ), async (c) => { - const { id } = c.req.valid("param") - const body = c.req.valid("json") as Omit - log.info("session restore route requested", { - workspaceID: id, - sessionID: body.sessionID, - directory: Instance.directory, - }) - try { - const result = await AppRuntime.runPromise( - Workspace.Service.use((svc) => - svc.sessionRestore({ - workspaceID: id, - ...body, - }), - ), - ) - log.info("session restore route complete", { - workspaceID: id, - sessionID: body.sessionID, - total: result.total, - }) - return c.json(result) - } catch (err) { - log.error("session restore route failed", { - workspaceID: id, - sessionID: body.sessionID, - error: errorData(err), - }) - throw err - } + const body = c.req.valid("json") + await AppRuntime.runPromise( + Workspace.Service.use((workspace) => + workspace.sessionWarp({ + workspaceID: body.id, + sessionID: body.sessionID, + }), + ), + ) + return c.body(null, 204) }, ), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts index 58d30b4c78..442e656554 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts @@ -1,4 +1,5 @@ import { NonNegativeInt } from "@/util/schema" +import { SessionID } from "@/session/schema" import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../middleware/authorization" @@ -21,6 +22,9 @@ export const ReplayPayload = Schema.Struct({ export const ReplayResponse = Schema.Struct({ sessionID: Schema.String, }) +export const SessionPayload = Schema.Struct({ + sessionID: SessionID, +}) export const HistoryPayload = Schema.Record(Schema.String, NonNegativeInt) export const HistoryEvent = Schema.Struct({ id: Schema.String, @@ -33,6 +37,7 @@ export const HistoryEvent = Schema.Struct({ export const SyncPaths = { start: `${root}/start`, replay: `${root}/replay`, + steal: `${root}/steal`, history: `${root}/history`, } as const @@ -60,6 +65,17 @@ export const SyncApi = HttpApi.make("sync") description: "Validate and replay a complete sync event history.", }), ), + HttpApiEndpoint.post("steal", SyncPaths.steal, { + payload: SessionPayload, + success: described(SessionPayload, "Session stolen into workspace"), + error: HttpApiError.BadRequest, + }).annotateMerge( + OpenApi.annotations({ + identifier: "sync.steal", + summary: "Steal session into workspace", + description: "Update a session to belong to the current workspace through the sync event system.", + }), + ), HttpApiEndpoint.post("history", SyncPaths.history, { payload: HistoryPayload, success: described(Schema.Array(HistoryEvent), "Sync events"), diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts index 08e9e044bb..f197ab9765 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts @@ -1,21 +1,17 @@ import { Workspace } from "@/control-plane/workspace" import { WorkspaceAdapterEntry } from "@/control-plane/types" -import { NonNegativeInt } from "@/util/schema" import { Schema, Struct } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../middleware/authorization" import { InstanceContextMiddleware } from "../middleware/instance-context" import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { described } from "./metadata" const root = "/experimental/workspace" -export const CreatePayload = Schema.Struct({ - ...Struct.omit(Workspace.CreateInput.fields, ["projectID", "extra"]), - extra: Schema.optional(Workspace.CreateInput.fields.extra), -}) -export const SessionRestorePayload = Schema.Struct(Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"])) -export const SessionRestoreResponse = Schema.Struct({ - total: NonNegativeInt, +export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"])) +export const WarpPayload = Schema.Struct({ + id: Schema.NullOr(Workspace.Info.fields.id), + sessionID: Workspace.SessionWarpInput.fields.sessionID, }) export const WorkspacePaths = { @@ -23,7 +19,7 @@ export const WorkspacePaths = { list: root, status: `${root}/status`, remove: `${root}/:id`, - sessionRestore: `${root}/:id/session-restore`, + warp: `${root}/warp`, } as const export const WorkspaceApi = HttpApi.make("workspace") @@ -79,16 +75,15 @@ export const WorkspaceApi = HttpApi.make("workspace") description: "Remove an existing workspace.", }), ), - HttpApiEndpoint.post("sessionRestore", WorkspacePaths.sessionRestore, { - params: { id: Workspace.Info.fields.id }, - payload: SessionRestorePayload, - success: described(SessionRestoreResponse, "Session replay started"), + HttpApiEndpoint.post("warp", WorkspacePaths.warp, { + payload: WarpPayload, + success: described(HttpApiSchema.NoContent, "Session warped"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ - identifier: "experimental.workspace.sessionRestore", - summary: "Restore session into workspace", - description: "Replay a session's sync events into the target workspace in batches.", + identifier: "experimental.workspace.warp", + summary: "Warp session into workspace", + description: "Move a session's sync history into the target workspace, or detach it to the local project.", }), ), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts index f4a2f315cd..152d22f98e 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts @@ -1,5 +1,6 @@ import { Workspace } from "@/control-plane/workspace" import * as InstanceState from "@/effect/instance-state" +import { Session } from "@/session/session" import { Database } from "@/storage/db" import { SyncEvent } from "@/sync" import { EventTable } from "@/sync/event.sql" @@ -12,7 +13,7 @@ import { or } from "drizzle-orm" import { Effect, Scope } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" -import { HistoryPayload, ReplayPayload } from "../groups/sync" +import { HistoryPayload, ReplayPayload, SessionPayload } from "../groups/sync" import * as Log from "@opencode-ai/core/util/log" const log = Log.create({ service: "server.sync" }) @@ -56,6 +57,25 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl return { sessionID: source } }) + const steal = Effect.fn("SyncHttpApi.steal")(function* (ctx: { payload: typeof SessionPayload.Type }) { + const workspaceID = yield* InstanceState.workspaceID + if (!workspaceID) throw new Error("Cannot steal session without workspace context") + + yield* sync.run(Session.Event.Updated, { + sessionID: ctx.payload.sessionID, + info: { + workspaceID, + }, + }) + + log.info("sync session stolen", { + sessionID: ctx.payload.sessionID, + workspaceID, + }) + + return { sessionID: ctx.payload.sessionID } + }) + const history = Effect.fn("SyncHttpApi.history")(function* (ctx: { payload: typeof HistoryPayload.Type }) { const exclude = Object.entries(ctx.payload) return Database.use((db) => @@ -72,6 +92,6 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl ) }) - return handlers.handle("start", start).handle("replay", replay).handle("history", history) + return handlers.handle("start", start).handle("replay", replay).handle("steal", steal).handle("history", history) }), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts index 570f355e57..b415943a62 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts @@ -4,7 +4,7 @@ import * as InstanceState from "@/effect/instance-state" import { Effect } from "effect" import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" -import { CreatePayload, SessionRestorePayload } from "../groups/workspace" +import { CreatePayload, WarpPayload } from "../groups/workspace" export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) => Effect.gen(function* () { @@ -39,13 +39,10 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac return yield* workspace.remove(ctx.params.id) }) - const sessionRestore = Effect.fn("WorkspaceHttpApi.sessionRestore")(function* (ctx: { - params: { id: Workspace.Info["id"] } - payload: typeof SessionRestorePayload.Type - }) { - return yield* workspace - .sessionRestore({ - workspaceID: ctx.params.id, + const warp = Effect.fn("WorkspaceHttpApi.warp")(function* (ctx: { payload: typeof WarpPayload.Type }) { + yield* workspace + .sessionWarp({ + workspaceID: ctx.payload.id, sessionID: ctx.payload.sessionID, }) .pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) @@ -57,6 +54,6 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac .handle("create", create) .handle("status", status) .handle("remove", remove) - .handle("sessionRestore", sessionRestore) + .handle("warp", warp) }), ) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 89b5641e58..71662dea90 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -155,7 +155,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, opts?: CorsOptions): H app.get(WorkspacePaths.list, (c) => handler(c.req.raw, context)) app.get(WorkspacePaths.status, (c) => handler(c.req.raw, context)) app.delete(WorkspacePaths.remove, (c) => handler(c.req.raw, context)) - app.post(WorkspacePaths.sessionRestore, (c) => handler(c.req.raw, context)) + app.post(WorkspacePaths.warp, (c) => handler(c.req.raw, context)) } return app diff --git a/packages/opencode/src/server/routes/instance/sync.ts b/packages/opencode/src/server/routes/instance/sync.ts index b7bf413d4e..9894d8c8ee 100644 --- a/packages/opencode/src/server/routes/instance/sync.ts +++ b/packages/opencode/src/server/routes/instance/sync.ts @@ -16,6 +16,9 @@ import { Workspace } from "@/control-plane/workspace" import { AppRuntime } from "@/effect/app-runtime" import { Instance } from "@/project/instance" import { errors } from "../../error" +import { Session } from "@/session/session" +import { WorkspaceContext } from "@/control-plane/workspace-context" +import { SessionID } from "@/session/schema" const ReplayEvent = z.object({ id: z.string(), @@ -24,6 +27,9 @@ const ReplayEvent = z.object({ type: z.string(), data: z.record(z.string(), z.unknown()), }) +const SessionPayload = z.object({ + sessionID: SessionID.zod, +}) const log = Log.create({ service: "server.sync" }) @@ -108,6 +114,47 @@ export const SyncRoutes = lazy(() => }) }, ) + .post( + "/steal", + describeRoute({ + summary: "Steal session into workspace", + description: "Update a session to belong to the current workspace through the sync event system.", + operationId: "sync.steal", + responses: { + 200: { + description: "Session stolen into workspace", + content: { + "application/json": { + schema: resolver(SessionPayload), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", SessionPayload), + async (c) => { + const body = c.req.valid("json") + const workspaceID = WorkspaceContext.workspaceID + if (!workspaceID) throw new Error("Cannot steal session without workspace context") + + SyncEvent.run(Session.Event.Updated, { + sessionID: body.sessionID, + info: { + workspaceID, + }, + }) + + log.info("sync session stolen", { + sessionID: body.sessionID, + workspaceID, + }) + + return c.json({ + sessionID: body.sessionID, + }) + }, + ) .post( "/history", describeRoute({ diff --git a/packages/opencode/src/sync/event.sql.ts b/packages/opencode/src/sync/event.sql.ts index b51b5a5dfe..547a80f0f3 100644 --- a/packages/opencode/src/sync/event.sql.ts +++ b/packages/opencode/src/sync/event.sql.ts @@ -3,6 +3,7 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" export const EventSequenceTable = sqliteTable("event_sequence", { aggregate_id: text().notNull().primaryKey(), seq: integer().notNull(), + owner_id: text(), }) export const EventTable = sqliteTable("event", { diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index 2654767e9a..62b30ccf9a 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -59,8 +59,11 @@ export interface Interface { data: Event["data"], options?: { publish?: boolean }, ) => Effect.Effect - readonly replay: (event: SerializedEvent, options?: { publish: boolean }) => Effect.Effect - readonly replayAll: (events: SerializedEvent[], options?: { publish: boolean }) => Effect.Effect + readonly replay: (event: SerializedEvent, options?: { publish: boolean; ownerID?: string }) => Effect.Effect + readonly replayAll: ( + events: SerializedEvent[], + options?: { publish: boolean; ownerID?: string }, + ) => Effect.Effect readonly remove: (aggregateID: string) => Effect.Effect } @@ -76,7 +79,7 @@ export const layer = Layer.effect(Service)( const row = Database.use((db) => db - .select({ seq: EventSequenceTable.seq }) + .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) .from(EventSequenceTable) .where(eq(EventSequenceTable.aggregate_id, event.aggregateID)) .get(), @@ -85,6 +88,10 @@ export const layer = Layer.effect(Service)( const latest = row?.seq ?? -1 if (event.seq <= latest) return + if (row?.ownerID && row.ownerID !== options?.ownerID) { + return + } + const expected = latest + 1 if (event.seq !== expected) { throw new Error( @@ -99,7 +106,7 @@ export const layer = Layer.effect(Service)( workspace: yield* InstanceState.workspaceID, } : undefined - process(def, event, { publish, context }) + process(def, event, { publish, context, ownerID: options?.ownerID }) }) const replayAll: Interface["replayAll"] = Effect.fn("SyncEvent.replayAll")(function* (events, options) { @@ -263,7 +270,7 @@ export function project( function process( def: Def, event: Event, - options: { publish: boolean; context?: PublishContext }, + options: { publish: boolean; context?: PublishContext; ownerID?: string }, ) { if (projectors == null) { throw new Error("No projectors available. Call `SyncEvent.init` to install projectors") @@ -274,8 +281,6 @@ function process( throw new Error(`Projector not found for event: ${def.type}`) } - // idempotent: need to ignore any events already logged - Database.transaction((tx) => { projector(tx, event.data, event) @@ -284,6 +289,7 @@ function process( .values({ aggregate_id: event.aggregateID, seq: event.seq, + owner_id: options?.ownerID, }) .onConflictDoUpdate({ target: EventSequenceTable.aggregate_id, @@ -332,11 +338,11 @@ function process( }) } -export function replay(event: SerializedEvent, options?: { publish: boolean }) { +export function replay(event: SerializedEvent, options?: { publish: boolean; ownerID?: string }) { return runtime.runSync((sync) => sync.replay(event, options)) } -export function replayAll(events: SerializedEvent[], options?: { publish: boolean }) { +export function replayAll(events: SerializedEvent[], options?: { publish: boolean; ownerID?: string }) { return runtime.runSync((sync) => sync.replayAll(events, options)) } @@ -348,6 +354,16 @@ export function remove(aggregateID: string) { return runtime.runSync((sync) => sync.remove(aggregateID)) } +export function claim(aggregateID: string, ownerID: string) { + Database.use((db) => + db + .update(EventSequenceTable) + .set({ owner_id: ownerID }) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .run(), + ) +} + export function payloads() { return registry .entries() diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index 10a05e3b1e..84f5670064 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -6,7 +6,7 @@ import { setTimeout as delay } from "node:timers/promises" import { NodeHttpServer } from "@effect/platform-node" import { Effect, Layer } from "effect" import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" -import { asc, eq } from "drizzle-orm" +import { eq } from "drizzle-orm" import * as Log from "@opencode-ai/core/util/log" import { Flag } from "@opencode-ai/core/flag/flag" import { GlobalBus, type GlobalEvent } from "@/bus/global" @@ -16,11 +16,10 @@ import { ProjectTable } from "@/project/project.sql" import { Instance } from "@/project/instance" import { WithInstance } from "../../src/project/with-instance" import { Session as SessionNs } from "@/session/session" -import { SessionID, MessageID, PartID } from "@/session/schema" +import { SessionID } from "@/session/schema" import { SessionTable } from "@/session/session.sql" -import { ModelID, ProviderID } from "@/provider/schema" import { SyncEvent } from "@/sync" -import { EventSequenceTable, EventTable } from "@/sync/event.sql" +import { EventSequenceTable } from "@/sync/event.sql" import { resetDatabase } from "../fixture/db" import { disposeAllInstances, provideTmpdirInstance, tmpdir } from "../fixture/fixture" import { testEffect } from "../lib/effect" @@ -111,8 +110,8 @@ async function withInstance(fn: (dir: string) => T | Promise) { const runWorkspace = (effect: Effect.Effect) => AppRuntime.runPromise(effect) const createWorkspace = (input: WorkspaceOld.CreateInput) => runWorkspace(WorkspaceOld.Service.use((workspace) => workspace.create(input))) -const restoreWorkspaceSession = (input: WorkspaceOld.SessionRestoreInput) => - runWorkspace(WorkspaceOld.Service.use((workspace) => workspace.sessionRestore(input))) +const warpWorkspaceSession = (input: WorkspaceOld.SessionWarpInput) => + runWorkspace(WorkspaceOld.Service.use((workspace) => workspace.sessionWarp(input))) const listWorkspaces = (project: Parameters[0]) => runWorkspace(WorkspaceOld.Service.use((workspace) => workspace.list(project))) const getWorkspace = (id: WorkspaceID) => runWorkspace(WorkspaceOld.Service.use((workspace) => workspace.get(id))) @@ -317,48 +316,24 @@ function sessionSequence(sessionID: SessionID) { )?.seq } -function eventRows(sessionID: SessionID) { +function sessionSequenceOwner(sessionID: SessionID) { return Database.use((db) => db - .select({ seq: EventTable.seq, type: EventTable.type, data: EventTable.data }) - .from(EventTable) - .where(eq(EventTable.aggregate_id, sessionID)) - .orderBy(asc(EventTable.seq)) - .all(), - ) + .select({ ownerID: EventSequenceTable.owner_id }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, sessionID)) + .get(), + )?.ownerID } function sessionUpdatedType() { return SyncEvent.versionedType(SessionNs.Event.Updated.type, SessionNs.Event.Updated.version) } -function replaceSessionEvents(sessionID: SessionID, count: number) { - Database.use((db) => { - db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, sessionID)).run() - if (count === 0) return - - db.insert(EventSequenceTable) - .values({ aggregate_id: sessionID, seq: count - 1 }) - .run() - db.insert(EventTable) - .values( - Array.from({ length: count }, (_, i) => ({ - id: `evt_${unique(`manual-${i}`)}`, - aggregate_id: sessionID, - seq: i, - type: sessionUpdatedType(), - data: { sessionID, info: { title: `manual ${i}` } }, - })), - ) - .run() - }) -} - describe("workspace-old schemas and exports", () => { test("keeps the historical event type names", () => { expect(WorkspaceOld.Event.Ready.type).toBe("workspace.ready") expect(WorkspaceOld.Event.Failed.type).toBe("workspace.failed") - expect(WorkspaceOld.Event.Restore.type).toBe("workspace.restore") expect(WorkspaceOld.Event.Status.type).toBe("workspace.status") }) @@ -375,17 +350,6 @@ describe("workspace-old schemas and exports", () => { expect(() => WorkspaceOld.CreateInput.zod.parse({ ...input, id: "bad" })).toThrow() expect(() => WorkspaceOld.CreateInput.zod.parse({ ...input, branch: 1 })).toThrow() }) - - test("validates session restore input", () => { - const input = { - workspaceID: WorkspaceID.ascending("wrk_schema_restore"), - sessionID: SessionID.descending("ses_schema_restore"), - } - - expect(WorkspaceOld.SessionRestoreInput.zod.parse(input)).toEqual(input) - expect(() => WorkspaceOld.SessionRestoreInput.zod.parse({ ...input, workspaceID: "bad" })).toThrow() - expect(() => WorkspaceOld.SessionRestoreInput.zod.parse({ ...input, sessionID: "bad" })).toThrow() - }) }) describe("workspace-old CRUD", () => { @@ -651,6 +615,144 @@ describe("workspace-old CRUD", () => { expect(await getWorkspace(info.id)).toBeUndefined() }) }) + + test("sessionWarp moves a session into a local workspace and claims ownership", async () => { + await withInstance(async (dir) => { + const previousType = unique("warp-prev-local") + const targetType = unique("warp-target-local") + const previous = workspaceInfo(Instance.project.id, previousType) + const target = workspaceInfo(Instance.project.id, targetType) + insertWorkspace(previous) + insertWorkspace(target) + registerAdapter(Instance.project.id, previousType, localAdapter(path.join(dir, "warp-prev-local")).adapter) + registerAdapter(Instance.project.id, targetType, localAdapter(path.join(dir, "warp-target-local")).adapter) + const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({}))) + attachSessionToWorkspace(session.id, previous.id) + + await warpWorkspaceSession({ workspaceID: target.id, sessionID: session.id }) + + expect( + Database.use((db) => + db + .select({ workspaceID: SessionTable.workspace_id }) + .from(SessionTable) + .where(eq(SessionTable.id, session.id)) + .get(), + )?.workspaceID, + ).toBe(target.id) + expect(sessionSequenceOwner(session.id)).toBe(target.id) + }) + }) + + test("sessionWarp detaches a session to the local project and claims project ownership", async () => { + await withInstance(async (dir) => { + const previousType = unique("warp-detach-local") + const previous = workspaceInfo(Instance.project.id, previousType) + insertWorkspace(previous) + registerAdapter(Instance.project.id, previousType, localAdapter(path.join(dir, "warp-detach-local")).adapter) + const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({}))) + attachSessionToWorkspace(session.id, previous.id) + + await warpWorkspaceSession({ workspaceID: null, sessionID: session.id }) + + expect( + Database.use((db) => + db + .select({ workspaceID: SessionTable.workspace_id }) + .from(SessionTable) + .where(eq(SessionTable.id, session.id)) + .get(), + )?.workspaceID, + ).toBeNull() + expect(sessionSequenceOwner(session.id)).toBe(Instance.project.id) + }) + }) + + it.live("sessionWarp syncs previous remote history, replays it, steals, and claims the sequence", () => { + const calls: FetchCall[] = [] + let historySessionID: SessionID | undefined + let historyNextSeq = 0 + return Effect.gen(function* () { + yield* HttpServer.serveEffect()( + Effect.gen(function* () { + const req = yield* HttpServerRequest.HttpServerRequest + const bodyText = yield* req.text + const call = { + url: new URL(req.url, "http://localhost"), + method: req.method, + headers: new Headers(req.headers), + bodyText, + json: bodyText ? JSON.parse(bodyText) : undefined, + } + calls.push(call) + if (call.url.pathname === "/warp-source/sync/history") { + return yield* HttpServerResponse.json([ + { + id: `evt_${unique("warp-source-history")}`, + aggregate_id: historySessionID!, + seq: historyNextSeq, + type: sessionUpdatedType(), + data: { sessionID: historySessionID!, info: { title: "from source history" } }, + }, + ]) + } + if (call.url.pathname === "/warp-target/sync/replay") + return yield* HttpServerResponse.json({ sessionID: "ok" }) + if (call.url.pathname === "/warp-target/sync/steal") + return yield* HttpServerResponse.json({ sessionID: "ok" }) + return HttpServerResponse.text("unexpected", { status: 500 }) + }), + ) + const url = yield* serverUrl() + yield* provideTmpdirInstance( + () => + Effect.gen(function* () { + const workspace = yield* WorkspaceOld.Service + const sessionSvc = yield* SessionNs.Service + const previousType = unique("warp-remote-source") + const targetType = unique("warp-remote-target") + const previous = workspaceInfo(Instance.project.id, previousType) + const target = workspaceInfo(Instance.project.id, targetType, { directory: "remote-target-dir" }) + insertWorkspace(previous) + insertWorkspace(target) + registerAdapter(Instance.project.id, previousType, remoteAdapter(`${url}/warp-source`).adapter) + registerAdapter(Instance.project.id, targetType, remoteAdapter(`${url}/warp-target`).adapter) + const session = yield* sessionSvc.create({}) + attachSessionToWorkspace(session.id, previous.id) + historySessionID = session.id + historyNextSeq = (sessionSequence(session.id) ?? -1) + 1 + + yield* workspace.sessionWarp({ workspaceID: target.id, sessionID: session.id }) + + expect(calls.map((call) => `${call.method} ${call.url.pathname}`)).toEqual([ + "POST /warp-source/sync/history", + "POST /warp-target/sync/replay", + "POST /warp-target/sync/steal", + ]) + expect(calls[0].json).toEqual({ [session.id]: historyNextSeq - 1 }) + expect(calls[1].json).toMatchObject({ + directory: "remote-target-dir", + events: [ + { + aggregateID: session.id, + seq: 0, + type: SyncEvent.versionedType(SessionNs.Event.Created.type, SessionNs.Event.Created.version), + }, + { + aggregateID: session.id, + seq: historyNextSeq, + type: sessionUpdatedType(), + }, + ], + }) + expect(calls[2].json).toEqual({ sessionID: session.id }) + expect((yield* sessionSvc.get(session.id)).title).toBe("from source history") + expect(sessionSequenceOwner(session.id)).toBe(target.id) + }), + { git: true }, + ) + }) + }) }) describe("workspace-old sync state", () => { @@ -1215,313 +1317,3 @@ describe("workspace-old waitForSync", () => { }) }, 7000) }) - -describe("workspace-old sessionRestore", () => { - test("throws when the workspace is missing", async () => { - await withInstance(async () => { - await expect( - restoreWorkspaceSession({ - workspaceID: WorkspaceID.ascending("wrk_restore_missing"), - sessionID: SessionID.descending("ses_restore_missing_workspace"), - }), - ).rejects.toThrow("Workspace not found: wrk_restore_missing") - }) - }) - - test("throws when switching a missing session fails", async () => { - await withInstance(async (dir) => { - const type = unique("restore-missing-session") - const info = workspaceInfo(Instance.project.id, type, { directory: dir }) - insertWorkspace(info) - registerAdapter(Instance.project.id, type, localAdapter(dir).adapter) - - await expect( - restoreWorkspaceSession({ workspaceID: info.id, sessionID: SessionID.descending("ses_missing_restore") }), - ).rejects.toThrow("NotFoundError") - await removeWorkspace(info.id) - }) - }) - - it.live("posts remote replay batches of 10, emits progress, and includes the workspace update event", () => { - const replay: FetchCall[] = [] - return Effect.gen(function* () { - yield* HttpServer.serveEffect()( - Effect.gen(function* () { - const req = yield* HttpServerRequest.HttpServerRequest - const bodyText = yield* req.text - const call = { - url: new URL(req.url, "http://localhost"), - method: req.method, - headers: new Headers(req.headers), - bodyText, - json: bodyText ? JSON.parse(bodyText) : undefined, - } - if (call.url.pathname === "/restore/sync/replay") { - replay.push(call) - return HttpServerResponse.fromWeb(Response.json({ ok: true })) - } - return HttpServerResponse.text("unexpected", { status: 500 }) - }), - ) - const url = yield* serverUrl() - yield* provideTmpdirInstance( - (dir) => - Effect.gen(function* () { - const workspace = yield* WorkspaceOld.Service - const sessionSvc = yield* SessionNs.Service - const captured = captureGlobalEvents() - try { - const type = unique("restore-remote") - const info = workspaceInfo(Instance.project.id, type, { directory: dir }) - insertWorkspace(info) - registerAdapter( - Instance.project.id, - type, - remoteAdapter(`${url}/restore/?ignored=1#hash`, { - directory: dir, - headers: { authorization: "Bearer restore" }, - }).adapter, - ) - const session = yield* sessionSvc.create({ title: "restore remote" }) - replaceSessionEvents(session.id, 24) - - const result = yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id }) - - expect(result).toEqual({ total: 3 }) - expect(replay).toHaveLength(3) - expect(replay.map((call) => call.url.pathname + call.url.search + call.url.hash)).toEqual([ - "/restore/sync/replay", - "/restore/sync/replay", - "/restore/sync/replay", - ]) - expect(replay.every((call) => call.headers.get("authorization") === "Bearer restore")).toBe(true) - expect(replay.every((call) => call.headers.get("content-type") === "application/json")).toBe(true) - expect(replay.map((call) => (call.json as { events: unknown[] }).events.length)).toEqual([10, 10, 5]) - expect(replay.map((call) => (call.json as { directory: string }).directory)).toEqual([dir, dir, dir]) - expect( - replay.flatMap((call) => - (call.json as { events: Array<{ seq: number }> }).events.map((event) => event.seq), - ), - ).toEqual(Array.from({ length: 25 }, (_, i) => i)) - expect( - (replay[2].json as { events: Array<{ seq: number; type: string; data: unknown }> }).events.at(-1), - ).toMatchObject({ - seq: 24, - type: sessionUpdatedType(), - data: { sessionID: session.id, info: { workspaceID: info.id } }, - }) - expect((yield* sessionSvc.get(session.id)).workspaceID).toBe(info.id) - expect( - captured.events - .filter( - (event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type, - ) - .map((event) => event.payload.properties.step), - ).toEqual([0, 1, 2, 3]) - yield* workspace.remove(info.id) - } finally { - captured.dispose() - } - }), - { git: true }, - ) - }) - }) - - it.live("remote restore sends an empty directory string when the workspace directory is null", () => { - const replay: FetchCall[] = [] - return Effect.gen(function* () { - yield* HttpServer.serveEffect()( - Effect.gen(function* () { - const req = yield* HttpServerRequest.HttpServerRequest - const bodyText = yield* req.text - replay.push({ - url: new URL(req.url, "http://localhost"), - method: req.method, - headers: new Headers(req.headers), - bodyText, - json: bodyText ? JSON.parse(bodyText) : undefined, - }) - return HttpServerResponse.fromWeb(Response.json({ ok: true })) - }), - ) - const url = yield* serverUrl() - yield* provideTmpdirInstance( - () => - Effect.gen(function* () { - const workspace = yield* WorkspaceOld.Service - const sessionSvc = yield* SessionNs.Service - const type = unique("restore-null-dir") - const info = workspaceInfo(Instance.project.id, type, { directory: null }) - insertWorkspace(info) - registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/null-dir`, { directory: null }).adapter) - const session = yield* sessionSvc.create({ title: "null dir" }) - replaceSessionEvents(session.id, 0) - - expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ - total: 1, - }) - expect((replay[0].json as { directory: string }).directory).toBe("") - expect((replay[0].json as { events: unknown[] }).events).toHaveLength(1) - yield* workspace.remove(info.id) - }), - { git: true }, - ) - }) - }) - - it.live("remote restore failures include status and body and do not emit completed batch progress", () => { - const replay: FetchCall[] = [] - return Effect.gen(function* () { - yield* HttpServer.serveEffect()( - Effect.gen(function* () { - const req = yield* HttpServerRequest.HttpServerRequest - const bodyText = yield* req.text - replay.push({ - url: new URL(req.url, "http://localhost"), - method: req.method, - headers: new Headers(req.headers), - bodyText, - json: bodyText ? JSON.parse(bodyText) : undefined, - }) - return HttpServerResponse.text("replay failed", { status: 503 }) - }), - ) - const url = yield* serverUrl() - yield* provideTmpdirInstance( - (dir) => - Effect.gen(function* () { - const workspace = yield* WorkspaceOld.Service - const sessionSvc = yield* SessionNs.Service - const captured = captureGlobalEvents() - try { - const type = unique("restore-remote-fail") - const info = workspaceInfo(Instance.project.id, type, { directory: dir }) - insertWorkspace(info) - registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/fail`, { directory: dir }).adapter) - const session = yield* sessionSvc.create({ title: "restore fail" }) - replaceSessionEvents(session.id, 11) - - const error = yield* Effect.flip( - workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id }), - ) - expect((error as Error).message).toContain( - `Failed to replay session ${session.id} into workspace ${info.id}: HTTP 503 replay failed`, - ) - - expect(replay).toHaveLength(1) - expect( - captured.events - .filter( - (event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type, - ) - .map((event) => event.payload.properties.step), - ).toEqual([0]) - yield* workspace.remove(info.id) - } finally { - captured.dispose() - } - }), - { git: true }, - ) - }) - }) - - it.live("local restore replays batches and emits progress", () => - provideTmpdirInstance( - (dir) => - Effect.gen(function* () { - const workspace = yield* WorkspaceOld.Service - const sessionSvc = yield* SessionNs.Service - const captured = captureGlobalEvents() - try { - const type = unique("restore-local") - const info = workspaceInfo(Instance.project.id, type, { directory: dir }) - insertWorkspace(info) - registerAdapter(Instance.project.id, type, localAdapter(dir).adapter) - const session = yield* sessionSvc.create({ title: "restore local" }) - replaceSessionEvents(session.id, 20) - - expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ - total: 3, - }) - expect((yield* sessionSvc.get(session.id)).workspaceID).toBe(info.id) - expect(eventRows(session.id).map((row) => row.seq)).toEqual(Array.from({ length: 21 }, (_, i) => i)) - expect( - captured.events - .filter( - (event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type, - ) - .map((event) => event.payload.properties.step), - ).toEqual([0, 1, 2, 3]) - yield* workspace.remove(info.id) - } finally { - captured.dispose() - } - }), - { git: true }, - ), - ) - - it.live("session restore includes real message and part events in sequence order", () => { - const replay: FetchCall[] = [] - return Effect.gen(function* () { - yield* HttpServer.serveEffect()( - Effect.gen(function* () { - const req = yield* HttpServerRequest.HttpServerRequest - const bodyText = yield* req.text - replay.push({ - url: new URL(req.url, "http://localhost"), - method: req.method, - headers: new Headers(req.headers), - bodyText, - json: bodyText ? JSON.parse(bodyText) : undefined, - }) - return HttpServerResponse.fromWeb(Response.json({ ok: true })) - }), - ) - const url = yield* serverUrl() - yield* provideTmpdirInstance( - (dir) => - Effect.gen(function* () { - const workspace = yield* WorkspaceOld.Service - const sessionSvc = yield* SessionNs.Service - const type = unique("restore-real-events") - const info = workspaceInfo(Instance.project.id, type, { directory: dir }) - insertWorkspace(info) - registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/real`, { directory: dir }).adapter) - const session = yield* sessionSvc.create({ title: "real events" }) - for (let i = 0; i < 3; i++) { - const msg = yield* sessionSvc.updateMessage({ - id: MessageID.ascending(), - role: "user", - sessionID: session.id, - agent: "build", - model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, - time: { created: Date.now() }, - }) - yield* sessionSvc.updatePart({ - id: PartID.ascending(), - sessionID: session.id, - messageID: msg.id, - type: "text", - text: `message ${i}`, - }) - } - const before = eventRows(session.id) - - expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ - total: 1, - }) - - const posted = (replay[0].json as { events: Array<{ seq: number; type: string }> }).events - expect(posted.map((event) => event.seq)).toEqual([...before.map((row) => row.seq), before.at(-1)!.seq + 1]) - expect(posted.map((event) => event.type).slice(0, -1)).toEqual(before.map((row) => row.type)) - expect(posted.at(-1)?.type).toBe(sessionUpdatedType()) - yield* workspace.remove(info.id) - }), - { git: true }, - ) - }) - }) -}) diff --git a/packages/opencode/test/server/httpapi-workspace.test.ts b/packages/opencode/test/server/httpapi-workspace.test.ts index 193c2971a1..21bf4120c9 100644 --- a/packages/opencode/test/server/httpapi-workspace.test.ts +++ b/packages/opencode/test/server/httpapi-workspace.test.ts @@ -168,22 +168,19 @@ describe("workspace HttpApi", () => { const created = yield* request(WorkspacePaths.list, dir, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ type: "local-test", branch: null, extra: null }), + body: JSON.stringify({ type: "local-test", branch: null }), }) expect(created.status).toBe(200) const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info expect(workspace).toMatchObject({ type: "local-test", name: "local-test" }) const session = yield* Session.Service.use((svc) => svc.create({})).pipe(provideInstance(dir)) - const restored = yield* request(WorkspacePaths.sessionRestore.replace(":id", workspace.id), dir, { + const warped = yield* request(WorkspacePaths.warp, dir, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ sessionID: session.id }), - }) - expect(restored.status).toBe(200) - expect((yield* Effect.promise(() => restored.json())) as { total: number }).toMatchObject({ - total: expect.any(Number), + body: JSON.stringify({ id: workspace.id, sessionID: session.id }), }) + expect(warped.status).toBe(204) const removed = yield* request(WorkspacePaths.remove.replace(":id", workspace.id), dir, { method: "DELETE" }) expect(removed.status).toBe(200) @@ -212,7 +209,6 @@ describe("workspace HttpApi", () => { expect((yield* Effect.promise(() => created.json())) as Workspace.Info).toMatchObject({ type: "local-test", name: "local-test", - extra: null, }) }), ) @@ -257,7 +253,6 @@ describe("workspace HttpApi", () => { expect((yield* Effect.promise(() => created.json())) as Workspace.Info).toMatchObject({ type: "local-test", name: "local-test", - extra: null, }) }), ) @@ -272,7 +267,7 @@ describe("workspace HttpApi", () => { const created = yield* request(WorkspacePaths.list, dir, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ type: "local-target", branch: null, extra: null }), + body: JSON.stringify({ type: "local-target", branch: null }), }) const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info @@ -327,7 +322,7 @@ describe("workspace HttpApi", () => { const created = yield* request(WorkspacePaths.list, dir, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ type: "remote-target", branch: null, extra: null }), + body: JSON.stringify({ type: "remote-target", branch: null }), }) const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info @@ -394,7 +389,7 @@ describe("workspace HttpApi", () => { const created = yield* request(WorkspacePaths.list, dir, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ type: "remote-session-target", branch: null, extra: null }), + body: JSON.stringify({ type: "remote-session-target", branch: null }), }) const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info const session = yield* Session.Service.use((svc) => svc.create()).pipe( diff --git a/packages/opencode/test/sync/index.test.ts b/packages/opencode/test/sync/index.test.ts index 234c5246ee..0986b39044 100644 --- a/packages/opencode/test/sync/index.test.ts +++ b/packages/opencode/test/sync/index.test.ts @@ -5,7 +5,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Bus } from "../../src/bus" import { SyncEvent } from "../../src/sync" import { Database } from "@/storage/db" -import { EventTable } from "../../src/sync/event.sql" +import { EventSequenceTable, EventTable } from "../../src/sync/event.sql" import { MessageID } from "../../src/session/schema" import { Flag } from "@opencode-ai/core/flag/flag" import { initProjectors } from "../../src/server/projectors" @@ -252,5 +252,76 @@ describe("SyncEvent", () => { }), ), ) + + it.live( + "claims unowned event sequence on replay with ownerID", + provideTmpdirInstance(() => + Effect.gen(function* () { + const { Created } = setup() + const id = MessageID.ascending() + + yield* SyncEvent.use.replay( + { + id: "evt_1", + type: SyncEvent.versionedType(Created.type, Created.version), + seq: 0, + aggregateID: id, + data: { id, name: "owned" }, + }, + { publish: false, ownerID: "owner-1" }, + ) + + const row = Database.use((db) => + db + .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) + .from(EventSequenceTable) + .get(), + ) + expect(row).toEqual({ seq: 0, ownerID: "owner-1" }) + }), + ), + ) + + it.live( + "ignores replay from a different owner after sequence is claimed", + provideTmpdirInstance(() => + Effect.gen(function* () { + const { Created } = setup() + const id = MessageID.ascending() + + yield* SyncEvent.use.replay( + { + id: "evt_1", + type: SyncEvent.versionedType(Created.type, Created.version), + seq: 0, + aggregateID: id, + data: { id, name: "first" }, + }, + { publish: false, ownerID: "owner-1" }, + ) + yield* SyncEvent.use.replay( + { + id: "evt_2", + type: SyncEvent.versionedType(Created.type, Created.version), + seq: 1, + aggregateID: id, + data: { id, name: "ignored" }, + }, + { publish: false, ownerID: "owner-2" }, + ) + + const events = Database.use((db) => db.select().from(EventTable).all()) + const sequence = Database.use((db) => + db + .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) + .from(EventSequenceTable) + .get(), + ) + expect(events).toHaveLength(1) + expect(events[0].id).toBe("evt_1") + expect(sequence).toEqual({ seq: 0, ownerID: "owner-1" }) + }), + ), + ) }) }) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index e94132c2b2..ab191b0566 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -4,58 +4,84 @@ import { client } from "./client.gen.js" import { buildClientParams, type Client, type Options as Options2, type TDataShape } from "./client/index.js" import type { AgentPartInput, + AppAgentsErrors, AppAgentsResponses, AppLogErrors, AppLogResponses, + AppSkillsErrors, AppSkillsResponses, Auth as Auth3, AuthRemoveErrors, AuthRemoveResponses, AuthSetErrors, AuthSetResponses, + CommandListErrors, CommandListResponses, Config as Config3, + ConfigGetErrors, ConfigGetResponses, + ConfigProvidersErrors, ConfigProvidersResponses, ConfigUpdateErrors, ConfigUpdateResponses, + EventSubscribeErrors, EventSubscribeResponses, EventTuiCommandExecute2, EventTuiPromptAppend2, EventTuiSessionSelect2, EventTuiToastShow2, + ExperimentalConsoleGetErrors, ExperimentalConsoleGetResponses, + ExperimentalConsoleListOrgsErrors, ExperimentalConsoleListOrgsResponses, ExperimentalConsoleSwitchOrgResponses, + ExperimentalResourceListErrors, ExperimentalResourceListResponses, + ExperimentalSessionListErrors, ExperimentalSessionListResponses, + ExperimentalWorkspaceAdapterListErrors, ExperimentalWorkspaceAdapterListResponses, ExperimentalWorkspaceCreateErrors, ExperimentalWorkspaceCreateResponses, + ExperimentalWorkspaceListErrors, ExperimentalWorkspaceListResponses, ExperimentalWorkspaceRemoveErrors, ExperimentalWorkspaceRemoveResponses, - ExperimentalWorkspaceSessionRestoreErrors, - ExperimentalWorkspaceSessionRestoreResponses, + ExperimentalWorkspaceStatusErrors, ExperimentalWorkspaceStatusResponses, + ExperimentalWorkspaceWarpErrors, + ExperimentalWorkspaceWarpResponses, + FileListErrors, FileListResponses, FilePartInput, FilePartSource, + FileReadErrors, FileReadResponses, + FileStatusErrors, FileStatusResponses, + FindFilesErrors, FindFilesResponses, + FindSymbolsErrors, FindSymbolsResponses, + FindTextErrors, FindTextResponses, + FormatterStatusErrors, FormatterStatusResponses, + GlobalConfigGetErrors, GlobalConfigGetResponses, GlobalConfigUpdateErrors, GlobalConfigUpdateResponses, + GlobalDisposeErrors, GlobalDisposeResponses, + GlobalEventErrors, GlobalEventResponses, + GlobalHealthErrors, GlobalHealthResponses, GlobalUpgradeErrors, GlobalUpgradeResponses, + InstanceDisposeErrors, InstanceDisposeResponses, + LspStatusErrors, LspStatusResponses, McpAddErrors, McpAddResponses, @@ -67,10 +93,13 @@ import type { McpAuthRemoveResponses, McpAuthStartErrors, McpAuthStartResponses, + McpConnectErrors, McpConnectResponses, + McpDisconnectErrors, McpDisconnectResponses, McpLocalConfig, McpRemoteConfig, + McpStatusErrors, McpStatusResponses, OutputFormat, Part as Part2, @@ -78,20 +107,27 @@ import type { PartDeleteResponses, PartUpdateErrors, PartUpdateResponses, + PathGetErrors, PathGetResponses, + PermissionListErrors, PermissionListResponses, PermissionReplyErrors, PermissionReplyResponses, PermissionRespondErrors, PermissionRespondResponses, PermissionRuleset, + ProjectCurrentErrors, ProjectCurrentResponses, + ProjectInitGitErrors, ProjectInitGitResponses, + ProjectListErrors, ProjectListResponses, ProjectUpdateErrors, ProjectUpdateResponses, Prompt, + ProviderAuthErrors, ProviderAuthResponses, + ProviderListErrors, ProviderListResponses, ProviderOauthAuthorizeErrors, ProviderOauthAuthorizeResponses, @@ -105,13 +141,16 @@ import type { PtyCreateResponses, PtyGetErrors, PtyGetResponses, + PtyListErrors, PtyListResponses, PtyRemoveErrors, PtyRemoveResponses, + PtyShellsErrors, PtyShellsResponses, PtyUpdateErrors, PtyUpdateResponses, QuestionAnswer, + QuestionListErrors, QuestionListResponses, QuestionRejectErrors, QuestionRejectResponses, @@ -130,12 +169,15 @@ import type { SessionDeleteMessageResponses, SessionDeleteResponses, SessionDelivery, + SessionDiffErrors, SessionDiffResponses, + SessionForkErrors, SessionForkResponses, SessionGetErrors, SessionGetResponses, SessionInitErrors, SessionInitResponses, + SessionListErrors, SessionListResponses, SessionMessageErrors, SessionMessageResponses, @@ -168,7 +210,10 @@ import type { SyncHistoryListResponses, SyncReplayErrors, SyncReplayResponses, + SyncStartErrors, SyncStartResponses, + SyncStealErrors, + SyncStealResponses, TextPartInput, ToolIdsErrors, ToolIdsResponses, @@ -176,34 +221,50 @@ import type { ToolListResponses, TuiAppendPromptErrors, TuiAppendPromptResponses, + TuiClearPromptErrors, TuiClearPromptResponses, + TuiControlNextErrors, TuiControlNextResponses, + TuiControlResponseErrors, TuiControlResponseResponses, TuiExecuteCommandErrors, TuiExecuteCommandResponses, + TuiOpenHelpErrors, TuiOpenHelpResponses, + TuiOpenModelsErrors, TuiOpenModelsResponses, + TuiOpenSessionsErrors, TuiOpenSessionsResponses, + TuiOpenThemesErrors, TuiOpenThemesResponses, TuiPublishErrors, TuiPublishResponses, TuiSelectSessionErrors, TuiSelectSessionResponses, + TuiShowToastErrors, TuiShowToastResponses, + TuiSubmitPromptErrors, TuiSubmitPromptResponses, + V2SessionCompactErrors, V2SessionCompactResponses, + V2SessionContextErrors, V2SessionContextResponses, V2SessionListErrors, V2SessionListResponses, V2SessionMessagesErrors, V2SessionMessagesResponses, + V2SessionPromptErrors, V2SessionPromptResponses, + V2SessionWaitErrors, V2SessionWaitResponses, + VcsDiffErrors, VcsDiffResponses, + VcsGetErrors, VcsGetResponses, WorktreeCreateErrors, WorktreeCreateInput, WorktreeCreateResponses, + WorktreeListErrors, WorktreeListResponses, WorktreeRemoveErrors, WorktreeRemoveInput, @@ -381,7 +442,7 @@ export class App extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/agent", ...options, ...params, @@ -411,7 +472,7 @@ export class App extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/skill", ...options, ...params, @@ -426,7 +487,7 @@ export class Config extends HeyApiClient { * Retrieve the current global OpenCode configuration settings and preferences. */ public get(options?: Options) { - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/global/config", ...options, }) @@ -464,7 +525,7 @@ export class Global extends HeyApiClient { * Get health information about the OpenCode server. */ public health(options?: Options) { - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/global/health", ...options, }) @@ -476,7 +537,7 @@ export class Global extends HeyApiClient { * Subscribe to global events from the OpenCode system using server-sent events. */ public event(options?: Options) { - return (options?.client ?? this.client).sse.get({ + return (options?.client ?? this.client).sse.get({ url: "/global/event", ...options, }) @@ -488,7 +549,7 @@ export class Global extends HeyApiClient { * Clean up and dispose all OpenCode instances, releasing all resources. */ public dispose(options?: Options) { - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/global/dispose", ...options, }) @@ -548,7 +609,7 @@ export class Event extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).sse.get({ + return (options?.client ?? this.client).sse.get({ url: "/event", ...options, ...params, @@ -580,7 +641,7 @@ export class Config2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/config", ...options, ...params, @@ -647,7 +708,7 @@ export class Config2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/config/providers", ...options, ...params, @@ -679,7 +740,11 @@ export class Console extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get< + ExperimentalConsoleGetResponses, + ExperimentalConsoleGetErrors, + ThrowOnError + >({ url: "/experimental/console", ...options, ...params, @@ -709,7 +774,11 @@ export class Console extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get< + ExperimentalConsoleListOrgsResponses, + ExperimentalConsoleListOrgsErrors, + ThrowOnError + >({ url: "/experimental/console/orgs", ...options, ...params, @@ -792,7 +861,11 @@ export class Session extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get< + ExperimentalSessionListResponses, + ExperimentalSessionListErrors, + ThrowOnError + >({ url: "/experimental/session", ...options, ...params, @@ -824,7 +897,11 @@ export class Resource extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get< + ExperimentalResourceListResponses, + ExperimentalResourceListErrors, + ThrowOnError + >({ url: "/experimental/resource", ...options, ...params, @@ -856,7 +933,11 @@ export class Adapter extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get< + ExperimentalWorkspaceAdapterListResponses, + ExperimentalWorkspaceAdapterListErrors, + ThrowOnError + >({ url: "/experimental/workspace/adapter", ...options, ...params, @@ -888,7 +969,11 @@ export class Workspace extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get< + ExperimentalWorkspaceListResponses, + ExperimentalWorkspaceListErrors, + ThrowOnError + >({ url: "/experimental/workspace", ...options, ...params, @@ -965,7 +1050,11 @@ export class Workspace extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get< + ExperimentalWorkspaceStatusResponses, + ExperimentalWorkspaceStatusErrors, + ThrowOnError + >({ url: "/experimental/workspace/status", ...options, ...params, @@ -1009,15 +1098,15 @@ export class Workspace extends HeyApiClient { } /** - * Restore session into workspace + * Warp session into workspace * - * Replay a session's sync events into the target workspace in batches. + * Move a session's sync history into the target workspace, or detach it to the local project. */ - public sessionRestore( - parameters: { - id: string + public warp( + parameters?: { directory?: string workspace?: string + id?: string | null sessionID?: string }, options?: Options, @@ -1027,20 +1116,20 @@ export class Workspace extends HeyApiClient { [ { args: [ - { in: "path", key: "id" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "id" }, { in: "body", key: "sessionID" }, ], }, ], ) return (options?.client ?? this.client).post< - ExperimentalWorkspaceSessionRestoreResponses, - ExperimentalWorkspaceSessionRestoreErrors, + ExperimentalWorkspaceWarpResponses, + ExperimentalWorkspaceWarpErrors, ThrowOnError >({ - url: "/experimental/workspace/{id}/session-restore", + url: "/experimental/workspace/warp", ...options, ...params, headers: { @@ -1206,7 +1295,7 @@ export class Worktree extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/experimental/worktree", ...options, ...params, @@ -1314,7 +1403,7 @@ export class Find extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/find", ...options, ...params, @@ -1352,7 +1441,7 @@ export class Find extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/find/file", ...options, ...params, @@ -1384,7 +1473,7 @@ export class Find extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/find/symbol", ...options, ...params, @@ -1418,7 +1507,7 @@ export class File extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/file", ...options, ...params, @@ -1450,7 +1539,7 @@ export class File extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/file/content", ...options, ...params, @@ -1480,7 +1569,7 @@ export class File extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/file/status", ...options, ...params, @@ -1512,7 +1601,7 @@ export class Instance extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/instance/dispose", ...options, ...params, @@ -1544,7 +1633,7 @@ export class Path extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/path", ...options, ...params, @@ -1576,7 +1665,7 @@ export class Vcs extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/vcs", ...options, ...params, @@ -1608,7 +1697,7 @@ export class Vcs extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/vcs/diff", ...options, ...params, @@ -1640,7 +1729,7 @@ export class Command extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/command", ...options, ...params, @@ -1672,7 +1761,7 @@ export class Lsp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/lsp", ...options, ...params, @@ -1704,7 +1793,7 @@ export class Formatter extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/formatter", ...options, ...params, @@ -1875,7 +1964,7 @@ export class Mcp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/mcp", ...options, ...params, @@ -1944,7 +2033,7 @@ export class Mcp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/mcp/{name}/connect", ...options, ...params, @@ -1974,7 +2063,7 @@ export class Mcp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/mcp/{name}/disconnect", ...options, ...params, @@ -2011,7 +2100,7 @@ export class Project extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/project", ...options, ...params, @@ -2041,7 +2130,7 @@ export class Project extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/project/current", ...options, ...params, @@ -2071,7 +2160,7 @@ export class Project extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/project/git/init", ...options, ...params, @@ -2155,7 +2244,7 @@ export class Pty extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/pty/shells", ...options, ...params, @@ -2185,7 +2274,7 @@ export class Pty extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/pty", ...options, ...params, @@ -2436,7 +2525,7 @@ export class Question extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/question", ...options, ...params, @@ -2539,7 +2628,7 @@ export class Permission extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/permission", ...options, ...params, @@ -2749,7 +2838,7 @@ export class Provider extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/provider", ...options, ...params, @@ -2779,7 +2868,7 @@ export class Provider extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/provider/auth", ...options, ...params, @@ -2828,7 +2917,7 @@ export class Session2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/session", ...options, ...params, @@ -3116,7 +3205,7 @@ export class Session2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/session/{sessionID}/diff", ...options, ...params, @@ -3318,7 +3407,7 @@ export class Session2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/session/{sessionID}/fork", ...options, ...params, @@ -3894,7 +3983,7 @@ export class Sync extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/sync/start", ...options, ...params, @@ -3956,6 +4045,43 @@ export class Sync extends HeyApiClient { }) } + /** + * Steal session into workspace + * + * Update a session to belong to the current workspace through the sync event system. + */ + public steal( + parameters?: { + directory?: string + workspace?: string + sessionID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/sync/steal", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + private _history?: History get history(): History { return (this._history ??= new History({ client: this.client })) @@ -4022,7 +4148,7 @@ export class Session3 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/api/session/{sessionID}/prompt", ...options, ...params, @@ -4059,7 +4185,7 @@ export class Session3 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/api/session/{sessionID}/compact", ...options, ...params, @@ -4091,7 +4217,7 @@ export class Session3 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/api/session/{sessionID}/wait", ...options, ...params, @@ -4123,7 +4249,7 @@ export class Session3 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/api/session/{sessionID}/context", ...options, ...params, @@ -4194,7 +4320,7 @@ export class Control extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/tui/control/next", ...options, ...params, @@ -4226,7 +4352,7 @@ export class Control extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/control/response", ...options, ...params, @@ -4300,7 +4426,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/open-help", ...options, ...params, @@ -4330,7 +4456,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/open-sessions", ...options, ...params, @@ -4360,7 +4486,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/open-themes", ...options, ...params, @@ -4390,7 +4516,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/open-models", ...options, ...params, @@ -4420,7 +4546,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/submit-prompt", ...options, ...params, @@ -4450,7 +4576,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/clear-prompt", ...options, ...params, @@ -4525,7 +4651,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/show-toast", ...options, ...params, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 86c5a762b1..a40b567f8c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -35,7 +35,6 @@ export type Event = | EventVcsBranchUpdated | EventWorkspaceReady | EventWorkspaceFailed - | EventWorkspaceRestore | EventWorkspaceStatus | EventWorktreeReady | EventWorktreeFailed @@ -801,7 +800,6 @@ export type GlobalEvent = { | EventVcsBranchUpdated | EventWorkspaceReady | EventWorkspaceFailed - | EventWorkspaceRestore | EventWorkspaceStatus | EventWorktreeReady | EventWorktreeFailed @@ -2478,17 +2476,6 @@ export type EventWorkspaceFailed = { } } -export type EventWorkspaceRestore = { - id: string - type: "workspace.restore" - properties: { - workspaceID: string - sessionID: string - total: number - step: number - } -} - export type EventWorkspaceStatus = { id: string type: "workspace.status" @@ -3358,6 +3345,15 @@ export type GlobalHealthData = { url: "/global/health" } +export type GlobalHealthErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalHealthError = GlobalHealthErrors[keyof GlobalHealthErrors] + export type GlobalHealthResponses = { /** * Health information @@ -3377,6 +3373,15 @@ export type GlobalEventData = { url: "/global/event" } +export type GlobalEventErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalEventError = GlobalEventErrors[keyof GlobalEventErrors] + export type GlobalEventResponses = { /** * Event stream @@ -3393,6 +3398,15 @@ export type GlobalConfigGetData = { url: "/global/config" } +export type GlobalConfigGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalConfigGetError = GlobalConfigGetErrors[keyof GlobalConfigGetErrors] + export type GlobalConfigGetResponses = { /** * Get global config info @@ -3434,6 +3448,15 @@ export type GlobalDisposeData = { url: "/global/dispose" } +export type GlobalDisposeErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalDisposeError = GlobalDisposeErrors[keyof GlobalDisposeErrors] + export type GlobalDisposeResponses = { /** * Global disposed @@ -3488,6 +3511,15 @@ export type EventSubscribeData = { url: "/event" } +export type EventSubscribeErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type EventSubscribeError = EventSubscribeErrors[keyof EventSubscribeErrors] + export type EventSubscribeResponses = { /** * Event stream @@ -3507,6 +3539,15 @@ export type ConfigGetData = { url: "/config" } +export type ConfigGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ConfigGetError = ConfigGetErrors[keyof ConfigGetErrors] + export type ConfigGetResponses = { /** * Get config info @@ -3554,6 +3595,15 @@ export type ConfigProvidersData = { url: "/config/providers" } +export type ConfigProvidersErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ConfigProvidersError = ConfigProvidersErrors[keyof ConfigProvidersErrors] + export type ConfigProvidersResponses = { /** * List of providers @@ -3578,6 +3628,15 @@ export type ExperimentalConsoleGetData = { url: "/experimental/console" } +export type ExperimentalConsoleGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalConsoleGetError = ExperimentalConsoleGetErrors[keyof ExperimentalConsoleGetErrors] + export type ExperimentalConsoleGetResponses = { /** * Active Console provider metadata @@ -3597,6 +3656,16 @@ export type ExperimentalConsoleListOrgsData = { url: "/experimental/console/orgs" } +export type ExperimentalConsoleListOrgsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalConsoleListOrgsError = + ExperimentalConsoleListOrgsErrors[keyof ExperimentalConsoleListOrgsErrors] + export type ExperimentalConsoleListOrgsResponses = { /** * Switchable Console orgs @@ -3735,6 +3804,15 @@ export type WorktreeListData = { url: "/experimental/worktree" } +export type WorktreeListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type WorktreeListError = WorktreeListErrors[keyof WorktreeListErrors] + export type WorktreeListResponses = { /** * List of worktree directories @@ -3816,6 +3894,15 @@ export type ExperimentalSessionListData = { url: "/experimental/session" } +export type ExperimentalSessionListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalSessionListError = ExperimentalSessionListErrors[keyof ExperimentalSessionListErrors] + export type ExperimentalSessionListResponses = { /** * List of sessions @@ -3835,6 +3922,15 @@ export type ExperimentalResourceListData = { url: "/experimental/resource" } +export type ExperimentalResourceListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalResourceListError = ExperimentalResourceListErrors[keyof ExperimentalResourceListErrors] + export type ExperimentalResourceListResponses = { /** * MCP resources @@ -3858,6 +3954,15 @@ export type FindTextData = { url: "/find" } +export type FindTextErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FindTextError = FindTextErrors[keyof FindTextErrors] + export type FindTextResponses = { /** * Matches @@ -3897,6 +4002,15 @@ export type FindFilesData = { url: "/find/file" } +export type FindFilesErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FindFilesError = FindFilesErrors[keyof FindFilesErrors] + export type FindFilesResponses = { /** * File paths @@ -3917,6 +4031,15 @@ export type FindSymbolsData = { url: "/find/symbol" } +export type FindSymbolsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FindSymbolsError = FindSymbolsErrors[keyof FindSymbolsErrors] + export type FindSymbolsResponses = { /** * Symbols @@ -3937,6 +4060,15 @@ export type FileListData = { url: "/file" } +export type FileListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FileListError = FileListErrors[keyof FileListErrors] + export type FileListResponses = { /** * Files and directories @@ -3957,6 +4089,15 @@ export type FileReadData = { url: "/file/content" } +export type FileReadErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FileReadError = FileReadErrors[keyof FileReadErrors] + export type FileReadResponses = { /** * File content @@ -3976,6 +4117,15 @@ export type FileStatusData = { url: "/file/status" } +export type FileStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FileStatusError = FileStatusErrors[keyof FileStatusErrors] + export type FileStatusResponses = { /** * File status @@ -3995,6 +4145,15 @@ export type InstanceDisposeData = { url: "/instance/dispose" } +export type InstanceDisposeErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type InstanceDisposeError = InstanceDisposeErrors[keyof InstanceDisposeErrors] + export type InstanceDisposeResponses = { /** * Instance disposed @@ -4014,6 +4173,15 @@ export type PathGetData = { url: "/path" } +export type PathGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PathGetError = PathGetErrors[keyof PathGetErrors] + export type PathGetResponses = { /** * Path @@ -4033,6 +4201,15 @@ export type VcsGetData = { url: "/vcs" } +export type VcsGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type VcsGetError = VcsGetErrors[keyof VcsGetErrors] + export type VcsGetResponses = { /** * VCS info @@ -4053,6 +4230,15 @@ export type VcsDiffData = { url: "/vcs/diff" } +export type VcsDiffErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type VcsDiffError = VcsDiffErrors[keyof VcsDiffErrors] + export type VcsDiffResponses = { /** * VCS diff @@ -4072,6 +4258,15 @@ export type CommandListData = { url: "/command" } +export type CommandListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type CommandListError = CommandListErrors[keyof CommandListErrors] + export type CommandListResponses = { /** * List of commands @@ -4091,6 +4286,15 @@ export type AppAgentsData = { url: "/agent" } +export type AppAgentsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AppAgentsError = AppAgentsErrors[keyof AppAgentsErrors] + export type AppAgentsResponses = { /** * List of agents @@ -4110,6 +4314,15 @@ export type AppSkillsData = { url: "/skill" } +export type AppSkillsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AppSkillsError = AppSkillsErrors[keyof AppSkillsErrors] + export type AppSkillsResponses = { /** * List of skills @@ -4134,6 +4347,15 @@ export type LspStatusData = { url: "/lsp" } +export type LspStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type LspStatusError = LspStatusErrors[keyof LspStatusErrors] + export type LspStatusResponses = { /** * LSP server status @@ -4153,6 +4375,15 @@ export type FormatterStatusData = { url: "/formatter" } +export type FormatterStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FormatterStatusError = FormatterStatusErrors[keyof FormatterStatusErrors] + export type FormatterStatusResponses = { /** * Formatter status @@ -4172,6 +4403,15 @@ export type McpStatusData = { url: "/mcp" } +export type McpStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type McpStatusError = McpStatusErrors[keyof McpStatusErrors] + export type McpStatusResponses = { /** * MCP server status @@ -4229,6 +4469,10 @@ export type McpAuthRemoveData = { } export type McpAuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError /** * Not found */ @@ -4262,7 +4506,7 @@ export type McpAuthStartData = { export type McpAuthStartErrors = { /** - * McpUnsupportedOAuthError + * McpUnsupportedOAuthError | BadRequest */ 400: McpUnsupportedOAuthError /** @@ -4335,7 +4579,7 @@ export type McpAuthAuthenticateData = { export type McpAuthAuthenticateErrors = { /** - * McpUnsupportedOAuthError + * McpUnsupportedOAuthError | BadRequest */ 400: McpUnsupportedOAuthError /** @@ -4367,6 +4611,15 @@ export type McpConnectData = { url: "/mcp/{name}/connect" } +export type McpConnectErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type McpConnectError = McpConnectErrors[keyof McpConnectErrors] + export type McpConnectResponses = { /** * MCP server connected successfully @@ -4388,6 +4641,15 @@ export type McpDisconnectData = { url: "/mcp/{name}/disconnect" } +export type McpDisconnectErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type McpDisconnectError = McpDisconnectErrors[keyof McpDisconnectErrors] + export type McpDisconnectResponses = { /** * MCP server disconnected successfully @@ -4407,6 +4669,15 @@ export type ProjectListData = { url: "/project" } +export type ProjectListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProjectListError = ProjectListErrors[keyof ProjectListErrors] + export type ProjectListResponses = { /** * List of projects @@ -4426,6 +4697,15 @@ export type ProjectCurrentData = { url: "/project/current" } +export type ProjectCurrentErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProjectCurrentError = ProjectCurrentErrors[keyof ProjectCurrentErrors] + export type ProjectCurrentResponses = { /** * Current project information @@ -4445,6 +4725,15 @@ export type ProjectInitGitData = { url: "/project/git/init" } +export type ProjectInitGitErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProjectInitGitError = ProjectInitGitErrors[keyof ProjectInitGitErrors] + export type ProjectInitGitResponses = { /** * Project information after git initialization @@ -4511,6 +4800,15 @@ export type PtyShellsData = { url: "/pty/shells" } +export type PtyShellsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PtyShellsError = PtyShellsErrors[keyof PtyShellsErrors] + export type PtyShellsResponses = { /** * List of shells @@ -4534,6 +4832,15 @@ export type PtyListData = { url: "/pty" } +export type PtyListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PtyListError = PtyListErrors[keyof PtyListErrors] + export type PtyListResponses = { /** * List of sessions @@ -4592,6 +4899,10 @@ export type PtyRemoveData = { } export type PtyRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError /** * Not found */ @@ -4622,6 +4933,10 @@ export type PtyGetData = { } export type PtyGetErrors = { + /** + * Bad request + */ + 400: BadRequestError /** * Not found */ @@ -4688,6 +5003,10 @@ export type PtyConnectTokenData = { } export type PtyConnectTokenErrors = { + /** + * Bad request + */ + 400: BadRequestError /** * Forbidden */ @@ -4722,6 +5041,15 @@ export type QuestionListData = { url: "/question" } +export type QuestionListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type QuestionListError = QuestionListErrors[keyof QuestionListErrors] + export type QuestionListResponses = { /** * List of pending questions @@ -4814,6 +5142,15 @@ export type PermissionListData = { url: "/permission" } +export type PermissionListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PermissionListError = PermissionListErrors[keyof PermissionListErrors] + export type PermissionListResponses = { /** * List of pending permissions @@ -4870,6 +5207,15 @@ export type ProviderListData = { url: "/provider" } +export type ProviderListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProviderListError = ProviderListErrors[keyof ProviderListErrors] + export type ProviderListResponses = { /** * List of providers @@ -4895,6 +5241,15 @@ export type ProviderAuthData = { url: "/provider/auth" } +export type ProviderAuthErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProviderAuthError2 = ProviderAuthErrors[keyof ProviderAuthErrors] + export type ProviderAuthResponses = { /** * Provider auth methods @@ -4996,6 +5351,15 @@ export type SessionListData = { url: "/session" } +export type SessionListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SessionListError = SessionListErrors[keyof SessionListErrors] + export type SessionListResponses = { /** * List of sessions @@ -5263,6 +5627,15 @@ export type SessionDiffData = { url: "/session/{sessionID}/diff" } +export type SessionDiffErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SessionDiffError = SessionDiffErrors[keyof SessionDiffErrors] + export type SessionDiffResponses = { /** * Successfully retrieved diff @@ -5450,6 +5823,15 @@ export type SessionForkData = { url: "/session/{sessionID}/fork" } +export type SessionForkErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SessionForkError = SessionForkErrors[keyof SessionForkErrors] + export type SessionForkResponses = { /** * 200 @@ -5973,6 +6355,15 @@ export type SyncStartData = { url: "/sync/start" } +export type SyncStartErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SyncStartError = SyncStartErrors[keyof SyncStartErrors] + export type SyncStartResponses = { /** * Workspace sync started @@ -6023,6 +6414,38 @@ export type SyncReplayResponses = { export type SyncReplayResponse = SyncReplayResponses[keyof SyncReplayResponses] +export type SyncStealData = { + body?: { + sessionID: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/steal" +} + +export type SyncStealErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SyncStealError = SyncStealErrors[keyof SyncStealErrors] + +export type SyncStealResponses = { + /** + * Session stolen into workspace + */ + 200: { + sessionID: string + } +} + +export type SyncStealResponse = SyncStealResponses[keyof SyncStealResponses] + export type SyncHistoryListData = { body?: { [key: string]: number @@ -6104,6 +6527,15 @@ export type V2SessionPromptData = { url: "/api/session/{sessionID}/prompt" } +export type V2SessionPromptErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type V2SessionPromptError = V2SessionPromptErrors[keyof V2SessionPromptErrors] + export type V2SessionPromptResponses = { /** * Session.Message @@ -6125,6 +6557,15 @@ export type V2SessionCompactData = { url: "/api/session/{sessionID}/compact" } +export type V2SessionCompactErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type V2SessionCompactError = V2SessionCompactErrors[keyof V2SessionCompactErrors] + export type V2SessionCompactResponses = { /** * @@ -6146,6 +6587,15 @@ export type V2SessionWaitData = { url: "/api/session/{sessionID}/wait" } +export type V2SessionWaitErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type V2SessionWaitError = V2SessionWaitErrors[keyof V2SessionWaitErrors] + export type V2SessionWaitResponses = { /** * @@ -6167,6 +6617,15 @@ export type V2SessionContextData = { url: "/api/session/{sessionID}/context" } +export type V2SessionContextErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type V2SessionContextError = V2SessionContextErrors[keyof V2SessionContextErrors] + export type V2SessionContextResponses = { /** * Success @@ -6246,6 +6705,15 @@ export type TuiOpenHelpData = { url: "/tui/open-help" } +export type TuiOpenHelpErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiOpenHelpError = TuiOpenHelpErrors[keyof TuiOpenHelpErrors] + export type TuiOpenHelpResponses = { /** * Help dialog opened successfully @@ -6265,6 +6733,15 @@ export type TuiOpenSessionsData = { url: "/tui/open-sessions" } +export type TuiOpenSessionsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiOpenSessionsError = TuiOpenSessionsErrors[keyof TuiOpenSessionsErrors] + export type TuiOpenSessionsResponses = { /** * Session dialog opened successfully @@ -6284,6 +6761,15 @@ export type TuiOpenThemesData = { url: "/tui/open-themes" } +export type TuiOpenThemesErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiOpenThemesError = TuiOpenThemesErrors[keyof TuiOpenThemesErrors] + export type TuiOpenThemesResponses = { /** * Theme dialog opened successfully @@ -6303,6 +6789,15 @@ export type TuiOpenModelsData = { url: "/tui/open-models" } +export type TuiOpenModelsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiOpenModelsError = TuiOpenModelsErrors[keyof TuiOpenModelsErrors] + export type TuiOpenModelsResponses = { /** * Model dialog opened successfully @@ -6322,6 +6817,15 @@ export type TuiSubmitPromptData = { url: "/tui/submit-prompt" } +export type TuiSubmitPromptErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiSubmitPromptError = TuiSubmitPromptErrors[keyof TuiSubmitPromptErrors] + export type TuiSubmitPromptResponses = { /** * Prompt submitted successfully @@ -6341,6 +6845,15 @@ export type TuiClearPromptData = { url: "/tui/clear-prompt" } +export type TuiClearPromptErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiClearPromptError = TuiClearPromptErrors[keyof TuiClearPromptErrors] + export type TuiClearPromptResponses = { /** * Prompt cleared successfully @@ -6395,6 +6908,15 @@ export type TuiShowToastData = { url: "/tui/show-toast" } +export type TuiShowToastErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiShowToastError = TuiShowToastErrors[keyof TuiShowToastErrors] + export type TuiShowToastResponses = { /** * Toast notification shown successfully @@ -6479,6 +7001,15 @@ export type TuiControlNextData = { url: "/tui/control/next" } +export type TuiControlNextErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiControlNextError = TuiControlNextErrors[keyof TuiControlNextErrors] + export type TuiControlNextResponses = { /** * Next TUI request @@ -6501,6 +7032,15 @@ export type TuiControlResponseData = { url: "/tui/control/response" } +export type TuiControlResponseErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiControlResponseError = TuiControlResponseErrors[keyof TuiControlResponseErrors] + export type TuiControlResponseResponses = { /** * Response submitted successfully @@ -6520,6 +7060,16 @@ export type ExperimentalWorkspaceAdapterListData = { url: "/experimental/workspace/adapter" } +export type ExperimentalWorkspaceAdapterListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceAdapterListError = + ExperimentalWorkspaceAdapterListErrors[keyof ExperimentalWorkspaceAdapterListErrors] + export type ExperimentalWorkspaceAdapterListResponses = { /** * Workspace adapters @@ -6544,6 +7094,15 @@ export type ExperimentalWorkspaceListData = { url: "/experimental/workspace" } +export type ExperimentalWorkspaceListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceListError = ExperimentalWorkspaceListErrors[keyof ExperimentalWorkspaceListErrors] + export type ExperimentalWorkspaceListResponses = { /** * Workspaces @@ -6559,7 +7118,7 @@ export type ExperimentalWorkspaceCreateData = { id?: string type: string branch: string | null - extra?: unknown | null + extra: unknown | null } path?: never query?: { @@ -6599,6 +7158,16 @@ export type ExperimentalWorkspaceStatusData = { url: "/experimental/workspace/status" } +export type ExperimentalWorkspaceStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceStatusError = + ExperimentalWorkspaceStatusErrors[keyof ExperimentalWorkspaceStatusErrors] + export type ExperimentalWorkspaceStatusResponses = { /** * Workspace status @@ -6644,41 +7213,37 @@ export type ExperimentalWorkspaceRemoveResponses = { export type ExperimentalWorkspaceRemoveResponse = ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] -export type ExperimentalWorkspaceSessionRestoreData = { +export type ExperimentalWorkspaceWarpData = { body?: { + id: string | null sessionID: string } - path: { - id: string - } + path?: never query?: { directory?: string workspace?: string } - url: "/experimental/workspace/{id}/session-restore" + url: "/experimental/workspace/warp" } -export type ExperimentalWorkspaceSessionRestoreErrors = { +export type ExperimentalWorkspaceWarpErrors = { /** * Bad request */ 400: BadRequestError } -export type ExperimentalWorkspaceSessionRestoreError = - ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors] +export type ExperimentalWorkspaceWarpError = ExperimentalWorkspaceWarpErrors[keyof ExperimentalWorkspaceWarpErrors] -export type ExperimentalWorkspaceSessionRestoreResponses = { +export type ExperimentalWorkspaceWarpResponses = { /** - * Session replay started + * Session warped */ - 200: { - total: number - } + 204: void } -export type ExperimentalWorkspaceSessionRestoreResponse = - ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses] +export type ExperimentalWorkspaceWarpResponse = + ExperimentalWorkspaceWarpResponses[keyof ExperimentalWorkspaceWarpResponses] export type PtyConnectData = { body?: never @@ -6693,6 +7258,10 @@ export type PtyConnectData = { } export type PtyConnectErrors = { + /** + * Bad request + */ + 400: BadRequestError /** * Forbidden */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 6ff18b5155..1a2f1e9475 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -218,6 +218,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get health information about the OpenCode server.", @@ -245,6 +255,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Subscribe to global events from the OpenCode system using server-sent events.", @@ -272,6 +292,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Retrieve the current global OpenCode configuration settings and preferences.", @@ -344,6 +374,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Clean up and dispose all OpenCode instances, releasing all resources.", @@ -470,6 +510,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get events", @@ -514,6 +564,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Retrieve the current OpenCode configuration settings and preferences.", @@ -636,6 +696,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of all configured AI providers and their default models.", @@ -680,6 +750,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get the active Console org name and the set of provider IDs managed by that Console org.", @@ -757,6 +837,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get the available Console orgs across logged-in accounts, including the current active org.", @@ -993,6 +1083,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "List all sandbox worktrees for the current project.", @@ -1292,6 +1392,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.", @@ -1340,6 +1450,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get all available MCP resources from connected servers. Optionally filter by name.", @@ -1456,6 +1576,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Search for text patterns across files in the project using ripgrep.", @@ -1540,6 +1670,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Search for files or directories by name or pattern in the project directory.", @@ -1596,6 +1736,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Search for workspace symbols like functions, classes, and variables using LSP.", @@ -1652,6 +1802,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "List files and directories in a specified path.", @@ -1704,6 +1864,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Read the content of a specified file.", @@ -1752,6 +1922,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get the git status of all files in the project.", @@ -1797,6 +1977,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Clean up and dispose the current OpenCode instance, releasing all resources.", @@ -1841,6 +2031,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Retrieve the current working directory and related path information for the OpenCode instance.", @@ -1885,6 +2085,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Retrieve version control system (VCS) information for the current project, such as git branch.", @@ -1942,6 +2152,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Retrieve the current git diff for the working tree or against the default branch.", @@ -1990,6 +2210,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of all available commands in the OpenCode system.", @@ -2038,6 +2268,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of all available AI agents in the OpenCode system.", @@ -2102,6 +2342,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of all available skills in the OpenCode system.", @@ -2150,6 +2400,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get LSP server status", @@ -2198,6 +2458,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get formatter status", @@ -2246,6 +2516,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get the status of all Model Context Protocol (MCP) servers.", @@ -2393,7 +2673,7 @@ } }, "400": { - "description": "McpUnsupportedOAuthError", + "description": "McpUnsupportedOAuthError | BadRequest", "content": { "application/json": { "schema": { @@ -2471,6 +2751,16 @@ } } }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, "404": { "description": "Not found", "content": { @@ -2622,7 +2912,7 @@ } }, "400": { - "description": "McpUnsupportedOAuthError", + "description": "McpUnsupportedOAuthError | BadRequest", "content": { "application/json": { "schema": { @@ -2693,6 +2983,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Connect an MCP server.", @@ -2745,6 +3045,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Disconnect an MCP server.", @@ -2792,6 +3102,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of projects that have been opened with OpenCode.", @@ -2836,6 +3156,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Retrieve the currently active project that OpenCode is working with.", @@ -2880,6 +3210,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Create a git repository for the current project and return the refreshed project info.", @@ -3053,6 +3393,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of available shells on the system.", @@ -3101,6 +3451,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.", @@ -3240,6 +3600,16 @@ } } }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, "404": { "description": "Not found", "content": { @@ -3393,6 +3763,16 @@ } } }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, "404": { "description": "Not found", "content": { @@ -3468,6 +3848,16 @@ } } }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, "403": { "description": "Forbidden", "content": { @@ -3535,6 +3925,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get all pending question requests across all sessions.", @@ -3751,6 +4151,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get all pending permission requests across all sessions.", @@ -3912,6 +4322,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of all available AI providers, including both available and connected ones.", @@ -3963,6 +4383,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Retrieve available authentication methods for all AI providers.", @@ -4236,6 +4666,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get a list of all OpenCode sessions, sorted by most recently updated.", @@ -4852,6 +5292,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get the file changes (diff) that resulted from a specific user message in the session.", @@ -5342,6 +5792,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Create a new session by forking an existing session at a specific message point.", @@ -6668,6 +7128,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Start sync loops for workspaces in the current project that have active sessions.", @@ -6785,6 +7255,84 @@ ] } }, + "/sync/steal": { + "post": { + "tags": ["sync"], + "operationId": "sync.steal", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Session stolen into workspace", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + } + }, + "required": ["sessionID"], + "additionalProperties": false, + "description": "Session stolen into workspace" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "description": "Update a session to belong to the current workspace through the sync event system.", + "summary": "Steal session into workspace", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.steal({\n ...\n})" + } + ] + } + }, "/sync/history": { "post": { "tags": ["sync"], @@ -6971,6 +7519,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Create a v2 session message and queue it for the agent loop.", @@ -7036,6 +7594,16 @@ "responses": { "204": { "description": "" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Compact a v2 session conversation.", @@ -7082,6 +7650,16 @@ "responses": { "204": { "description": "" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Wait for a v2 session agent loop to become idle.", @@ -7138,6 +7716,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Retrieve the active context messages for a v2 session (all messages after the last compaction).", @@ -7317,6 +7905,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Open the help dialog in the TUI to display user assistance information.", @@ -7362,6 +7960,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Open the session dialog.", @@ -7407,6 +8015,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Open the theme dialog.", @@ -7452,6 +8070,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Open the model dialog.", @@ -7497,6 +8125,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Submit the prompt.", @@ -7542,6 +8180,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Clear the prompt.", @@ -7658,6 +8306,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Show a toast notification in the TUI.", @@ -7897,6 +8555,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Retrieve the next TUI request from the queue for processing.", @@ -7942,6 +8610,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Submit a response to the TUI request queue to complete a pending request.", @@ -8010,6 +8688,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "List all available workspace adapters for the current project.", @@ -8058,6 +8746,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "List all workspaces.", @@ -8145,7 +8843,7 @@ ] } }, - "required": ["type", "branch"], + "required": ["type", "branch", "extra"], "additionalProperties": false } } @@ -8206,6 +8904,16 @@ } } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } } }, "description": "Get connection status for workspaces in the current project.", @@ -8281,10 +8989,10 @@ ] } }, - "/experimental/workspace/{id}/session-restore": { + "/experimental/workspace/warp": { "post": { "tags": ["workspace"], - "operationId": "experimental.workspace.sessionRestore", + "operationId": "experimental.workspace.warp", "parameters": [ { "name": "directory", @@ -8301,36 +9009,11 @@ "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "schema": { - "type": "string", - "pattern": "^wrk.*" - }, - "required": true } ], "responses": { - "200": { - "description": "Session replay started", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["total"], - "additionalProperties": false, - "description": "Session replay started" - } - } - } + "204": { + "description": "Session warped" }, "400": { "description": "Bad request", @@ -8343,19 +9026,22 @@ } } }, - "description": "Replay a session's sync events into the target workspace in batches.", - "summary": "Restore session into workspace", + "description": "Move a session's sync history into the target workspace, or detach it to the local project.", + "summary": "Warp session into workspace", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { + "id": { + "type": "string" + }, "sessionID": { "type": "string" } }, - "required": ["sessionID"], + "required": ["id", "sessionID"], "additionalProperties": false } } @@ -8364,7 +9050,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.sessionRestore({\n ...\n})" + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.warp({\n ...\n})" } ] } @@ -8412,6 +9098,16 @@ } } }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, "403": { "description": "Forbidden", "content": { @@ -8538,9 +9234,6 @@ { "$ref": "#/components/schemas/EventWorkspaceFailed" }, - { - "$ref": "#/components/schemas/EventWorkspaceRestore" - }, { "$ref": "#/components/schemas/EventWorkspaceStatus" }, @@ -10737,9 +11430,6 @@ { "$ref": "#/components/schemas/EventWorkspaceFailed" }, - { - "$ref": "#/components/schemas/EventWorkspaceRestore" - }, { "$ref": "#/components/schemas/EventWorkspaceStatus" }, @@ -15793,41 +16483,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventWorkspaceRestore": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["workspace.restore"] - }, - "properties": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "total": { - "type": "integer", - "minimum": 0 - }, - "step": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["workspaceID", "sessionID", "total", "step"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventWorkspaceStatus": { "type": "object", "properties": { From f33b17e8ac157237fdf3c4d3ff06ced126fb4752 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 5 May 2026 01:29:49 +0000 Subject: [PATCH 20/27] chore: generate --- packages/sdk/js/src/v2/gen/sdk.gen.ts | 207 +++----- packages/sdk/js/src/v2/gen/types.gen.ts | 562 +-------------------- packages/sdk/openapi.json | 646 +----------------------- 3 files changed, 67 insertions(+), 1348 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index ab191b0566..ffc0970c0e 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -4,84 +4,58 @@ import { client } from "./client.gen.js" import { buildClientParams, type Client, type Options as Options2, type TDataShape } from "./client/index.js" import type { AgentPartInput, - AppAgentsErrors, AppAgentsResponses, AppLogErrors, AppLogResponses, - AppSkillsErrors, AppSkillsResponses, Auth as Auth3, AuthRemoveErrors, AuthRemoveResponses, AuthSetErrors, AuthSetResponses, - CommandListErrors, CommandListResponses, Config as Config3, - ConfigGetErrors, ConfigGetResponses, - ConfigProvidersErrors, ConfigProvidersResponses, ConfigUpdateErrors, ConfigUpdateResponses, - EventSubscribeErrors, EventSubscribeResponses, EventTuiCommandExecute2, EventTuiPromptAppend2, EventTuiSessionSelect2, EventTuiToastShow2, - ExperimentalConsoleGetErrors, ExperimentalConsoleGetResponses, - ExperimentalConsoleListOrgsErrors, ExperimentalConsoleListOrgsResponses, ExperimentalConsoleSwitchOrgResponses, - ExperimentalResourceListErrors, ExperimentalResourceListResponses, - ExperimentalSessionListErrors, ExperimentalSessionListResponses, - ExperimentalWorkspaceAdapterListErrors, ExperimentalWorkspaceAdapterListResponses, ExperimentalWorkspaceCreateErrors, ExperimentalWorkspaceCreateResponses, - ExperimentalWorkspaceListErrors, ExperimentalWorkspaceListResponses, ExperimentalWorkspaceRemoveErrors, ExperimentalWorkspaceRemoveResponses, - ExperimentalWorkspaceStatusErrors, ExperimentalWorkspaceStatusResponses, ExperimentalWorkspaceWarpErrors, ExperimentalWorkspaceWarpResponses, - FileListErrors, FileListResponses, FilePartInput, FilePartSource, - FileReadErrors, FileReadResponses, - FileStatusErrors, FileStatusResponses, - FindFilesErrors, FindFilesResponses, - FindSymbolsErrors, FindSymbolsResponses, - FindTextErrors, FindTextResponses, - FormatterStatusErrors, FormatterStatusResponses, - GlobalConfigGetErrors, GlobalConfigGetResponses, GlobalConfigUpdateErrors, GlobalConfigUpdateResponses, - GlobalDisposeErrors, GlobalDisposeResponses, - GlobalEventErrors, GlobalEventResponses, - GlobalHealthErrors, GlobalHealthResponses, GlobalUpgradeErrors, GlobalUpgradeResponses, - InstanceDisposeErrors, InstanceDisposeResponses, - LspStatusErrors, LspStatusResponses, McpAddErrors, McpAddResponses, @@ -93,13 +67,10 @@ import type { McpAuthRemoveResponses, McpAuthStartErrors, McpAuthStartResponses, - McpConnectErrors, McpConnectResponses, - McpDisconnectErrors, McpDisconnectResponses, McpLocalConfig, McpRemoteConfig, - McpStatusErrors, McpStatusResponses, OutputFormat, Part as Part2, @@ -107,27 +78,20 @@ import type { PartDeleteResponses, PartUpdateErrors, PartUpdateResponses, - PathGetErrors, PathGetResponses, - PermissionListErrors, PermissionListResponses, PermissionReplyErrors, PermissionReplyResponses, PermissionRespondErrors, PermissionRespondResponses, PermissionRuleset, - ProjectCurrentErrors, ProjectCurrentResponses, - ProjectInitGitErrors, ProjectInitGitResponses, - ProjectListErrors, ProjectListResponses, ProjectUpdateErrors, ProjectUpdateResponses, Prompt, - ProviderAuthErrors, ProviderAuthResponses, - ProviderListErrors, ProviderListResponses, ProviderOauthAuthorizeErrors, ProviderOauthAuthorizeResponses, @@ -141,16 +105,13 @@ import type { PtyCreateResponses, PtyGetErrors, PtyGetResponses, - PtyListErrors, PtyListResponses, PtyRemoveErrors, PtyRemoveResponses, - PtyShellsErrors, PtyShellsResponses, PtyUpdateErrors, PtyUpdateResponses, QuestionAnswer, - QuestionListErrors, QuestionListResponses, QuestionRejectErrors, QuestionRejectResponses, @@ -169,15 +130,12 @@ import type { SessionDeleteMessageResponses, SessionDeleteResponses, SessionDelivery, - SessionDiffErrors, SessionDiffResponses, - SessionForkErrors, SessionForkResponses, SessionGetErrors, SessionGetResponses, SessionInitErrors, SessionInitResponses, - SessionListErrors, SessionListResponses, SessionMessageErrors, SessionMessageResponses, @@ -210,7 +168,6 @@ import type { SyncHistoryListResponses, SyncReplayErrors, SyncReplayResponses, - SyncStartErrors, SyncStartResponses, SyncStealErrors, SyncStealResponses, @@ -221,50 +178,34 @@ import type { ToolListResponses, TuiAppendPromptErrors, TuiAppendPromptResponses, - TuiClearPromptErrors, TuiClearPromptResponses, - TuiControlNextErrors, TuiControlNextResponses, - TuiControlResponseErrors, TuiControlResponseResponses, TuiExecuteCommandErrors, TuiExecuteCommandResponses, - TuiOpenHelpErrors, TuiOpenHelpResponses, - TuiOpenModelsErrors, TuiOpenModelsResponses, - TuiOpenSessionsErrors, TuiOpenSessionsResponses, - TuiOpenThemesErrors, TuiOpenThemesResponses, TuiPublishErrors, TuiPublishResponses, TuiSelectSessionErrors, TuiSelectSessionResponses, - TuiShowToastErrors, TuiShowToastResponses, - TuiSubmitPromptErrors, TuiSubmitPromptResponses, - V2SessionCompactErrors, V2SessionCompactResponses, - V2SessionContextErrors, V2SessionContextResponses, V2SessionListErrors, V2SessionListResponses, V2SessionMessagesErrors, V2SessionMessagesResponses, - V2SessionPromptErrors, V2SessionPromptResponses, - V2SessionWaitErrors, V2SessionWaitResponses, - VcsDiffErrors, VcsDiffResponses, - VcsGetErrors, VcsGetResponses, WorktreeCreateErrors, WorktreeCreateInput, WorktreeCreateResponses, - WorktreeListErrors, WorktreeListResponses, WorktreeRemoveErrors, WorktreeRemoveInput, @@ -442,7 +383,7 @@ export class App extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/agent", ...options, ...params, @@ -472,7 +413,7 @@ export class App extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/skill", ...options, ...params, @@ -487,7 +428,7 @@ export class Config extends HeyApiClient { * Retrieve the current global OpenCode configuration settings and preferences. */ public get(options?: Options) { - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/global/config", ...options, }) @@ -525,7 +466,7 @@ export class Global extends HeyApiClient { * Get health information about the OpenCode server. */ public health(options?: Options) { - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/global/health", ...options, }) @@ -537,7 +478,7 @@ export class Global extends HeyApiClient { * Subscribe to global events from the OpenCode system using server-sent events. */ public event(options?: Options) { - return (options?.client ?? this.client).sse.get({ + return (options?.client ?? this.client).sse.get({ url: "/global/event", ...options, }) @@ -549,7 +490,7 @@ export class Global extends HeyApiClient { * Clean up and dispose all OpenCode instances, releasing all resources. */ public dispose(options?: Options) { - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/global/dispose", ...options, }) @@ -609,7 +550,7 @@ export class Event extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).sse.get({ + return (options?.client ?? this.client).sse.get({ url: "/event", ...options, ...params, @@ -641,7 +582,7 @@ export class Config2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/config", ...options, ...params, @@ -708,7 +649,7 @@ export class Config2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/config/providers", ...options, ...params, @@ -740,11 +681,7 @@ export class Console extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get< - ExperimentalConsoleGetResponses, - ExperimentalConsoleGetErrors, - ThrowOnError - >({ + return (options?.client ?? this.client).get({ url: "/experimental/console", ...options, ...params, @@ -774,11 +711,7 @@ export class Console extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get< - ExperimentalConsoleListOrgsResponses, - ExperimentalConsoleListOrgsErrors, - ThrowOnError - >({ + return (options?.client ?? this.client).get({ url: "/experimental/console/orgs", ...options, ...params, @@ -861,11 +794,7 @@ export class Session extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get< - ExperimentalSessionListResponses, - ExperimentalSessionListErrors, - ThrowOnError - >({ + return (options?.client ?? this.client).get({ url: "/experimental/session", ...options, ...params, @@ -897,11 +826,7 @@ export class Resource extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get< - ExperimentalResourceListResponses, - ExperimentalResourceListErrors, - ThrowOnError - >({ + return (options?.client ?? this.client).get({ url: "/experimental/resource", ...options, ...params, @@ -933,11 +858,7 @@ export class Adapter extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get< - ExperimentalWorkspaceAdapterListResponses, - ExperimentalWorkspaceAdapterListErrors, - ThrowOnError - >({ + return (options?.client ?? this.client).get({ url: "/experimental/workspace/adapter", ...options, ...params, @@ -969,11 +890,7 @@ export class Workspace extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get< - ExperimentalWorkspaceListResponses, - ExperimentalWorkspaceListErrors, - ThrowOnError - >({ + return (options?.client ?? this.client).get({ url: "/experimental/workspace", ...options, ...params, @@ -1050,11 +967,7 @@ export class Workspace extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get< - ExperimentalWorkspaceStatusResponses, - ExperimentalWorkspaceStatusErrors, - ThrowOnError - >({ + return (options?.client ?? this.client).get({ url: "/experimental/workspace/status", ...options, ...params, @@ -1106,7 +1019,7 @@ export class Workspace extends HeyApiClient { parameters?: { directory?: string workspace?: string - id?: string | null + id?: string sessionID?: string }, options?: Options, @@ -1295,7 +1208,7 @@ export class Worktree extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/experimental/worktree", ...options, ...params, @@ -1403,7 +1316,7 @@ export class Find extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/find", ...options, ...params, @@ -1441,7 +1354,7 @@ export class Find extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/find/file", ...options, ...params, @@ -1473,7 +1386,7 @@ export class Find extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/find/symbol", ...options, ...params, @@ -1507,7 +1420,7 @@ export class File extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/file", ...options, ...params, @@ -1539,7 +1452,7 @@ export class File extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/file/content", ...options, ...params, @@ -1569,7 +1482,7 @@ export class File extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/file/status", ...options, ...params, @@ -1601,7 +1514,7 @@ export class Instance extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/instance/dispose", ...options, ...params, @@ -1633,7 +1546,7 @@ export class Path extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/path", ...options, ...params, @@ -1665,7 +1578,7 @@ export class Vcs extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/vcs", ...options, ...params, @@ -1697,7 +1610,7 @@ export class Vcs extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/vcs/diff", ...options, ...params, @@ -1729,7 +1642,7 @@ export class Command extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/command", ...options, ...params, @@ -1761,7 +1674,7 @@ export class Lsp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/lsp", ...options, ...params, @@ -1793,7 +1706,7 @@ export class Formatter extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/formatter", ...options, ...params, @@ -1964,7 +1877,7 @@ export class Mcp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/mcp", ...options, ...params, @@ -2033,7 +1946,7 @@ export class Mcp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/mcp/{name}/connect", ...options, ...params, @@ -2063,7 +1976,7 @@ export class Mcp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/mcp/{name}/disconnect", ...options, ...params, @@ -2100,7 +2013,7 @@ export class Project extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/project", ...options, ...params, @@ -2130,7 +2043,7 @@ export class Project extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/project/current", ...options, ...params, @@ -2160,7 +2073,7 @@ export class Project extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/project/git/init", ...options, ...params, @@ -2244,7 +2157,7 @@ export class Pty extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/pty/shells", ...options, ...params, @@ -2274,7 +2187,7 @@ export class Pty extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/pty", ...options, ...params, @@ -2525,7 +2438,7 @@ export class Question extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/question", ...options, ...params, @@ -2628,7 +2541,7 @@ export class Permission extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/permission", ...options, ...params, @@ -2838,7 +2751,7 @@ export class Provider extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/provider", ...options, ...params, @@ -2868,7 +2781,7 @@ export class Provider extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/provider/auth", ...options, ...params, @@ -2917,7 +2830,7 @@ export class Session2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/session", ...options, ...params, @@ -3205,7 +3118,7 @@ export class Session2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/session/{sessionID}/diff", ...options, ...params, @@ -3407,7 +3320,7 @@ export class Session2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/session/{sessionID}/fork", ...options, ...params, @@ -3983,7 +3896,7 @@ export class Sync extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/sync/start", ...options, ...params, @@ -4148,7 +4061,7 @@ export class Session3 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/api/session/{sessionID}/prompt", ...options, ...params, @@ -4185,7 +4098,7 @@ export class Session3 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/api/session/{sessionID}/compact", ...options, ...params, @@ -4217,7 +4130,7 @@ export class Session3 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/api/session/{sessionID}/wait", ...options, ...params, @@ -4249,7 +4162,7 @@ export class Session3 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/api/session/{sessionID}/context", ...options, ...params, @@ -4320,7 +4233,7 @@ export class Control extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).get({ url: "/tui/control/next", ...options, ...params, @@ -4352,7 +4265,7 @@ export class Control extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/control/response", ...options, ...params, @@ -4426,7 +4339,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/open-help", ...options, ...params, @@ -4456,7 +4369,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/open-sessions", ...options, ...params, @@ -4486,7 +4399,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/open-themes", ...options, ...params, @@ -4516,7 +4429,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/open-models", ...options, ...params, @@ -4546,7 +4459,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/submit-prompt", ...options, ...params, @@ -4576,7 +4489,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/clear-prompt", ...options, ...params, @@ -4651,7 +4564,7 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/tui/show-toast", ...options, ...params, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index a40b567f8c..c0255754d9 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3345,15 +3345,6 @@ export type GlobalHealthData = { url: "/global/health" } -export type GlobalHealthErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type GlobalHealthError = GlobalHealthErrors[keyof GlobalHealthErrors] - export type GlobalHealthResponses = { /** * Health information @@ -3373,15 +3364,6 @@ export type GlobalEventData = { url: "/global/event" } -export type GlobalEventErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type GlobalEventError = GlobalEventErrors[keyof GlobalEventErrors] - export type GlobalEventResponses = { /** * Event stream @@ -3398,15 +3380,6 @@ export type GlobalConfigGetData = { url: "/global/config" } -export type GlobalConfigGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type GlobalConfigGetError = GlobalConfigGetErrors[keyof GlobalConfigGetErrors] - export type GlobalConfigGetResponses = { /** * Get global config info @@ -3448,15 +3421,6 @@ export type GlobalDisposeData = { url: "/global/dispose" } -export type GlobalDisposeErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type GlobalDisposeError = GlobalDisposeErrors[keyof GlobalDisposeErrors] - export type GlobalDisposeResponses = { /** * Global disposed @@ -3511,15 +3475,6 @@ export type EventSubscribeData = { url: "/event" } -export type EventSubscribeErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type EventSubscribeError = EventSubscribeErrors[keyof EventSubscribeErrors] - export type EventSubscribeResponses = { /** * Event stream @@ -3539,15 +3494,6 @@ export type ConfigGetData = { url: "/config" } -export type ConfigGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ConfigGetError = ConfigGetErrors[keyof ConfigGetErrors] - export type ConfigGetResponses = { /** * Get config info @@ -3595,15 +3541,6 @@ export type ConfigProvidersData = { url: "/config/providers" } -export type ConfigProvidersErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ConfigProvidersError = ConfigProvidersErrors[keyof ConfigProvidersErrors] - export type ConfigProvidersResponses = { /** * List of providers @@ -3628,15 +3565,6 @@ export type ExperimentalConsoleGetData = { url: "/experimental/console" } -export type ExperimentalConsoleGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalConsoleGetError = ExperimentalConsoleGetErrors[keyof ExperimentalConsoleGetErrors] - export type ExperimentalConsoleGetResponses = { /** * Active Console provider metadata @@ -3656,16 +3584,6 @@ export type ExperimentalConsoleListOrgsData = { url: "/experimental/console/orgs" } -export type ExperimentalConsoleListOrgsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalConsoleListOrgsError = - ExperimentalConsoleListOrgsErrors[keyof ExperimentalConsoleListOrgsErrors] - export type ExperimentalConsoleListOrgsResponses = { /** * Switchable Console orgs @@ -3804,15 +3722,6 @@ export type WorktreeListData = { url: "/experimental/worktree" } -export type WorktreeListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type WorktreeListError = WorktreeListErrors[keyof WorktreeListErrors] - export type WorktreeListResponses = { /** * List of worktree directories @@ -3894,15 +3803,6 @@ export type ExperimentalSessionListData = { url: "/experimental/session" } -export type ExperimentalSessionListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalSessionListError = ExperimentalSessionListErrors[keyof ExperimentalSessionListErrors] - export type ExperimentalSessionListResponses = { /** * List of sessions @@ -3922,15 +3822,6 @@ export type ExperimentalResourceListData = { url: "/experimental/resource" } -export type ExperimentalResourceListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalResourceListError = ExperimentalResourceListErrors[keyof ExperimentalResourceListErrors] - export type ExperimentalResourceListResponses = { /** * MCP resources @@ -3954,15 +3845,6 @@ export type FindTextData = { url: "/find" } -export type FindTextErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FindTextError = FindTextErrors[keyof FindTextErrors] - export type FindTextResponses = { /** * Matches @@ -4002,15 +3884,6 @@ export type FindFilesData = { url: "/find/file" } -export type FindFilesErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FindFilesError = FindFilesErrors[keyof FindFilesErrors] - export type FindFilesResponses = { /** * File paths @@ -4031,15 +3904,6 @@ export type FindSymbolsData = { url: "/find/symbol" } -export type FindSymbolsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FindSymbolsError = FindSymbolsErrors[keyof FindSymbolsErrors] - export type FindSymbolsResponses = { /** * Symbols @@ -4060,15 +3924,6 @@ export type FileListData = { url: "/file" } -export type FileListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FileListError = FileListErrors[keyof FileListErrors] - export type FileListResponses = { /** * Files and directories @@ -4089,15 +3944,6 @@ export type FileReadData = { url: "/file/content" } -export type FileReadErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FileReadError = FileReadErrors[keyof FileReadErrors] - export type FileReadResponses = { /** * File content @@ -4117,15 +3963,6 @@ export type FileStatusData = { url: "/file/status" } -export type FileStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FileStatusError = FileStatusErrors[keyof FileStatusErrors] - export type FileStatusResponses = { /** * File status @@ -4145,15 +3982,6 @@ export type InstanceDisposeData = { url: "/instance/dispose" } -export type InstanceDisposeErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type InstanceDisposeError = InstanceDisposeErrors[keyof InstanceDisposeErrors] - export type InstanceDisposeResponses = { /** * Instance disposed @@ -4173,15 +4001,6 @@ export type PathGetData = { url: "/path" } -export type PathGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PathGetError = PathGetErrors[keyof PathGetErrors] - export type PathGetResponses = { /** * Path @@ -4201,15 +4020,6 @@ export type VcsGetData = { url: "/vcs" } -export type VcsGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type VcsGetError = VcsGetErrors[keyof VcsGetErrors] - export type VcsGetResponses = { /** * VCS info @@ -4230,15 +4040,6 @@ export type VcsDiffData = { url: "/vcs/diff" } -export type VcsDiffErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type VcsDiffError = VcsDiffErrors[keyof VcsDiffErrors] - export type VcsDiffResponses = { /** * VCS diff @@ -4258,15 +4059,6 @@ export type CommandListData = { url: "/command" } -export type CommandListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type CommandListError = CommandListErrors[keyof CommandListErrors] - export type CommandListResponses = { /** * List of commands @@ -4286,15 +4078,6 @@ export type AppAgentsData = { url: "/agent" } -export type AppAgentsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AppAgentsError = AppAgentsErrors[keyof AppAgentsErrors] - export type AppAgentsResponses = { /** * List of agents @@ -4314,15 +4097,6 @@ export type AppSkillsData = { url: "/skill" } -export type AppSkillsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AppSkillsError = AppSkillsErrors[keyof AppSkillsErrors] - export type AppSkillsResponses = { /** * List of skills @@ -4347,15 +4121,6 @@ export type LspStatusData = { url: "/lsp" } -export type LspStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type LspStatusError = LspStatusErrors[keyof LspStatusErrors] - export type LspStatusResponses = { /** * LSP server status @@ -4375,15 +4140,6 @@ export type FormatterStatusData = { url: "/formatter" } -export type FormatterStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FormatterStatusError = FormatterStatusErrors[keyof FormatterStatusErrors] - export type FormatterStatusResponses = { /** * Formatter status @@ -4403,15 +4159,6 @@ export type McpStatusData = { url: "/mcp" } -export type McpStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type McpStatusError = McpStatusErrors[keyof McpStatusErrors] - export type McpStatusResponses = { /** * MCP server status @@ -4469,10 +4216,6 @@ export type McpAuthRemoveData = { } export type McpAuthRemoveErrors = { - /** - * Bad request - */ - 400: BadRequestError /** * Not found */ @@ -4506,7 +4249,7 @@ export type McpAuthStartData = { export type McpAuthStartErrors = { /** - * McpUnsupportedOAuthError | BadRequest + * McpUnsupportedOAuthError */ 400: McpUnsupportedOAuthError /** @@ -4579,7 +4322,7 @@ export type McpAuthAuthenticateData = { export type McpAuthAuthenticateErrors = { /** - * McpUnsupportedOAuthError | BadRequest + * McpUnsupportedOAuthError */ 400: McpUnsupportedOAuthError /** @@ -4611,15 +4354,6 @@ export type McpConnectData = { url: "/mcp/{name}/connect" } -export type McpConnectErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type McpConnectError = McpConnectErrors[keyof McpConnectErrors] - export type McpConnectResponses = { /** * MCP server connected successfully @@ -4641,15 +4375,6 @@ export type McpDisconnectData = { url: "/mcp/{name}/disconnect" } -export type McpDisconnectErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type McpDisconnectError = McpDisconnectErrors[keyof McpDisconnectErrors] - export type McpDisconnectResponses = { /** * MCP server disconnected successfully @@ -4669,15 +4394,6 @@ export type ProjectListData = { url: "/project" } -export type ProjectListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProjectListError = ProjectListErrors[keyof ProjectListErrors] - export type ProjectListResponses = { /** * List of projects @@ -4697,15 +4413,6 @@ export type ProjectCurrentData = { url: "/project/current" } -export type ProjectCurrentErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProjectCurrentError = ProjectCurrentErrors[keyof ProjectCurrentErrors] - export type ProjectCurrentResponses = { /** * Current project information @@ -4725,15 +4432,6 @@ export type ProjectInitGitData = { url: "/project/git/init" } -export type ProjectInitGitErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProjectInitGitError = ProjectInitGitErrors[keyof ProjectInitGitErrors] - export type ProjectInitGitResponses = { /** * Project information after git initialization @@ -4800,15 +4498,6 @@ export type PtyShellsData = { url: "/pty/shells" } -export type PtyShellsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PtyShellsError = PtyShellsErrors[keyof PtyShellsErrors] - export type PtyShellsResponses = { /** * List of shells @@ -4832,15 +4521,6 @@ export type PtyListData = { url: "/pty" } -export type PtyListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PtyListError = PtyListErrors[keyof PtyListErrors] - export type PtyListResponses = { /** * List of sessions @@ -4899,10 +4579,6 @@ export type PtyRemoveData = { } export type PtyRemoveErrors = { - /** - * Bad request - */ - 400: BadRequestError /** * Not found */ @@ -4933,10 +4609,6 @@ export type PtyGetData = { } export type PtyGetErrors = { - /** - * Bad request - */ - 400: BadRequestError /** * Not found */ @@ -5003,10 +4675,6 @@ export type PtyConnectTokenData = { } export type PtyConnectTokenErrors = { - /** - * Bad request - */ - 400: BadRequestError /** * Forbidden */ @@ -5041,15 +4709,6 @@ export type QuestionListData = { url: "/question" } -export type QuestionListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type QuestionListError = QuestionListErrors[keyof QuestionListErrors] - export type QuestionListResponses = { /** * List of pending questions @@ -5142,15 +4801,6 @@ export type PermissionListData = { url: "/permission" } -export type PermissionListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PermissionListError = PermissionListErrors[keyof PermissionListErrors] - export type PermissionListResponses = { /** * List of pending permissions @@ -5207,15 +4857,6 @@ export type ProviderListData = { url: "/provider" } -export type ProviderListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProviderListError = ProviderListErrors[keyof ProviderListErrors] - export type ProviderListResponses = { /** * List of providers @@ -5241,15 +4882,6 @@ export type ProviderAuthData = { url: "/provider/auth" } -export type ProviderAuthErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProviderAuthError2 = ProviderAuthErrors[keyof ProviderAuthErrors] - export type ProviderAuthResponses = { /** * Provider auth methods @@ -5351,15 +4983,6 @@ export type SessionListData = { url: "/session" } -export type SessionListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SessionListError = SessionListErrors[keyof SessionListErrors] - export type SessionListResponses = { /** * List of sessions @@ -5627,15 +5250,6 @@ export type SessionDiffData = { url: "/session/{sessionID}/diff" } -export type SessionDiffErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SessionDiffError = SessionDiffErrors[keyof SessionDiffErrors] - export type SessionDiffResponses = { /** * Successfully retrieved diff @@ -5823,15 +5437,6 @@ export type SessionForkData = { url: "/session/{sessionID}/fork" } -export type SessionForkErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SessionForkError = SessionForkErrors[keyof SessionForkErrors] - export type SessionForkResponses = { /** * 200 @@ -6355,15 +5960,6 @@ export type SyncStartData = { url: "/sync/start" } -export type SyncStartErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SyncStartError = SyncStartErrors[keyof SyncStartErrors] - export type SyncStartResponses = { /** * Workspace sync started @@ -6527,15 +6123,6 @@ export type V2SessionPromptData = { url: "/api/session/{sessionID}/prompt" } -export type V2SessionPromptErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type V2SessionPromptError = V2SessionPromptErrors[keyof V2SessionPromptErrors] - export type V2SessionPromptResponses = { /** * Session.Message @@ -6557,15 +6144,6 @@ export type V2SessionCompactData = { url: "/api/session/{sessionID}/compact" } -export type V2SessionCompactErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type V2SessionCompactError = V2SessionCompactErrors[keyof V2SessionCompactErrors] - export type V2SessionCompactResponses = { /** * @@ -6587,15 +6165,6 @@ export type V2SessionWaitData = { url: "/api/session/{sessionID}/wait" } -export type V2SessionWaitErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type V2SessionWaitError = V2SessionWaitErrors[keyof V2SessionWaitErrors] - export type V2SessionWaitResponses = { /** * @@ -6617,15 +6186,6 @@ export type V2SessionContextData = { url: "/api/session/{sessionID}/context" } -export type V2SessionContextErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type V2SessionContextError = V2SessionContextErrors[keyof V2SessionContextErrors] - export type V2SessionContextResponses = { /** * Success @@ -6705,15 +6265,6 @@ export type TuiOpenHelpData = { url: "/tui/open-help" } -export type TuiOpenHelpErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiOpenHelpError = TuiOpenHelpErrors[keyof TuiOpenHelpErrors] - export type TuiOpenHelpResponses = { /** * Help dialog opened successfully @@ -6733,15 +6284,6 @@ export type TuiOpenSessionsData = { url: "/tui/open-sessions" } -export type TuiOpenSessionsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiOpenSessionsError = TuiOpenSessionsErrors[keyof TuiOpenSessionsErrors] - export type TuiOpenSessionsResponses = { /** * Session dialog opened successfully @@ -6761,15 +6303,6 @@ export type TuiOpenThemesData = { url: "/tui/open-themes" } -export type TuiOpenThemesErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiOpenThemesError = TuiOpenThemesErrors[keyof TuiOpenThemesErrors] - export type TuiOpenThemesResponses = { /** * Theme dialog opened successfully @@ -6789,15 +6322,6 @@ export type TuiOpenModelsData = { url: "/tui/open-models" } -export type TuiOpenModelsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiOpenModelsError = TuiOpenModelsErrors[keyof TuiOpenModelsErrors] - export type TuiOpenModelsResponses = { /** * Model dialog opened successfully @@ -6817,15 +6341,6 @@ export type TuiSubmitPromptData = { url: "/tui/submit-prompt" } -export type TuiSubmitPromptErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiSubmitPromptError = TuiSubmitPromptErrors[keyof TuiSubmitPromptErrors] - export type TuiSubmitPromptResponses = { /** * Prompt submitted successfully @@ -6845,15 +6360,6 @@ export type TuiClearPromptData = { url: "/tui/clear-prompt" } -export type TuiClearPromptErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiClearPromptError = TuiClearPromptErrors[keyof TuiClearPromptErrors] - export type TuiClearPromptResponses = { /** * Prompt cleared successfully @@ -6908,15 +6414,6 @@ export type TuiShowToastData = { url: "/tui/show-toast" } -export type TuiShowToastErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiShowToastError = TuiShowToastErrors[keyof TuiShowToastErrors] - export type TuiShowToastResponses = { /** * Toast notification shown successfully @@ -7001,15 +6498,6 @@ export type TuiControlNextData = { url: "/tui/control/next" } -export type TuiControlNextErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiControlNextError = TuiControlNextErrors[keyof TuiControlNextErrors] - export type TuiControlNextResponses = { /** * Next TUI request @@ -7032,15 +6520,6 @@ export type TuiControlResponseData = { url: "/tui/control/response" } -export type TuiControlResponseErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiControlResponseError = TuiControlResponseErrors[keyof TuiControlResponseErrors] - export type TuiControlResponseResponses = { /** * Response submitted successfully @@ -7060,16 +6539,6 @@ export type ExperimentalWorkspaceAdapterListData = { url: "/experimental/workspace/adapter" } -export type ExperimentalWorkspaceAdapterListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceAdapterListError = - ExperimentalWorkspaceAdapterListErrors[keyof ExperimentalWorkspaceAdapterListErrors] - export type ExperimentalWorkspaceAdapterListResponses = { /** * Workspace adapters @@ -7094,15 +6563,6 @@ export type ExperimentalWorkspaceListData = { url: "/experimental/workspace" } -export type ExperimentalWorkspaceListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceListError = ExperimentalWorkspaceListErrors[keyof ExperimentalWorkspaceListErrors] - export type ExperimentalWorkspaceListResponses = { /** * Workspaces @@ -7118,7 +6578,7 @@ export type ExperimentalWorkspaceCreateData = { id?: string type: string branch: string | null - extra: unknown | null + extra?: unknown | null } path?: never query?: { @@ -7158,16 +6618,6 @@ export type ExperimentalWorkspaceStatusData = { url: "/experimental/workspace/status" } -export type ExperimentalWorkspaceStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceStatusError = - ExperimentalWorkspaceStatusErrors[keyof ExperimentalWorkspaceStatusErrors] - export type ExperimentalWorkspaceStatusResponses = { /** * Workspace status @@ -7215,7 +6665,7 @@ export type ExperimentalWorkspaceRemoveResponse = export type ExperimentalWorkspaceWarpData = { body?: { - id: string | null + id: string sessionID: string } path?: never @@ -7258,10 +6708,6 @@ export type PtyConnectData = { } export type PtyConnectErrors = { - /** - * Bad request - */ - 400: BadRequestError /** * Forbidden */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 1a2f1e9475..db8889f1a4 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -218,16 +218,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get health information about the OpenCode server.", @@ -255,16 +245,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Subscribe to global events from the OpenCode system using server-sent events.", @@ -292,16 +272,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Retrieve the current global OpenCode configuration settings and preferences.", @@ -374,16 +344,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Clean up and dispose all OpenCode instances, releasing all resources.", @@ -510,16 +470,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get events", @@ -564,16 +514,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Retrieve the current OpenCode configuration settings and preferences.", @@ -696,16 +636,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of all configured AI providers and their default models.", @@ -750,16 +680,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get the active Console org name and the set of provider IDs managed by that Console org.", @@ -837,16 +757,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get the available Console orgs across logged-in accounts, including the current active org.", @@ -1083,16 +993,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "List all sandbox worktrees for the current project.", @@ -1392,16 +1292,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.", @@ -1450,16 +1340,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get all available MCP resources from connected servers. Optionally filter by name.", @@ -1576,16 +1456,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Search for text patterns across files in the project using ripgrep.", @@ -1670,16 +1540,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Search for files or directories by name or pattern in the project directory.", @@ -1736,16 +1596,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Search for workspace symbols like functions, classes, and variables using LSP.", @@ -1802,16 +1652,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "List files and directories in a specified path.", @@ -1864,16 +1704,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Read the content of a specified file.", @@ -1922,16 +1752,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get the git status of all files in the project.", @@ -1977,16 +1797,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Clean up and dispose the current OpenCode instance, releasing all resources.", @@ -2031,16 +1841,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Retrieve the current working directory and related path information for the OpenCode instance.", @@ -2085,16 +1885,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Retrieve version control system (VCS) information for the current project, such as git branch.", @@ -2152,16 +1942,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Retrieve the current git diff for the working tree or against the default branch.", @@ -2210,16 +1990,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of all available commands in the OpenCode system.", @@ -2268,16 +2038,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of all available AI agents in the OpenCode system.", @@ -2342,16 +2102,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of all available skills in the OpenCode system.", @@ -2400,16 +2150,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get LSP server status", @@ -2458,16 +2198,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get formatter status", @@ -2516,16 +2246,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get the status of all Model Context Protocol (MCP) servers.", @@ -2673,7 +2393,7 @@ } }, "400": { - "description": "McpUnsupportedOAuthError | BadRequest", + "description": "McpUnsupportedOAuthError", "content": { "application/json": { "schema": { @@ -2751,16 +2471,6 @@ } } }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, "404": { "description": "Not found", "content": { @@ -2912,7 +2622,7 @@ } }, "400": { - "description": "McpUnsupportedOAuthError | BadRequest", + "description": "McpUnsupportedOAuthError", "content": { "application/json": { "schema": { @@ -2983,16 +2693,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Connect an MCP server.", @@ -3045,16 +2745,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Disconnect an MCP server.", @@ -3102,16 +2792,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of projects that have been opened with OpenCode.", @@ -3156,16 +2836,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Retrieve the currently active project that OpenCode is working with.", @@ -3210,16 +2880,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Create a git repository for the current project and return the refreshed project info.", @@ -3393,16 +3053,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of available shells on the system.", @@ -3451,16 +3101,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.", @@ -3600,16 +3240,6 @@ } } }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, "404": { "description": "Not found", "content": { @@ -3763,16 +3393,6 @@ } } }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, "404": { "description": "Not found", "content": { @@ -3848,16 +3468,6 @@ } } }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, "403": { "description": "Forbidden", "content": { @@ -3925,16 +3535,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get all pending question requests across all sessions.", @@ -4151,16 +3751,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get all pending permission requests across all sessions.", @@ -4322,16 +3912,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of all available AI providers, including both available and connected ones.", @@ -4383,16 +3963,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Retrieve available authentication methods for all AI providers.", @@ -4666,16 +4236,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get a list of all OpenCode sessions, sorted by most recently updated.", @@ -5292,16 +4852,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get the file changes (diff) that resulted from a specific user message in the session.", @@ -5792,16 +5342,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Create a new session by forking an existing session at a specific message point.", @@ -7128,16 +6668,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Start sync loops for workspaces in the current project that have active sessions.", @@ -7519,16 +7049,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Create a v2 session message and queue it for the agent loop.", @@ -7594,16 +7114,6 @@ "responses": { "204": { "description": "" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Compact a v2 session conversation.", @@ -7650,16 +7160,6 @@ "responses": { "204": { "description": "" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Wait for a v2 session agent loop to become idle.", @@ -7716,16 +7216,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Retrieve the active context messages for a v2 session (all messages after the last compaction).", @@ -7905,16 +7395,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Open the help dialog in the TUI to display user assistance information.", @@ -7960,16 +7440,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Open the session dialog.", @@ -8015,16 +7485,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Open the theme dialog.", @@ -8070,16 +7530,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Open the model dialog.", @@ -8125,16 +7575,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Submit the prompt.", @@ -8180,16 +7620,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Clear the prompt.", @@ -8306,16 +7736,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Show a toast notification in the TUI.", @@ -8555,16 +7975,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Retrieve the next TUI request from the queue for processing.", @@ -8610,16 +8020,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Submit a response to the TUI request queue to complete a pending request.", @@ -8688,16 +8088,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "List all available workspace adapters for the current project.", @@ -8746,16 +8136,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "List all workspaces.", @@ -8843,7 +8223,7 @@ ] } }, - "required": ["type", "branch", "extra"], + "required": ["type", "branch"], "additionalProperties": false } } @@ -8904,16 +8284,6 @@ } } } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } } }, "description": "Get connection status for workspaces in the current project.", @@ -9098,16 +8468,6 @@ } } }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, "403": { "description": "Forbidden", "content": { From 2740d398fa26df560eeb0566226004677c84d0c2 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 5 May 2026 11:37:18 +1000 Subject: [PATCH 21/27] devex: Enable Electron MCP servers with DevTools debug port (#25795) --- packages/desktop-electron/src/main/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index c9f16606ae..af7fd42583 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -74,6 +74,7 @@ setupApp() function setupApp() { ensureLoopbackNoProxy() app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>") + if (!app.isPackaged) app.commandLine.appendSwitch("remote-debugging-port", "9222") if (!app.requestSingleInstanceLock()) { app.quit() From edd480f56be832bd3daa871b5bbb6c124bc10a4e Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 4 May 2026 22:06:33 -0400 Subject: [PATCH 22/27] fix(tui): fix type error for calling workspace.warp (#25801) --- .../src/cli/cmd/tui/component/dialog-workspace-create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index e2af0d63e1..ad40637575 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -77,7 +77,7 @@ export async function warpWorkspaceSession(input: { }): Promise { const result = await input.sdk.client.experimental.workspace .warp({ - id: input.workspaceID, + id: input.workspaceID ?? undefined, sessionID: input.sessionID, }) .catch(() => undefined) From f6a3615f59e51dec879a7f8d0cce584b05d4c9e2 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Tue, 5 May 2026 10:15:00 +0800 Subject: [PATCH 23/27] fix(console): remove Cloudflare cache config from download fetch (#25804) --- .../app/src/routes/download/[channel]/[platform].ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/console/app/src/routes/download/[channel]/[platform].ts b/packages/console/app/src/routes/download/[channel]/[platform].ts index 4ae8e2465f..7a4b5ef65e 100644 --- a/packages/console/app/src/routes/download/[channel]/[platform].ts +++ b/packages/console/app/src/routes/download/[channel]/[platform].ts @@ -32,13 +32,6 @@ export async function GET({ params: { platform, channel } }: APIEvent) { const resp = await fetch( `https://github.com/anomalyco/${channel === "stable" ? "opencode" : "opencode-beta"}/releases/latest/download/${assetName}`, - { - cf: { - // in case gh releases has rate limits - cacheTtl: 60 * 5, - cacheEverything: true, - }, - } as any, ) const downloadName = downloadNames[platform] From 0df2bb0f3b29b8b98d80c0bd3b1d5c8aac21098f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 4 May 2026 22:22:39 -0400 Subject: [PATCH 24/27] docs: restore v2 todo --- specs/v2/todo.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 specs/v2/todo.md diff --git a/specs/v2/todo.md b/specs/v2/todo.md new file mode 100644 index 0000000000..3a4b9cf241 --- /dev/null +++ b/specs/v2/todo.md @@ -0,0 +1,59 @@ +# TODO + +ok we need to work towards a launch of v2 so we can get out of this rebuild phase + +## Kill Hono - Kit + +Hono needs to go away so zod can go away. this is almost done + +## New Data Mode - Dax + +This is mostly done. I'm working through modeling subagents, skill invocations +and shell commands. + +## Rework agent loop - Kit? + +I think this needs to be done so we can take advantage of the simpler data +model. It can stop doing all the + +## Rework compaction - Aiden? + +The new agent loop needs to trigger compaction properly + +## Plugin API design - ??? + +We need to figure out how we want server plugins to work and what hooks are useful. + +Some ideas: + +- plugins get immer drafts so bad mutations can be thrown away +- plugins get global "opencode" instance like in that post i showed +- opencode instance has stuff like `opencode.session.prompt()` or + `opencode.tool.register({...})` + +## Rework Config - ??? + +We should do another pass on config to clean up any mistakes we made with it and +simplify as much as possible. Old configs should get auto-converted to new + +## Auth - ??? + +I have a basic auth system that can track any kind of auth, not just providers + +## Model Database - ??? + +I have a basic model service that allows for models to be registered dynamically + +## Provider - ??? + +Providers should register as plugins and autoload based on whatever logic they +want / config. They should register models into model database + +## Event - Kit/James + +I have this v2/event.ts but it needs to be self contained instead of using the +old bus system + +## Everything is hotreloadable - ??? + +Instead of needing to tear down things when something changes every service should emit granular events so services can react to them and reconfigure themselves. Allows frontend to receive these too, eg model.added. also prevents startup from blocking From 39c88f9afb2281ae3df290f4d88acaf2f8e8398b Mon Sep 17 00:00:00 2001 From: Dax Date: Mon, 4 May 2026 22:35:21 -0400 Subject: [PATCH 25/27] Improve v2 session message rendering (#25634) --- packages/core/src/global.ts | 2 + .../src/cli/cmd/tui/context/sync-v2.tsx | 16 +- .../tui/feature-plugins/system/session-v2.tsx | 193 +++++++++----- packages/opencode/src/id/id.ts | 1 + packages/opencode/src/session/processor.ts | 9 +- .../opencode/src/session/projectors-next.ts | 6 +- packages/opencode/src/session/prompt.ts | 9 +- packages/opencode/src/v2/auth.ts | 246 ++++++++++++++++++ packages/opencode/src/v2/model.ts | 192 ++++++++++++++ packages/opencode/src/v2/session-event.ts | 23 +- .../src/v2/session-message-updater.ts | 6 +- packages/opencode/src/v2/session-message.ts | 12 +- packages/opencode/src/v2/session.ts | 76 ++++-- .../test/server/httpapi-session.test.ts | 7 +- .../test/v2/session-message-updater.test.ts | 19 +- specs/v2/session-concepts-gap.md | 131 ---------- specs/v2/todo.md | 4 +- 17 files changed, 677 insertions(+), 275 deletions(-) create mode 100644 packages/opencode/src/v2/auth.ts create mode 100644 packages/opencode/src/v2/model.ts delete mode 100644 specs/v2/session-concepts-gap.md diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index 1acc3f47f1..6560d308c1 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -71,6 +71,8 @@ export const layer = Layer.effect( Effect.sync(() => Service.of(make())), ) +export const defaultLayer = layer + export const layerWith = (input: Partial) => Layer.effect( Service, diff --git a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx index 9801f0a2f8..d9d23999d2 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx @@ -11,21 +11,21 @@ import { createSimpleContext } from "./helper" import { useSDK } from "./sdk" function activeAssistant(messages: SessionMessage[]) { - const index = messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed) + const index = messages.findIndex((message) => message.type === "assistant" && !message.time.completed) if (index < 0) return const assistant = messages[index] return assistant?.type === "assistant" ? assistant : undefined } function activeCompaction(messages: SessionMessage[]) { - const index = messages.findLastIndex((message) => message.type === "compaction") + const index = messages.findIndex((message) => message.type === "compaction") if (index < 0) return const compaction = messages[index] return compaction?.type === "compaction" ? compaction : undefined } function activeShell(messages: SessionMessage[], callID: string) { - const index = messages.findLastIndex((message) => message.type === "shell" && message.callID === callID) + const index = messages.findIndex((message) => message.type === "shell" && message.callID === callID) if (index < 0) return const shell = messages[index] return shell?.type === "shell" ? shell : undefined @@ -74,7 +74,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( switch (event.type) { case "session.next.prompted": { update(event.properties.sessionID, (draft) => { - draft.push({ + draft.unshift({ id: event.id, type: "user", text: event.properties.prompt.text, @@ -87,7 +87,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( } case "session.next.synthetic": update(event.properties.sessionID, (draft) => { - draft.push({ + draft.unshift({ id: event.id, type: "synthetic", sessionID: event.properties.sessionID, @@ -98,7 +98,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( break case "session.next.shell.started": update(event.properties.sessionID, (draft) => { - draft.push({ + draft.unshift({ id: event.id, type: "shell", callID: event.properties.callID, @@ -120,7 +120,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( update(event.properties.sessionID, (draft) => { const currentAssistant = activeAssistant(draft) if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp - draft.push({ + draft.unshift({ id: event.id, type: "assistant", agent: event.properties.agent, @@ -259,7 +259,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( break case "session.next.compaction.started": update(event.properties.sessionID, (draft) => { - draft.push({ + draft.unshift({ id: event.id, type: "compaction", reason: event.properties.reason, 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 7270a9c3b7..2e5cea9804 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,7 +5,7 @@ import { Spinner } from "@tui/component/spinner" import { useTheme } from "@tui/context/theme" import { useLocal } from "@tui/context/local" import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" -import type { SyntaxStyle } from "@opentui/core" +import { TextAttributes, type BoxRenderable, type SyntaxStyle } from "@opentui/core" import { Locale } from "@/util/locale" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import path from "path" @@ -44,6 +44,10 @@ function View(props: { api: TuiPluginApi; sessionID: string }) { const messages = createMemo(() => sync.data.messages[props.sessionID] ?? []) const renderedMessages = createMemo(() => messages().toReversed()) const lastAssistant = createMemo(() => renderedMessages().findLast((message) => message.type === "assistant")) + const lastUserCreated = (index: number) => + renderedMessages() + .slice(0, index) + .findLast((message) => message.type === "user")?.time.created createEffect(() => { void sync.session.message.sync(props.sessionID) @@ -83,10 +87,11 @@ function View(props: { api: TuiPluginApi; sessionID: string }) { last={lastAssistant()?.id === message.id} syntax={syntax()} subtleSyntax={subtleSyntax()} + start={lastUserCreated(index())} /> - + <> @@ -146,63 +151,36 @@ function UserMessage(props: { message: SessionMessageUser; index: number }) { - - - } - > - {props.message.text} - - - - - {(file) => ( - - {file.mime} - {file.name ?? file.uri} - - )} - - - {(agent) => ( - - agent - {agent.name} - - )} - - - - {Locale.todayTimeOrDateTime(props.message.time.created)} - - - ) -} - -function SyntheticMessage(props: { message: SessionMessageSynthetic; index: number }) { - const { theme } = useTheme() - return ( - - Synthetic {props.message.text} + + + + {(file) => ( + + {file.mime} + {file.name ?? file.uri} + + )} + + + {(agent) => ( + + agent + {agent.name} + + )} + + + ) } @@ -237,7 +215,7 @@ function ShellMessage(props: { message: SessionMessageShell }) { } function CompactionMessage(props: { message: SessionMessageCompaction }) { - const { theme } = useTheme() + const { theme, syntax } = useTheme() return ( - {props.message.summary} + {(summary) => ( + + + + )} ) @@ -294,12 +284,13 @@ function AssistantMessage(props: { last: boolean syntax: SyntaxStyle subtleSyntax: SyntaxStyle + start?: number }) { const { theme } = useTheme() const local = useLocal() const duration = createMemo(() => { if (!props.message.time.completed) return 0 - return props.message.time.completed - props.message.time.created + return props.message.time.completed - (props.start ?? props.message.time.created) }) const model = createMemo(() => { const variant = props.message.model.variant ? `/${props.message.model.variant}` : "" @@ -361,7 +352,7 @@ function AssistantText(props: { part: SessionMessageAssistantText; syntax: Synta const { theme } = useTheme() return ( - + (props.part.state.status === "error" ? props.part.state.error.message : undefined)) + const complete = createMemo(() => !!props.complete) const denied = createMemo(() => { const message = error() if (!message) return false return ( message.includes("QuestionRejectedError") || message.includes("rejected permission") || + message.includes("specified a rule") || message.includes("user dismissed") ) }) + const fg = createMemo(() => { + if (error()) return theme.error + if (complete()) return theme.textMuted + return theme.text + }) + const attributes = createMemo(() => (denied() ? TextAttributes.STRIKETHROUGH : undefined)) return ( - - - - {props.children} - - - - ~ {props.pending}} when={props.complete}> - {props.icon} {props.children} - - - - - - {error()} - + error() && setHover(true)} + onMouseOut={() => setHover(false)} + onMouseUp={() => { + if (!error()) return + if (renderer.getSelection()?.getSelectedText()) return + setShowError((prev) => !prev) + }} + renderBefore={function () { + const el = this as BoxRenderable + const parent = el.parent + if (!parent) return + const previous = parent.getChildren()[parent.getChildren().indexOf(el) - 1] + if (!previous) { + setMargin(0) + return + } + if (previous.id.startsWith("text")) setMargin(1) + }} + > + + + + + + + + {props.icon} + + + + + ~ + + + + + + + + + + {props.children} + + + + + {props.pending} + + + + + + + {error()} + + + ) } diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts index 46c210fa5d..6d9a6447a0 100644 --- a/packages/opencode/src/id/id.ts +++ b/packages/opencode/src/id/id.ts @@ -13,6 +13,7 @@ const prefixes = { tool: "tool", workspace: "wrk", entry: "ent", + account: "act", } as const export function schema(prefix: keyof typeof prefixes) { diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index cf1a7e0ae9..f22da92927 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -22,6 +22,7 @@ import * as Log from "@opencode-ai/core/util/log" import { isRecord } from "@/util/record" import { EventV2 } from "@/v2/event" import { SessionEvent } from "@/v2/session-event" +import { Modelv2 } from "@/v2/model" import * as DateTime from "effect/DateTime" const DOOM_LOOP_THRESHOLD = 3 @@ -432,9 +433,9 @@ export const layer: Layer.Layer< sessionID: ctx.sessionID, agent: input.assistantMessage.agent, model: { - id: ctx.model.id, - providerID: ctx.model.providerID, - variant: input.assistantMessage.variant, + id: Modelv2.ID.make(ctx.model.id), + providerID: Modelv2.ProviderID.make(ctx.model.providerID), + variant: Modelv2.VariantID.make(input.assistantMessage.variant ?? "default"), }, snapshot: ctx.snapshot, timestamp: DateTime.makeUnsafe(Date.now()), @@ -655,7 +656,7 @@ export const layer: Layer.Layer< EventV2.run(SessionEvent.Step.Failed.Sync, { sessionID: ctx.sessionID, error: { - type: error.name, + type: "unknown", message: errorMessage(e), }, timestamp: DateTime.makeUnsafe(Date.now()), diff --git a/packages/opencode/src/session/projectors-next.ts b/packages/opencode/src/session/projectors-next.ts index 88f73acf1a..93298170cc 100644 --- a/packages/opencode/src/session/projectors-next.ts +++ b/packages/opencode/src/session/projectors-next.ts @@ -132,11 +132,7 @@ export default [ SyncEvent.project(SessionEvent.ModelSwitched.Sync, (db, data, event) => { db.update(SessionTable) .set({ - model: { - id: data.id, - providerID: data.providerID, - variant: data.variant, - }, + model: data.model, time_updated: DateTime.toEpochMillis(data.timestamp), }) .where(eq(SessionTable.id, data.sessionID)) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 0590fc3827..e1fa81abf1 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -56,6 +56,7 @@ import { SessionRunState } from "./run-state" import { EffectBridge } from "@/effect/bridge" import { EventV2 } from "@/v2/event" import { SessionEvent } from "@/v2/session-event" +import { Modelv2 } from "@/v2/model" import { AgentAttachment, FileAttachment, Source } from "@/v2/session-prompt" import * as DateTime from "effect/DateTime" import { eq } from "@/storage/db" @@ -978,9 +979,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the EventV2.run(SessionEvent.ModelSwitched.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(info.time.created), - id: info.model.modelID, - providerID: info.model.providerID, - variant: info.model.variant, + model: { + id: Modelv2.ID.make(info.model.modelID), + providerID: Modelv2.ProviderID.make(info.model.providerID), + variant: Modelv2.VariantID.make(info.model.variant ?? "default"), + }, }) } diff --git a/packages/opencode/src/v2/auth.ts b/packages/opencode/src/v2/auth.ts new file mode 100644 index 0000000000..1cc443974d --- /dev/null +++ b/packages/opencode/src/v2/auth.ts @@ -0,0 +1,246 @@ +import path from "path" +import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect" +import { Identifier } from "@opencode-ai/core/util/identifier" +import { NonNegativeInt, withStatics } from "@/util/schema" +import { Global } from "@opencode-ai/core/global" +import { AppFileSystem } from "@opencode-ai/core/filesystem" + +export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" + +const AccountID = Schema.String.pipe( + Schema.brand("AccountID"), + withStatics((schema) => ({ create: () => schema.make("acc_" + Identifier.ascending()) })), +) +export type AccountID = typeof AccountID.Type + +export const ServiceID = Schema.String.pipe(Schema.brand("ServiceID")) +export type ServiceID = typeof ServiceID.Type + +export class OAuthCredential extends Schema.Class("AuthV2.OAuthCredential")({ + type: Schema.Literal("oauth"), + refresh: Schema.String, + access: Schema.String, + expires: NonNegativeInt, +}) {} + +export class ApiKeyCredential extends Schema.Class("AuthV2.ApiKeyCredential")({ + type: Schema.Literal("api"), + key: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), +}) {} + +export const Credential = Schema.Union([OAuthCredential, ApiKeyCredential]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ + identifier: "AuthV2.Credential", + }) +export type Credential = Schema.Schema.Type + +export class Account extends Schema.Class("AuthV2.Account")({ + id: AccountID, + serviceID: ServiceID, + description: Schema.String, + credential: Credential, +}) {} + +export class AuthFileWriteError extends Schema.TaggedErrorClass()("AuthV2.FileWriteError", { + operation: Schema.Union([Schema.Literal("migrate"), Schema.Literal("write")]), + cause: Schema.Defect, +}) {} + +export type AuthError = AuthFileWriteError + +interface Writable { + version: 2 + accounts: Record + active: Record +} + +const decodeV1 = Schema.decodeUnknownOption(Schema.Record(Schema.String, Credential)) + +function migrate(old: Record): Writable { + const accounts: Record = {} + const active: Record = {} + for (const [serviceID, value] of Object.entries(old)) { + const decoded = Option.getOrElse(decodeV1({ [serviceID]: value }), () => ({})) + const parsed = (decoded as Record)[serviceID] + if (!parsed) continue + const id = Identifier.ascending() + const accountID = AccountID.make(id) + const brandedServiceID = ServiceID.make(serviceID) + accounts[id] = new Account({ + id: accountID, + serviceID: brandedServiceID, + description: "default", + credential: parsed, + }) + active[brandedServiceID] = accountID + } + return { version: 2, accounts, active } +} + +export interface Interface { + readonly get: (accountID: AccountID) => Effect.Effect + readonly all: () => Effect.Effect + readonly create: (input: { + serviceID: ServiceID + credential: Credential + description?: string + active?: boolean + }) => Effect.Effect + readonly update: ( + accountID: AccountID, + updates: Partial>, + ) => Effect.Effect + readonly remove: (accountID: AccountID) => Effect.Effect + readonly activate: (accountID: AccountID) => Effect.Effect + readonly active: (serviceID: ServiceID) => Effect.Effect + readonly forService: (serviceID: ServiceID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Auth") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fsys = yield* AppFileSystem.Service + const global = yield* Global.Service + const file = path.join(global.data, "auth-v2.json") + + const load: () => Effect.Effect = Effect.fnUntraced(function* () { + if (process.env.OPENCODE_AUTH_CONTENT) { + try { + return JSON.parse(process.env.OPENCODE_AUTH_CONTENT) + } catch {} + } + + const raw = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null)) + + if (!raw || typeof raw !== "object") return { version: 2, accounts: {}, active: {} } + + if ("version" in raw && raw.version === 2) return raw as Writable + + const migrated = migrate(raw as Record) + yield* fsys + .writeJson(file, migrated, 0o600) + .pipe(Effect.mapError((cause) => new AuthFileWriteError({ operation: "migrate", cause }))) + return migrated + }) + + const write = (data: Writable) => + fsys + .writeJson(file, data, 0o600) + .pipe(Effect.mapError((cause) => new AuthFileWriteError({ operation: "write", cause }))) + + const state = SynchronizedRef.makeUnsafe(yield* load()) + + const result: Interface = { + get: Effect.fn("AuthV2.get")(function* (accountID) { + return (yield* SynchronizedRef.get(state)).accounts[accountID] + }), + + all: Effect.fn("AuthV2.all")(function* () { + return Object.values((yield* SynchronizedRef.get(state)).accounts) + }), + + active: Effect.fn("AuthV2.active")(function* (serviceID) { + const data = yield* SynchronizedRef.get(state) + return ( + data.accounts[data.active[serviceID]] ?? Object.values(data.accounts).find((a) => a.serviceID === serviceID) + ) + }), + + forService: Effect.fn("AuthV2.list")(function* (serviceID) { + return Object.values((yield* SynchronizedRef.get(state)).accounts).filter((a) => a.serviceID === serviceID) + }), + + create: Effect.fn("AuthV2.add")(function* (input) { + return yield* SynchronizedRef.modifyEffect( + state, + Effect.fnUntraced(function* (data) { + const account = new Account({ + id: AccountID.make(Identifier.ascending()), + serviceID: input.serviceID, + description: input.description ?? "default", + credential: input.credential, + }) + const next = { + ...data, + accounts: { ...data.accounts, [account.id]: account }, + active: + (input.active ?? Object.values(data.accounts).every((a) => a.serviceID !== input.serviceID)) + ? { ...data.active, [input.serviceID]: account.id } + : data.active, + } + + yield* write(next) + return [account, next] as const + }), + ) + }), + + update: Effect.fn("AuthV2.update")(function* (accountID, updates) { + yield* SynchronizedRef.modifyEffect( + state, + Effect.fnUntraced(function* (data) { + const existing = data.accounts[accountID] + if (!existing) return [undefined, data] as const + + const next = { + ...data, + accounts: { + ...data.accounts, + [accountID]: new Account({ + id: accountID, + serviceID: existing.serviceID, + description: updates.description ?? existing.description, + credential: updates.credential ?? existing.credential, + }), + }, + } + + yield* write(next) + return [undefined, next] as const + }), + ) + }), + + remove: Effect.fn("AuthV2.remove")(function* (accountID) { + yield* SynchronizedRef.modifyEffect( + state, + Effect.fnUntraced(function* (data) { + const accounts = { ...data.accounts } + const active = { ...data.active } + if (accounts[accountID] && active[accounts[accountID].serviceID] === accountID) + delete active[accounts[accountID].serviceID] + delete accounts[accountID] + + const next = { ...data, accounts, active } + yield* write(next) + return [undefined, next] as const + }), + ) + }), + + activate: Effect.fn("AuthV2.activate")(function* (accountID) { + yield* SynchronizedRef.modifyEffect( + state, + Effect.fnUntraced(function* (data) { + const account = data.accounts[accountID] + if (!account) return [undefined, data] as const + + const next = { ...data, active: { ...data.active, [account.serviceID]: accountID } } + yield* write(next) + return [undefined, next] as const + }), + ) + }), + } + + return Service.of(result) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Global.defaultLayer)) + +export * as AuthV2 from "./auth" diff --git a/packages/opencode/src/v2/model.ts b/packages/opencode/src/v2/model.ts new file mode 100644 index 0000000000..db66199a59 --- /dev/null +++ b/packages/opencode/src/v2/model.ts @@ -0,0 +1,192 @@ +import { withStatics } from "@/util/schema" +import { Array, Context, Effect, HashMap, Layer, Option, Order, pipe, Schema } from "effect" +import { DateTimeUtcFromMillis } from "effect/Schema" + +export const ID = Schema.String.pipe(Schema.brand("Model.ID")) +export type ID = typeof ID.Type + +export const ProviderID = Schema.String.pipe( + Schema.brand("Model.ProviderID"), + withStatics((schema) => ({ + // Well-known providers + opencode: schema.make("opencode"), + anthropic: schema.make("anthropic"), + openai: schema.make("openai"), + google: schema.make("google"), + googleVertex: schema.make("google-vertex"), + githubCopilot: schema.make("github-copilot"), + amazonBedrock: schema.make("amazon-bedrock"), + azure: schema.make("azure"), + openrouter: schema.make("openrouter"), + mistral: schema.make("mistral"), + gitlab: schema.make("gitlab"), + })), +) +export type ProviderID = typeof ProviderID.Type + +export const VariantID = Schema.String.pipe(Schema.brand("VariantID")) +export type VariantID = typeof VariantID.Type + +// Grouping of models, eg claude opus, claude sonnet +export const Family = Schema.String.pipe(Schema.brand("Family")) +export type Family = typeof Family.Type + +const OpenAIResponses = Schema.Struct({ + type: Schema.Literal("openai/responses"), + url: Schema.String, + websocket: Schema.optional(Schema.Boolean), +}) + +const OpenAICompletions = Schema.Struct({ + type: Schema.Literal("openai/completions"), + url: Schema.String, + reasoning: Schema.Union([ + Schema.Struct({ + type: Schema.Literal("reasoning_content"), + }), + Schema.Struct({ + type: Schema.Literal("reasoning_details"), + }), + ]).pipe(Schema.optional), +}) +export type OpenAICompletions = typeof OpenAICompletions.Type + +const AnthropicMessages = Schema.Struct({ + type: Schema.Literal("anthropic/messages"), + url: Schema.String, +}) + +export const Endpoint = Schema.Union([OpenAIResponses, OpenAICompletions, AnthropicMessages]).pipe( + Schema.toTaggedUnion("type"), +) +export type Endpoint = typeof Endpoint.Type + +export const Capabilities = Schema.Struct({ + tools: Schema.Boolean, + // mime patterns, image, audio, video/*, text/* + input: Schema.String.pipe(Schema.Array), + output: Schema.String.pipe(Schema.Array), +}) +export type Capabilities = typeof Capabilities.Type + +export const Options = Schema.Struct({ + headers: Schema.Record(Schema.String, Schema.String), + body: Schema.Record(Schema.String, Schema.Any), +}) +export type Options = typeof Options.Type + +export const Cost = Schema.Struct({ + tier: Schema.Struct({ + type: Schema.Literal("context"), + size: Schema.Int, + }).pipe(Schema.optional), + input: Schema.Finite, + output: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), +}) + +export const Ref = Schema.Struct({ + id: ID, + providerID: ProviderID, + variant: VariantID, +}) +export type Ref = typeof Ref.Type + +export class Info extends Schema.Class("Model.Info")({ + id: ID, + providerID: ProviderID, + family: Family.pipe(Schema.optional), + name: Schema.String, + endpoint: Endpoint, + capabilities: Capabilities, + options: Schema.Struct({ + ...Options.fields, + variant: Schema.String.pipe(Schema.optional), + }), + variants: Schema.Struct({ + id: VariantID, + ...Options.fields, + }).pipe(Schema.Array), + time: Schema.Struct({ + released: DateTimeUtcFromMillis, + }), + cost: Cost.pipe(Schema.Array), + status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), + limit: Schema.Struct({ + context: Schema.Int, + input: Schema.Int.pipe(Schema.optional), + output: Schema.Int, + }), +}) {} + +export function parse(input: string): { providerID: ProviderID; modelID: ID } { + const [providerID, ...modelID] = input.split("/") + return { + providerID: ProviderID.make(providerID), + modelID: ID.make(modelID.join("/")), + } +} + +export interface Interface { + readonly get: (providerID: ProviderID, modelID: ID) => Effect.Effect> + readonly add: (model: Info) => Effect.Effect + readonly remove: (providerID: ProviderID, modelID: ID) => Effect.Effect + readonly all: () => Effect.Effect + readonly default: () => Effect.Effect> + readonly small: (provider: ProviderID) => Effect.Effect> +} + +export class Service extends Context.Service()("@opencode/v2/Model") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + let models = HashMap.empty() + + function key(providerID: ProviderID, modelID: ID) { + return `${providerID}/${modelID}` + } + + const result: Interface = { + get: Effect.fn("V2Model.get")(function* (providerID, modelID) { + return HashMap.get(models, key(providerID, modelID)) + }), + + add: Effect.fn("V2Model.add")(function* (model) { + models = HashMap.set(models, key(model.providerID, model.id), model) + }), + + remove: Effect.fn("V2Model.remove")(function* (providerID, modelID) { + models = HashMap.remove(models, key(providerID, modelID)) + }), + + all: Effect.fn("V2Model.all")(function* () { + return pipe( + models, + HashMap.toValues, + Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)), + ) + }), + + default: Effect.fn("V2Model.default")(function* () { + const all = yield* result.all() + return Option.fromUndefinedOr(all[0]) + }), + + small: Effect.fn("V2Model.small")(function* (providerID) { + const all = yield* result.all() + const match = all.find((model) => model.providerID === providerID && model.id.toLowerCase().includes("small")) + return Option.fromUndefinedOr(match) + }), + } + + return Service.of(result) + }), +) + +export const defaultLayer = layer + +export * as Modelv2 from "./model" diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index 47938dcbed..7c768bd551 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -5,8 +5,8 @@ import { FileAttachment, Prompt } from "./session-prompt" import { Schema } from "effect" export { FileAttachment } import { ToolOutput } from "./tool-output" -import { ModelID, ProviderID } from "@/provider/schema" import { V2Schema } from "./schema" +import { Modelv2 } from "./model" export const Source = Schema.Struct({ start: NonNegativeInt, @@ -22,10 +22,13 @@ const Base = { sessionID: SessionID, } -const Error = Schema.Struct({ - type: Schema.String, +export const UnknownError = Schema.Struct({ + type: Schema.Literal("unknown"), message: Schema.String, +}).annotate({ + identifier: "Session.Error.Unknown", }) +export type UnknownError = Schema.Schema.Type export const AgentSwitched = EventV2.define({ type: "session.next.agent.switched", @@ -44,9 +47,7 @@ export const ModelSwitched = EventV2.define({ version: 1, schema: { ...Base, - id: ModelID, - providerID: ProviderID, - variant: Schema.String.pipe(Schema.optional), + model: Modelv2.Ref, }, }) export type ModelSwitched = Schema.Schema.Type @@ -103,11 +104,7 @@ export namespace Step { schema: { ...Base, agent: Schema.String, - model: Schema.Struct({ - id: Schema.String, - providerID: Schema.String, - variant: Schema.String.pipe(Schema.optional), - }), + model: Modelv2.Ref, snapshot: Schema.String.pipe(Schema.optional), }, }) @@ -139,7 +136,7 @@ export namespace Step { aggregate: "sessionID", schema: { ...Base, - error: Error, + error: UnknownError, }, }) export type Failed = Schema.Schema.Type @@ -296,7 +293,7 @@ export namespace Tool { schema: { ...Base, callID: Schema.String, - error: Error, + error: UnknownError, provider: Schema.Struct({ executed: Schema.Boolean, metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), diff --git a/packages/opencode/src/v2/session-message-updater.ts b/packages/opencode/src/v2/session-message-updater.ts index d5d5aac7b7..80ecb1011e 100644 --- a/packages/opencode/src/v2/session-message-updater.ts +++ b/packages/opencode/src/v2/session-message-updater.ts @@ -109,11 +109,7 @@ export function update(adapter: Adapter, event: SessionEvent.Eve id: event.id, type: "model-switched", metadata: event.metadata, - model: { - id: event.data.id, - providerID: event.data.providerID, - variant: event.data.variant, - }, + model: event.data.model, time: { created: event.data.timestamp }, }), ) diff --git a/packages/opencode/src/v2/session-message.ts b/packages/opencode/src/v2/session-message.ts index 94f6b1cac2..024e28c450 100644 --- a/packages/opencode/src/v2/session-message.ts +++ b/packages/opencode/src/v2/session-message.ts @@ -4,6 +4,7 @@ import { SessionEvent } from "./session-event" import { EventV2 } from "./event" import { ToolOutput } from "./tool-output" import { V2Schema } from "./schema" +import { Modelv2 } from "./model" export const ID = EventV2.ID export type ID = Schema.Schema.Type @@ -25,11 +26,7 @@ export class AgentSwitched extends Schema.Class("Session.Message. export class ModelSwitched extends Schema.Class("Session.Message.ModelSwitched")({ ...Base, type: Schema.Literal("model-switched"), - model: Schema.Struct({ - id: SessionEvent.ModelSwitched.fields.data.fields.id, - providerID: SessionEvent.ModelSwitched.fields.data.fields.providerID, - variant: SessionEvent.ModelSwitched.fields.data.fields.variant, - }), + model: Modelv2.Ref, }) {} export class User extends Schema.Class("Session.Message.User")({ @@ -87,10 +84,7 @@ export class ToolStateError extends Schema.Class("Session.Messag input: Schema.Record(Schema.String, Schema.Unknown), content: ToolOutput.Content.pipe(Schema.Array), structured: ToolOutput.Structured, - error: Schema.Struct({ - type: Schema.String, - message: Schema.String, - }), + error: SessionEvent.UnknownError, }) {} export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index 1f4cbcf1e0..bb86f039b2 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -3,17 +3,17 @@ import { SessionID } from "@/session/schema" import { WorkspaceID } from "@/control-plane/schema" import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "@/storage/db" import * as Database from "@/storage/db" -import { Context, DateTime, Effect, Layer, Schema } from "effect" +import { Context, DateTime, Effect, Layer, Option, Schema } from "effect" import { SessionMessage } from "./session-message" import type { Prompt } from "./session-prompt" import { EventV2 } from "./event" import { ProjectID } from "@/project/schema" -import { ModelID, ProviderID } from "@/provider/schema" import { SessionEvent } from "./session-event" import { V2Schema } from "./schema" import { optionalOmitUndefined } from "@/util/schema" +import { Modelv2 } from "./model" -export const Delivery = Schema.Union([Schema.Literal("immediate"), Schema.Literal("deferred")]).annotate({ +export const Delivery = Schema.Literals(["immediate", "deferred"]).annotate({ identifier: "Session.Delivery", }) export type Delivery = Schema.Schema.Type @@ -27,11 +27,7 @@ export class Info extends Schema.Class("Session.Info")({ workspaceID: optionalOmitUndefined(WorkspaceID), path: optionalOmitUndefined(Schema.String), agent: optionalOmitUndefined(Schema.String), - model: Schema.Struct({ - id: ModelID, - providerID: ProviderID, - variant: optionalOmitUndefined(Schema.String), - }).pipe(optionalOmitUndefined), + model: Modelv2.Ref.pipe(optionalOmitUndefined), time: Schema.Struct({ created: V2Schema.DateTimeUtcFromMillis, updated: V2Schema.DateTimeUtcFromMillis, @@ -53,7 +49,18 @@ export class Info extends Schema.Class("Session.Info")({ */ }) {} +export class NotFoundError extends Schema.TaggedErrorClass()("Session.NotFoundError", { + sessionID: SessionID, +}) {} + export interface Interface { + readonly create: (input?: { + agent?: string + model?: Modelv2.Ref + parentID?: SessionID + workspaceID?: WorkspaceID + }) => Effect.Effect + readonly get: (sessionID: SessionID) => Effect.Effect readonly list: (input: { limit?: number order?: "asc" | "desc" @@ -88,13 +95,15 @@ export interface Interface { }) => Effect.Effect readonly shell: (input: { id?: EventV2.ID; sessionID: SessionID; command: string }) => Effect.Effect readonly skill: (input: { id?: EventV2.ID; sessionID: SessionID; skill: string }) => Effect.Effect + readonly subagent: (input: { + id?: EventV2.ID + parentID: SessionID + prompt: Prompt + agent: string + model?: Modelv2.Ref + }) => Effect.Effect readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect - readonly switchModel: (input: { - sessionID: SessionID - id: ModelID - providerID: ProviderID - variant?: string - }) => Effect.Effect + readonly switchModel: (input: { sessionID: SessionID; model: Modelv2.Ref }) => Effect.Effect readonly compact: (sessionID: SessionID) => Effect.Effect readonly wait: (sessionID: SessionID) => Effect.Effect } @@ -120,9 +129,9 @@ export const layer = Layer.effect( agent: row.agent ?? undefined, model: row.model ? { - id: ModelID.make(row.model.id), - providerID: ProviderID.make(row.model.providerID), - variant: row.model.variant, + id: Modelv2.ID.make(row.model.id), + providerID: Modelv2.ProviderID.make(row.model.providerID), + variant: Modelv2.VariantID.make(row.model.variant ?? "default"), } : undefined, time: { @@ -134,6 +143,14 @@ export const layer = Layer.effect( } const result: Interface = { + create: Effect.fn("V2Session.create")(function* (_input) { + return {} as any + }), + get: Effect.fn("V2Session.get")(function* (sessionID) { + const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()) + if (!row) return yield* new NotFoundError({ sessionID }) + return fromRow(row) + }), list: Effect.fn("V2Session.list")(function* (input) { const direction = input.cursor?.direction ?? "next" let order = input.order ?? "desc" @@ -262,11 +279,30 @@ export const layer = Layer.effect( EventV2.run(SessionEvent.ModelSwitched.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(Date.now()), - id: input.id, - providerID: input.providerID, - variant: input.variant, + model: input.model, }) }), + subagent: Effect.fn("V2Session.subagent")(function* (input) { + const parent = yield* result.get(input.parentID) + const session = yield* result.create({ + agent: input.agent, + model: input.model, + parentID: input.parentID, + workspaceID: parent.workspaceID, + }) + yield* result.prompt({ + prompt: input.prompt, + sessionID: session.id, + }) + yield* Effect.gen(function* () { + yield* result.wait(session.id) + const messages = yield* result.messages({ sessionID: session.id, order: "desc" }) + const assistant = messages.find((msg) => msg.type === "assistant") + if (!assistant) return + const text = assistant.content.findLast((part) => part.type === "text") + if (!text) return + }).pipe(Effect.forkChild()) + }), compact: Effect.fn("V2Session.compact")(function* (_sessionID) {}), wait: Effect.fn("V2Session.wait")(function* (_sessionID) {}), } diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index c9a0b53bb4..34cecd80d0 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -19,6 +19,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { Database } from "@/storage/db" import { SessionMessageTable, SessionTable } from "@/session/session.sql" import { SessionMessage } from "../../src/v2/session-message" +import { Modelv2 } from "../../src/v2/model" import * as DateTime from "effect/DateTime" import * as Log from "@opencode-ai/core/util/log" import { eq } from "drizzle-orm" @@ -214,7 +215,11 @@ describe("session HttpApi", () => { id: SessionMessage.ID.create(), type: "assistant", agent: "build", - model: { id: "model", providerID: "provider" }, + model: { + id: Modelv2.ID.make("model"), + providerID: Modelv2.ProviderID.make("provider"), + variant: Modelv2.VariantID.make("default"), + }, time: { created: DateTime.makeUnsafe(1) }, content: [], }) diff --git a/packages/opencode/test/v2/session-message-updater.test.ts b/packages/opencode/test/v2/session-message-updater.test.ts index 128177167c..44ac031eda 100644 --- a/packages/opencode/test/v2/session-message-updater.test.ts +++ b/packages/opencode/test/v2/session-message-updater.test.ts @@ -2,6 +2,7 @@ import { expect, test } from "bun:test" import * as DateTime from "effect/DateTime" import { SessionID } from "../../src/session/schema" import { EventV2 } from "../../src/v2/event" +import { Modelv2 } from "../../src/v2/model" import { SessionEvent } from "../../src/v2/session-event" import { SessionMessageUpdater } from "../../src/v2/session-message-updater" @@ -16,7 +17,11 @@ test("step snapshots carry over to assistant messages", () => { sessionID, timestamp: DateTime.makeUnsafe(1), agent: "build", - model: { id: "model", providerID: "provider" }, + model: { + id: Modelv2.ID.make("model"), + providerID: Modelv2.ProviderID.make("provider"), + variant: Modelv2.VariantID.make("default"), + }, snapshot: "before", }, } satisfies SessionEvent.Event) @@ -56,7 +61,11 @@ test("text ended populates assistant text content", () => { sessionID, timestamp: DateTime.makeUnsafe(1), agent: "build", - model: { id: "model", providerID: "provider" }, + model: { + id: Modelv2.ID.make("model"), + providerID: Modelv2.ProviderID.make("provider"), + variant: Modelv2.VariantID.make("default"), + }, }, } satisfies SessionEvent.Event) @@ -96,7 +105,11 @@ test("tool completion stores completed timestamp", () => { sessionID, timestamp: DateTime.makeUnsafe(1), agent: "build", - model: { id: "model", providerID: "provider" }, + model: { + id: Modelv2.ID.make("model"), + providerID: Modelv2.ProviderID.make("provider"), + variant: Modelv2.VariantID.make("default"), + }, }, } satisfies SessionEvent.Event) diff --git a/specs/v2/session-concepts-gap.md b/specs/v2/session-concepts-gap.md deleted file mode 100644 index 20d84c8f47..0000000000 --- a/specs/v2/session-concepts-gap.md +++ /dev/null @@ -1,131 +0,0 @@ -# Session V2 Concept Gaps - -Compared with `packages/opencode/src/session/message-v2.ts` and `packages/opencode/src/session/processor.ts`, `packages/opencode/src/v2` currently captures the rough event stream for prompts, assistant steps, text, reasoning, tools, retries, and compaction, but it does not yet capture several persisted-message and processor concepts. - -## Message Metadata - -- User messages are missing selected `agent`, `model`, `system`, enabled `tools`, output `format`, and summary metadata. -- Assistant messages are missing `parentID`, `agent`, `providerID`, `modelID`, `variant`, `path.cwd`, `path.root`, deprecated `mode`, `summary`, `structured`, `finish`, and typed `error`. - -## Output Format - -- Text output format. -- JSON-schema output format. -- Structured-output retry count. -- Structured assistant result payload. -- Structured-output error classification. - -## Errors - -- Aborted error. -- Provider auth error. -- API error with status, retryability, headers, body, and metadata. -- Context-overflow error. -- Output-length error. -- Unknown error. -- V2 mostly reduces assistant errors to strings, except retry errors. - -## Part Identity - -- V1 has stable `MessageID`, `PartID`, `sessionID`, and `messageID` on every part. -- V2 assistant content does not preserve stable per-content IDs. -- Stable content IDs matter for deltas, updates, removals, sync events, and UI reconciliation. - -## Part Timing And Metadata - -- V1 text, reasoning, and tool states carry timing and provider metadata. -- V2 assistant text and reasoning content only store text. -- V2 events include metadata, but `SessionEntry` currently drops most provider metadata. - -## Snapshots And Patches - -- Snapshot parts. -- Patch parts. -- Step-start snapshot references. -- Step-finish snapshot references. -- Processor behavior that tracks a snapshot before the stream and emits patches after step finish or cleanup. - -## Step Boundaries - -- V1 stores `step-start` and `step-finish` as first-class parts. -- V2 has `step.started` and `step.ended` events, but the assistant entry only stores aggregate cost and tokens. -- V2 does not preserve step boundary parts, finish reason, or snapshot details in the entry model. - -## Compaction - -- V1 compaction parts have `auto`, `overflow`, and `tail_start_id`. -- V2 compacted events have `auto` and optional `overflow`, but no retained-tail marker. -- V1 also has history filtering semantics around completed summary messages and retained tails. - -## Files And Sources - -- V1 file parts have `mime`, `filename`, `url`, and typed source information. -- V1 source variants include file, symbol, and resource sources. -- Symbol sources include LSP range, name, and kind. -- Resource sources include client name and URI. -- V2 file attachments have `uri`, `mime`, `name`, `description`, and a generic text source, but lose source type, LSP metadata, and resource metadata. - -## Agents And Subtasks - -- Agent parts. -- Subtask parts. -- Subtask prompt, description, agent, model, and command. -- V2 has agent attachments on prompts, but no assistant/session content equivalent for subtask execution. - -## Text Flags - -- Synthetic text flag. -- Ignored text flag. -- V2 has a separate synthetic entry, but no ignored text concept. - -## Tool Calls - -- V1 pending tool state stores parsed input and raw input text separately. -- V2 pending tool state stores a string input but does not preserve a separate raw field. -- V1 completed tool state has `time.start`, `time.end`, and optional `time.compacted`. -- V2 tool time has `created`, `ran`, `completed`, and `pruned`, but the stepper currently does not set `completed` or `pruned`. -- V1 error tool state has `time.start` and `time.end`. -- V1 supports interrupted tool errors with `metadata.interrupted` and preserved partial output. -- V1 tracks provider execution and provider call metadata. -- V2 events include provider info, but `SessionEntryStepper` drops it from entries. -- V1 has tool-output compaction and truncation behavior via `time.compacted`. - -## Media Handling - -- V1 models tool attachments as file parts and has provider-specific handling for media in tool results. -- V1 can strip media, inject synthetic user messages for unsupported providers, and uses a synthetic attachment prompt. -- V2 has attachments but not these model-message conversion semantics. - -## Retries - -- V1 stores retries as independently addressable retry parts. -- V2 stores retries as an assistant aggregate. -- V2 captures some retry information, but not the independent part identity/update model. - -## Processor Control Flow - -- Session status transitions: busy, retry, and idle. -- Retry policy integration. -- Context-overflow-driven compaction. -- Abort and interrupt handling. -- Permission-denied blocking. -- Doom-loop detection. -- Plugin hook for `experimental.text.complete`. -- Background summary generation after steps. -- Cleanup semantics for open text, reasoning, and tool calls. - -## Sync And Bus Events - -- Message updated. -- Message removed. -- Message part updated. -- Message part delta. -- Message part removed. -- V2 has domain events, but not the sync/bus event model for persisted message and part updates/removals. - -## History Retrieval - -- Cursor encoding and decoding. -- Paged message retrieval. -- Reverse streaming through history. -- Compaction-aware history filtering. diff --git a/specs/v2/todo.md b/specs/v2/todo.md index 3a4b9cf241..77c650e55f 100644 --- a/specs/v2/todo.md +++ b/specs/v2/todo.md @@ -20,7 +20,7 @@ model. It can stop doing all the The new agent loop needs to trigger compaction properly -## Plugin API design - ??? +## Plugin API design - James? We need to figure out how we want server plugins to work and what hooks are useful. @@ -49,7 +49,7 @@ I have a basic model service that allows for models to be registered dynamically Providers should register as plugins and autoload based on whatever logic they want / config. They should register models into model database -## Event - Kit/James +## Event - Kit I have this v2/event.ts but it needs to be self contained instead of using the old bus system From 75d141b574b94e304c5222daecd4aa68bb9df1e8 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 4 May 2026 22:36:06 -0400 Subject: [PATCH 26/27] fix(session): cancel subtask child sessions (#25798) --- packages/opencode/src/session/prompt.ts | 4 +- packages/opencode/src/tool/task.ts | 27 +- packages/opencode/test/session/prompt.test.ts | 37 ++ packages/opencode/test/tool/task.test.ts | 518 ++++++++++-------- 4 files changed, 339 insertions(+), 247 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index e1fa81abf1..8286ecf8e6 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1,6 +1,5 @@ import path from "path" import os from "os" -import z from "zod" import * as EffectZod from "@/util/effect-zod" import { SessionID, MessageID, PartID } from "./schema" import { MessageV2 } from "./message-v2" @@ -121,9 +120,8 @@ export const layer = Layer.effect( return yield* EffectBridge.make() }) const ops = Effect.fn("SessionPrompt.ops")(function* () { - const run = yield* runner() return { - cancel: (sessionID: SessionID) => run.fork(cancel(sessionID)), + cancel: (sessionID: SessionID) => cancel(sessionID), resolvePromptParts: (template: string) => resolvePromptParts(template), prompt: (input: PromptInput) => prompt(input), } satisfies TaskPromptOps diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index e58ea9b122..22e4e5671c 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -6,10 +6,11 @@ import { MessageV2 } from "../session/message-v2" import { Agent } from "../agent/agent" import type { SessionPrompt } from "../session/prompt" import { Config } from "@/config/config" -import { Effect, Schema } from "effect" +import { Effect, Exit, Schema } from "effect" +import { EffectBridge } from "@/effect/bridge" export interface TaskPromptOps { - cancel(sessionID: SessionID): void + cancel(sessionID: SessionID): Effect.Effect resolvePromptParts(template: string): Effect.Effect prompt(input: SessionPrompt.PromptInput): Effect.Effect } @@ -118,16 +119,18 @@ export const TaskTool = Tool.define( const ops = ctx.extra?.promptOps as TaskPromptOps if (!ops) return yield* Effect.fail(new Error("TaskTool requires promptOps in ctx.extra")) + const runCancel = yield* EffectBridge.make() const messageID = MessageID.ascending() + const cancel = ops.cancel(nextSession.id) - function cancel() { - ops.cancel(nextSession.id) + function onAbort() { + runCancel.fork(cancel) } return yield* Effect.acquireUseRelease( Effect.sync(() => { - ctx.abort.addEventListener("abort", cancel) + ctx.abort.addEventListener("abort", onAbort) }), () => Effect.gen(function* () { @@ -163,10 +166,16 @@ export const TaskTool = Tool.define( ].join("\n"), } }), - () => - Effect.sync(() => { - ctx.abort.removeEventListener("abort", cancel) - }), + (_, exit) => + Effect.gen(function* () { + if (Exit.hasInterrupts(exit)) yield* cancel + }).pipe( + Effect.ensuring( + Effect.sync(() => { + ctx.abort.removeEventListener("abort", onAbort) + }), + ), + ), ) }) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index a602c0c8d7..c5170f3464 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -858,6 +858,43 @@ it.live( 30_000, ) +it.live( + "cancel propagates from slash command subtask to child session", + () => + provideTmpdirServer( + Effect.fnUntraced(function* ({ llm }) { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const status = yield* SessionStatus.Service + const chat = yield* sessions.create({ title: "Pinned" }) + yield* llm.hang + const msg = yield* user(chat.id, "hello") + yield* addSubtask(chat.id, msg.id) + + const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) + yield* llm.wait(1) + + const msgs = yield* MessageV2.filterCompactedEffect(chat.id) + const taskMsg = msgs.find((item) => item.info.role === "assistant" && item.info.agent === "general") + const tool = taskMsg ? toolPart(taskMsg.parts) : undefined + const sessionID = tool?.state.status === "running" ? tool.state.metadata?.sessionId : undefined + expect(typeof sessionID).toBe("string") + if (typeof sessionID !== "string") throw new Error("missing child session id") + const childID = SessionID.make(sessionID) + expect((yield* status.get(childID)).type).toBe("busy") + + yield* prompt.cancel(chat.id) + const exit = yield* Fiber.await(fiber) + expect(Exit.isSuccess(exit)).toBe(true) + + expect((yield* status.get(chat.id)).type).toBe("idle") + expect((yield* status.get(childID)).type).toBe("idle") + }), + { git: true, config: providerCfg }, + ), + 10_000, +) + it.live( "cancel with queued callers resolves all cleanly", () => diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index a8d62bb68c..f75fcf84b8 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -1,18 +1,17 @@ import { afterEach, describe, expect } from "bun:test" -import { Effect, Layer } from "effect" +import { Effect, Exit, Fiber, Layer } from "effect" import { Agent } from "../../src/agent/agent" import { Config } from "@/config/config" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import { Instance } from "../../src/project/instance" import { Session } from "@/session/session" import { MessageV2 } from "../../src/session/message-v2" import type { SessionPrompt } from "../../src/session/prompt" -import { MessageID, PartID } from "../../src/session/schema" +import { MessageID, PartID, SessionID } from "../../src/session/schema" import { ModelID, ProviderID } from "../../src/provider/schema" import { TaskTool, type TaskPromptOps } from "../../src/tool/task" import { Truncate } from "@/tool/truncate" import { ToolRegistry } from "@/tool/registry" -import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture" +import { disposeAllInstances } from "../fixture/fixture" import { testEffect } from "../lib/effect" afterEach(async () => { @@ -35,6 +34,14 @@ const it = testEffect( ), ) +function defer() { + let resolve!: (value: T | PromiseLike) => void + const promise = new Promise((done) => { + resolve = done + }) + return { promise, resolve } +} + const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") { const session = yield* Session.Service const chat = yield* session.create({ title }) @@ -66,7 +73,7 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") { function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; text?: string }): TaskPromptOps { return { - cancel() {}, + cancel: () => Effect.void, resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]), prompt: (input) => Effect.sync(() => { @@ -107,102 +114,270 @@ function reply(input: SessionPrompt.PromptInput, text: string): MessageV2.WithPa } describe("tool.task", () => { - it.live("description sorts subagents by name and is stable across calls", () => - provideTmpdirInstance( - () => - Effect.gen(function* () { - const agent = yield* Agent.Service - const build = yield* agent.get("build") - const registry = yield* ToolRegistry.Service - const get = Effect.fnUntraced(function* () { - const tools = yield* registry.tools({ ...ref, agent: build }) - return tools.find((tool) => tool.id === TaskTool.id)?.description ?? "" - }) - const first = yield* get() - const second = yield* get() + it.instance( + "description sorts subagents by name and is stable across calls", + () => + Effect.gen(function* () { + const agent = yield* Agent.Service + const build = yield* agent.get("build") + const registry = yield* ToolRegistry.Service + const get = Effect.fnUntraced(function* () { + const tools = yield* registry.tools({ ...ref, agent: build }) + return tools.find((tool) => tool.id === TaskTool.id)?.description ?? "" + }) + const first = yield* get() + const second = yield* get() - expect(first).toBe(second) + expect(first).toBe(second) - const alpha = first.indexOf("- alpha: Alpha agent") - const explore = first.indexOf("- explore:") - const general = first.indexOf("- general:") - const zebra = first.indexOf("- zebra: Zebra agent") + const alpha = first.indexOf("- alpha: Alpha agent") + const explore = first.indexOf("- explore:") + const general = first.indexOf("- general:") + const zebra = first.indexOf("- zebra: Zebra agent") - expect(alpha).toBeGreaterThan(-1) - expect(explore).toBeGreaterThan(alpha) - expect(general).toBeGreaterThan(explore) - expect(zebra).toBeGreaterThan(general) - }), - { - config: { - agent: { - zebra: { - description: "Zebra agent", - mode: "subagent", - }, - alpha: { - description: "Alpha agent", - mode: "subagent", - }, + expect(alpha).toBeGreaterThan(-1) + expect(explore).toBeGreaterThan(alpha) + expect(general).toBeGreaterThan(explore) + expect(zebra).toBeGreaterThan(general) + }), + { + config: { + agent: { + zebra: { + description: "Zebra agent", + mode: "subagent", + }, + alpha: { + description: "Alpha agent", + mode: "subagent", }, }, }, - ), + }, ) - it.live("description hides denied subagents for the caller", () => - provideTmpdirInstance( - () => - Effect.gen(function* () { - const agent = yield* Agent.Service - const build = yield* agent.get("build") - const registry = yield* ToolRegistry.Service - const description = - (yield* registry.tools({ ...ref, agent: build })).find((tool) => tool.id === TaskTool.id)?.description ?? "" + it.instance( + "description hides denied subagents for the caller", + () => + Effect.gen(function* () { + const agent = yield* Agent.Service + const build = yield* agent.get("build") + const registry = yield* ToolRegistry.Service + const description = + (yield* registry.tools({ ...ref, agent: build })).find((tool) => tool.id === TaskTool.id)?.description ?? "" - expect(description).toContain("- alpha: Alpha agent") - expect(description).not.toContain("- zebra: Zebra agent") - }), - { - config: { - permission: { - task: { - "*": "allow", - zebra: "deny", - }, + expect(description).toContain("- alpha: Alpha agent") + expect(description).not.toContain("- zebra: Zebra agent") + }), + { + config: { + permission: { + task: { + "*": "allow", + zebra: "deny", }, - agent: { - zebra: { - description: "Zebra agent", - mode: "subagent", - }, - alpha: { - description: "Alpha agent", - mode: "subagent", - }, + }, + agent: { + zebra: { + description: "Zebra agent", + mode: "subagent", + }, + alpha: { + description: "Alpha agent", + mode: "subagent", }, }, }, - ), + }, ) - it.live("execute resumes an existing task session from task_id", () => - provideTmpdirInstance(() => + it.instance("execute resumes an existing task session from task_id", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" }) + const tool = yield* TaskTool + const def = yield* tool.init() + let seen: SessionPrompt.PromptInput | undefined + const promptOps = stubOps({ text: "resumed", onPrompt: (input) => (seen = input) }) + + const result = yield* def.execute( + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + task_id: child.id, + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + const kids = yield* sessions.children(chat.id) + expect(kids).toHaveLength(1) + expect(kids[0]?.id).toBe(child.id) + expect(result.metadata.sessionId).toBe(child.id) + expect(result.output).toContain(`task_id: ${child.id}`) + expect(seen?.sessionID).toBe(child.id) + }), + ) + + it.instance("execute asks by default and skips checks when bypassed", () => + Effect.gen(function* () { + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + const calls: unknown[] = [] + const promptOps = stubOps() + + const exec = (extra?: Record) => + def.execute( + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps, ...extra }, + messages: [], + metadata: () => Effect.void, + ask: (input) => + Effect.sync(() => { + calls.push(input) + }), + }, + ) + + yield* exec() + yield* exec({ bypassAgentCheck: true }) + + expect(calls).toHaveLength(1) + expect(calls[0]).toEqual({ + permission: "task", + patterns: ["general"], + always: ["*"], + metadata: { + description: "inspect bug", + subagent_type: "general", + }, + }) + }), + ) + + it.instance("execute cancels child session when abort signal fires", () => + Effect.gen(function* () { + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + const ready = defer() + const cancelled = defer() + const abort = new AbortController() + const promptOps: TaskPromptOps = { + cancel: (sessionID) => + Effect.sync(() => { + cancelled.resolve(sessionID) + }), + resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]), + prompt: (input) => + Effect.promise(() => { + ready.resolve(input) + return cancelled.promise + }).pipe(Effect.as(reply(input, "cancelled"))), + } + + const fiber = yield* def + .execute( + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: abort.signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + .pipe(Effect.forkChild) + + const input = yield* Effect.promise(() => ready.promise) + abort.abort() + expect(yield* Effect.promise(() => cancelled.promise)).toBe(input.sessionID) + + const exit = yield* Fiber.await(fiber) + expect(Exit.isSuccess(exit)).toBe(true) + }), + ) + + it.instance("execute creates a child when task_id does not exist", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + let seen: SessionPrompt.PromptInput | undefined + const promptOps = stubOps({ text: "created", onPrompt: (input) => (seen = input) }) + + const result = yield* def.execute( + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + task_id: "ses_missing", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + const kids = yield* sessions.children(chat.id) + expect(kids).toHaveLength(1) + expect(kids[0]?.id).toBe(result.metadata.sessionId) + expect(result.metadata.sessionId).not.toBe("ses_missing") + expect(result.output).toContain(`task_id: ${result.metadata.sessionId}`) + expect(seen?.sessionID).toBe(result.metadata.sessionId) + }), + ) + + it.instance( + "execute shapes child permissions for task, todowrite, and primary tools", + () => Effect.gen(function* () { const sessions = yield* Session.Service const { chat, assistant } = yield* seed() - const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" }) const tool = yield* TaskTool const def = yield* tool.init() let seen: SessionPrompt.PromptInput | undefined - const promptOps = stubOps({ text: "resumed", onPrompt: (input) => (seen = input) }) + const promptOps = stubOps({ onPrompt: (input) => (seen = input) }) const result = yield* def.execute( { description: "inspect bug", prompt: "look into the cache key path", - subagent_type: "general", - task_id: child.id, + subagent_type: "reviewer", }, { sessionID: chat.id, @@ -216,172 +391,45 @@ describe("tool.task", () => { }, ) - const kids = yield* sessions.children(chat.id) - expect(kids).toHaveLength(1) - expect(kids[0]?.id).toBe(child.id) - expect(result.metadata.sessionId).toBe(child.id) - expect(result.output).toContain(`task_id: ${child.id}`) - expect(seen?.sessionID).toBe(child.id) - }), - ), - ) - - it.live("execute asks by default and skips checks when bypassed", () => - provideTmpdirInstance(() => - Effect.gen(function* () { - const { chat, assistant } = yield* seed() - const tool = yield* TaskTool - const def = yield* tool.init() - const calls: unknown[] = [] - const promptOps = stubOps() - - const exec = (extra?: Record) => - def.execute( - { - description: "inspect bug", - prompt: "look into the cache key path", - subagent_type: "general", - }, - { - sessionID: chat.id, - messageID: assistant.id, - agent: "build", - abort: new AbortController().signal, - extra: { promptOps, ...extra }, - messages: [], - metadata: () => Effect.void, - ask: (input) => - Effect.sync(() => { - calls.push(input) - }), - }, - ) - - yield* exec() - yield* exec({ bypassAgentCheck: true }) - - expect(calls).toHaveLength(1) - expect(calls[0]).toEqual({ - permission: "task", - patterns: ["general"], - always: ["*"], - metadata: { - description: "inspect bug", - subagent_type: "general", + const child = yield* sessions.get(result.metadata.sessionId) + expect(child.parentID).toBe(chat.id) + expect(child.permission).toEqual([ + { + permission: "todowrite", + pattern: "*", + action: "deny", }, + { + permission: "bash", + pattern: "*", + action: "allow", + }, + { + permission: "read", + pattern: "*", + action: "allow", + }, + ]) + expect(seen?.tools).toEqual({ + todowrite: false, + bash: false, + read: false, }) }), - ), - ) - - it.live("execute creates a child when task_id does not exist", () => - provideTmpdirInstance(() => - Effect.gen(function* () { - const sessions = yield* Session.Service - const { chat, assistant } = yield* seed() - const tool = yield* TaskTool - const def = yield* tool.init() - let seen: SessionPrompt.PromptInput | undefined - const promptOps = stubOps({ text: "created", onPrompt: (input) => (seen = input) }) - - const result = yield* def.execute( - { - description: "inspect bug", - prompt: "look into the cache key path", - subagent_type: "general", - task_id: "ses_missing", - }, - { - sessionID: chat.id, - messageID: assistant.id, - agent: "build", - abort: new AbortController().signal, - extra: { promptOps }, - messages: [], - metadata: () => Effect.void, - ask: () => Effect.void, - }, - ) - - const kids = yield* sessions.children(chat.id) - expect(kids).toHaveLength(1) - expect(kids[0]?.id).toBe(result.metadata.sessionId) - expect(result.metadata.sessionId).not.toBe("ses_missing") - expect(result.output).toContain(`task_id: ${result.metadata.sessionId}`) - expect(seen?.sessionID).toBe(result.metadata.sessionId) - }), - ), - ) - - it.live("execute shapes child permissions for task, todowrite, and primary tools", () => - provideTmpdirInstance( - () => - Effect.gen(function* () { - const sessions = yield* Session.Service - const { chat, assistant } = yield* seed() - const tool = yield* TaskTool - const def = yield* tool.init() - let seen: SessionPrompt.PromptInput | undefined - const promptOps = stubOps({ onPrompt: (input) => (seen = input) }) - - const result = yield* def.execute( - { - description: "inspect bug", - prompt: "look into the cache key path", - subagent_type: "reviewer", + { + config: { + agent: { + reviewer: { + mode: "subagent", + permission: { + task: "allow", }, - { - sessionID: chat.id, - messageID: assistant.id, - agent: "build", - abort: new AbortController().signal, - extra: { promptOps }, - messages: [], - metadata: () => Effect.void, - ask: () => Effect.void, - }, - ) - - const child = yield* sessions.get(result.metadata.sessionId) - expect(child.parentID).toBe(chat.id) - expect(child.permission).toEqual([ - { - permission: "todowrite", - pattern: "*", - action: "deny", - }, - { - permission: "bash", - pattern: "*", - action: "allow", - }, - { - permission: "read", - pattern: "*", - action: "allow", - }, - ]) - expect(seen?.tools).toEqual({ - todowrite: false, - bash: false, - read: false, - }) - }), - { - config: { - agent: { - reviewer: { - mode: "subagent", - permission: { - task: "allow", - }, - }, - }, - experimental: { - primary_tools: ["bash", "read"], }, }, + experimental: { + primary_tools: ["bash", "read"], + }, }, - ), + }, ) }) From 2d0a757eb2dbeabad64af02a9fb3602d4ccefd5b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 5 May 2026 02:37:07 +0000 Subject: [PATCH 27/27] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 61 +++++----- packages/sdk/openapi.json | 146 +++++++++--------------- 2 files changed, 83 insertions(+), 124 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index c0255754d9..7734ca53eb 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1875,9 +1875,11 @@ export type SyncEventSessionNextModelSwitched = { data: { timestamp: number sessionID: string - id: string - providerID: string - variant?: string + model: { + id: string + providerID: string + variant: string + } } } @@ -1948,7 +1950,7 @@ export type SyncEventSessionNextStepStarted = { model: { id: string providerID: string - variant?: string + variant: string } snapshot?: string } @@ -1987,10 +1989,7 @@ export type SyncEventSessionNextStepFailed = { data: { timestamp: number sessionID: string - error: { - type: string - message: string - } + error: SessionErrorUnknown } } @@ -2188,10 +2187,7 @@ export type SyncEventSessionNextToolFailed = { timestamp: number sessionID: string callID: string - error: { - type: string - message: string - } + error: SessionErrorUnknown provider: { executed: boolean metadata?: { @@ -2616,9 +2612,11 @@ export type EventSessionNextModelSwitched = { properties: { timestamp: number sessionID: string - id: string - providerID: string - variant?: string + model: { + id: string + providerID: string + variant: string + } } } @@ -2693,7 +2691,7 @@ export type EventSessionNextStepStarted = { model: { id: string providerID: string - variant?: string + variant: string } snapshot?: string } @@ -2720,16 +2718,18 @@ export type EventSessionNextStepEnded = { } } +export type SessionErrorUnknown = { + type: "unknown" + message: string +} + export type EventSessionNextStepFailed = { id: string type: "session.next.step.failed" properties: { timestamp: number sessionID: string - error: { - type: string - message: string - } + error: SessionErrorUnknown } } @@ -2900,10 +2900,7 @@ export type EventSessionNextToolFailed = { timestamp: number sessionID: string callID: string - error: { - type: string - message: string - } + error: SessionErrorUnknown provider: { executed: boolean metadata?: { @@ -2994,7 +2991,7 @@ export type SessionInfo = { model?: { id: string providerID: string - variant?: string + variant: string } time: { created: number @@ -3030,7 +3027,7 @@ export type SessionMessageModelSwitched = { model: { id: string providerID: string - variant?: string + variant: string } } @@ -3124,10 +3121,7 @@ export type SessionMessageToolStateError = { structured: { [key: string]: unknown } - error: { - type: string - message: string - } + error: SessionErrorUnknown } export type SessionMessageAssistantTool = { @@ -3167,7 +3161,7 @@ export type SessionMessageAssistant = { model: { id: string providerID: string - variant?: string + variant: string } content: Array snapshot?: { @@ -3185,10 +3179,7 @@ export type SessionMessageAssistant = { write: number } } - error?: { - type: string - message: string - } + error?: SessionErrorUnknown } export type SessionMessageCompaction = { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index db8889f1a4..fea9dd5a95 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -13998,17 +13998,24 @@ "sessionID": { "type": "string" }, - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" + "model": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": ["id", "providerID", "variant"], + "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "id", "providerID"], + "required": ["timestamp", "sessionID", "model"], "additionalProperties": false } }, @@ -14231,7 +14238,7 @@ "type": "string" } }, - "required": ["id", "providerID"], + "required": ["id", "providerID", "variant"], "additionalProperties": false }, "snapshot": { @@ -14357,17 +14364,7 @@ "type": "string" }, "error": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["type", "message"], - "additionalProperties": false + "$ref": "#/components/schemas/SessionErrorUnknown" } }, "required": ["timestamp", "sessionID", "error"], @@ -14979,17 +14976,7 @@ "type": "string" }, "error": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["type", "message"], - "additionalProperties": false + "$ref": "#/components/schemas/SessionErrorUnknown" }, "provider": { "type": "object", @@ -16267,17 +16254,24 @@ "sessionID": { "type": "string" }, - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" + "model": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": ["id", "providerID", "variant"], + "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "id", "providerID"], + "required": ["timestamp", "sessionID", "model"], "additionalProperties": false } }, @@ -16496,7 +16490,7 @@ "type": "string" } }, - "required": ["id", "providerID"], + "required": ["id", "providerID", "variant"], "additionalProperties": false }, "snapshot": { @@ -16580,6 +16574,20 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "SessionErrorUnknown": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["unknown"] + }, + "message": { + "type": "string" + } + }, + "required": ["type", "message"], + "additionalProperties": false + }, "EventSessionNextStepFailed": { "type": "object", "properties": { @@ -16600,17 +16608,7 @@ "type": "string" }, "error": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["type", "message"], - "additionalProperties": false + "$ref": "#/components/schemas/SessionErrorUnknown" } }, "required": ["timestamp", "sessionID", "error"], @@ -17113,17 +17111,7 @@ "type": "string" }, "error": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["type", "message"], - "additionalProperties": false + "$ref": "#/components/schemas/SessionErrorUnknown" }, "provider": { "type": "object", @@ -17376,7 +17364,7 @@ "type": "string" } }, - "required": ["id", "providerID"], + "required": ["id", "providerID", "variant"], "additionalProperties": false }, "time": { @@ -17472,7 +17460,7 @@ "type": "string" } }, - "required": ["id", "providerID"], + "required": ["id", "providerID", "variant"], "additionalProperties": false } }, @@ -17731,17 +17719,7 @@ "type": "object" }, "error": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["type", "message"], - "additionalProperties": false + "$ref": "#/components/schemas/SessionErrorUnknown" } }, "required": ["status", "input", "content", "structured", "error"], @@ -17854,7 +17832,7 @@ "type": "string" } }, - "required": ["id", "providerID"], + "required": ["id", "providerID", "variant"], "additionalProperties": false }, "content": { @@ -17921,17 +17899,7 @@ "additionalProperties": false }, "error": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["type", "message"], - "additionalProperties": false + "$ref": "#/components/schemas/SessionErrorUnknown" } }, "required": ["id", "time", "type", "agent", "model", "content"],