From 01f031919297b4f1d6cb1f883cc0dbc1481b73a3 Mon Sep 17 00:00:00 2001 From: Derek Barrera Date: Mon, 6 Apr 2026 09:50:36 -0400 Subject: [PATCH 01/23] fix(lsp): MEMORY LEAK: ensure typescript server uses native project config (#19953) --- packages/opencode/src/lsp/server.ts | 12 +++- packages/opencode/test/lsp/index.test.ts | 78 +++++++++++++++++++++ packages/web/src/content/docs/ecosystem.mdx | 2 + 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index aa9bc884a8..7421ed5436 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -105,7 +105,17 @@ export namespace LSPServer { if (!tsserver) return const bin = await Npm.which("typescript-language-server") if (!bin) return - const proc = spawn(bin, ["--stdio"], { + + const args = ["--stdio", "--tsserver-log-verbosity", "off", "--tsserver-path", tsserver] + + if ( + !(await pathExists(path.join(root, "tsconfig.json"))) && + !(await pathExists(path.join(root, "jsconfig.json"))) + ) { + args.push("--ignore-node-modules") + } + + const proc = spawn(bin, args, { cwd: root, env: { ...process.env, diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index 7e514e39b1..ceadfe6518 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -1,6 +1,8 @@ import { describe, expect, spyOn, test } from "bun:test" import path from "path" +import fs from "fs/promises" import * as Lsp from "../../src/lsp/index" +import * as launch from "../../src/lsp/launch" import { LSPServer } from "../../src/lsp/server" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" @@ -52,4 +54,80 @@ describe("lsp.spawn", () => { await Instance.disposeAll() } }) + + test("spawns builtin Typescript LSP with correct arguments", async () => { + await using tmp = await tmpdir() + + // Create dummy tsserver to satisfy Module.resolve + const tsdk = path.join(tmp.path, "node_modules", "typescript", "lib") + await fs.mkdir(tsdk, { recursive: true }) + await fs.writeFile(path.join(tsdk, "tsserver.js"), "") + + const spawnSpy = spyOn(launch, "spawn").mockImplementation( + () => + ({ + stdin: {}, + stdout: {}, + stderr: {}, + on: () => {}, + kill: () => {}, + }) as any, + ) + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await LSPServer.Typescript.spawn(tmp.path) + }, + }) + + expect(spawnSpy).toHaveBeenCalled() + const args = spawnSpy.mock.calls[0][1] as string[] + + expect(args).toContain("--tsserver-path") + expect(args).toContain("--tsserver-log-verbosity") + expect(args).toContain("off") + } finally { + spawnSpy.mockRestore() + } + }) + + test("spawns builtin Typescript LSP with --ignore-node-modules if no config is found", async () => { + await using tmp = await tmpdir() + + // Create dummy tsserver to satisfy Module.resolve + const tsdk = path.join(tmp.path, "node_modules", "typescript", "lib") + await fs.mkdir(tsdk, { recursive: true }) + await fs.writeFile(path.join(tsdk, "tsserver.js"), "") + + // NO tsconfig.json or jsconfig.json created here + + const spawnSpy = spyOn(launch, "spawn").mockImplementation( + () => + ({ + stdin: {}, + stdout: {}, + stderr: {}, + on: () => {}, + kill: () => {}, + }) as any, + ) + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await LSPServer.Typescript.spawn(tmp.path) + }, + }) + + expect(spawnSpy).toHaveBeenCalled() + const args = spawnSpy.mock.calls[0][1] as string[] + + expect(args).toContain("--ignore-node-modules") + } finally { + spawnSpy.mockRestore() + } + }) }) diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx index 30b53eeca7..055daf1419 100644 --- a/packages/web/src/content/docs/ecosystem.mdx +++ b/packages/web/src/content/docs/ecosystem.mdx @@ -32,6 +32,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw | [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions for non-interactive shell commands - prevents hangs from TTY-dependent operations | | [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime | | [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs | +| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers | | [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Fast Apply editing, WarpGrep codebase search, and context compaction via Morph | | [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible | | [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions | @@ -42,6 +43,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw | [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interactive plan review with visual annotation and private/offline sharing | | [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Extend opencode /commands into a powerful orchestration system with granular flow control | | [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Schedule recurring jobs using launchd (Mac) or systemd (Linux) with cron syntax | +| [opencode-conductor](https://github.com/derekbar90/opencode-conductor) | Protocol-Driven Workflow: Automation of the Context -> Spec -> Plan -> Implement lifecycle. | | [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → Implement workflow with session continuity | | [octto](https://github.com/vtemian/octto) | Interactive browser UI for AI brainstorming with multi-question forms | | [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code-style background agents with async delegation and context persistence | From 24bdd3c9fb933c1663f1adfa61c9c8c49d5d52c4 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 6 Apr 2026 13:51:36 +0000 Subject: [PATCH 02/23] chore: generate --- packages/opencode/test/lsp/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index ceadfe6518..cfab72d834 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -57,7 +57,7 @@ describe("lsp.spawn", () => { test("spawns builtin Typescript LSP with correct arguments", async () => { await using tmp = await tmpdir() - + // Create dummy tsserver to satisfy Module.resolve const tsdk = path.join(tmp.path, "node_modules", "typescript", "lib") await fs.mkdir(tsdk, { recursive: true }) From 965c751522a72f16a523dc86168aadd349765139 Mon Sep 17 00:00:00 2001 From: MC Date: Mon, 6 Apr 2026 11:50:24 -0400 Subject: [PATCH 03/23] docs: update Cloudflare provider setup to reflect /connect prompt flow (#20589) --- packages/web/src/content/docs/providers.mdx | 77 +++++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index b14c8ab10a..bd7e10f928 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -490,37 +490,42 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try Cloudflare AI Gateway lets you access models from OpenAI, Anthropic, Workers AI, and more through a unified endpoint. With [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/) you don't need separate API keys for each provider. -1. Head over to the [Cloudflare dashboard](https://dash.cloudflare.com/), navigate to **AI** > **AI Gateway**, and create a new gateway. +1. Head over to the [Cloudflare dashboard](https://dash.cloudflare.com/), navigate to **AI** > **AI Gateway**, and create a new gateway. Note your **Account ID** and **Gateway ID**. -2. Set your Account ID and Gateway ID as environment variables. - - ```bash title="~/.bash_profile" - export CLOUDFLARE_ACCOUNT_ID=your-32-character-account-id - export CLOUDFLARE_GATEWAY_ID=your-gateway-id - ``` - -3. Run the `/connect` command and search for **Cloudflare AI Gateway**. +2. Run the `/connect` command and search for **Cloudflare AI Gateway**. ```txt /connect ``` -4. Enter your Cloudflare API token. +3. Enter your **Account ID** when prompted. ```txt - ┌ API key + ┌ Enter your Cloudflare Account ID │ │ └ enter ``` - Or set it as an environment variable. +4. Enter your **Gateway ID** when prompted. - ```bash title="~/.bash_profile" - export CLOUDFLARE_API_TOKEN=your-api-token + ```txt + ┌ Enter your Cloudflare AI Gateway ID + │ + │ + └ enter ``` -5. Run the `/models` command to select a model. +5. Enter your **Cloudflare API token**. + + ```txt + ┌ Gateway API token + │ + │ + └ enter + ``` + +6. Run the `/models` command to select a model. ```txt /models @@ -542,27 +547,38 @@ Cloudflare AI Gateway lets you access models from OpenAI, Anthropic, Workers AI, } ``` + Alternatively, you can set environment variables instead of using `/connect`. + + ```bash title="~/.bash_profile" + export CLOUDFLARE_ACCOUNT_ID=your-32-character-account-id + export CLOUDFLARE_GATEWAY_ID=your-gateway-id + export CLOUDFLARE_API_TOKEN=your-api-token + ``` + --- ### Cloudflare Workers AI Cloudflare Workers AI lets you run AI models on Cloudflare's global network directly via REST API, with no separate provider accounts needed for supported models. -1. Head over to the [Cloudflare dashboard](https://dash.cloudflare.com/), navigate to **Workers AI**, and select **Use REST API** to get your Account ID and create an API token. +1. Head over to the [Cloudflare dashboard](https://dash.cloudflare.com/), navigate to **Workers AI**, and select **Use REST API** to get your **Account ID** and create an API token. -2. Set your Account ID as an environment variable. - - ```bash title="~/.bash_profile" - export CLOUDFLARE_ACCOUNT_ID=your-32-character-account-id - ``` - -3. Run the `/connect` command and search for **Cloudflare Workers AI**. +2. Run the `/connect` command and search for **Cloudflare Workers AI**. ```txt /connect ``` -4. Enter your Cloudflare API token. +3. Enter your **Account ID** when prompted. + + ```txt + ┌ Enter your Cloudflare Account ID + │ + │ + └ enter + ``` + +4. Enter your **Cloudflare API key**. ```txt ┌ API key @@ -571,18 +587,19 @@ Cloudflare Workers AI lets you run AI models on Cloudflare's global network dire └ enter ``` - Or set it as an environment variable. - - ```bash title="~/.bash_profile" - export CLOUDFLARE_API_KEY=your-api-token - ``` - 5. Run the `/models` command to select a model. ```txt /models ``` + Alternatively, you can set environment variables instead of using `/connect`. + + ```bash title="~/.bash_profile" + export CLOUDFLARE_ACCOUNT_ID=your-32-character-account-id + export CLOUDFLARE_API_KEY=your-api-token + ``` + --- ### Cortecs From 2e4c43c1cf6a14c6b2d1d502b70337fae35bc1ce Mon Sep 17 00:00:00 2001 From: Dax Date: Mon, 6 Apr 2026 12:17:29 -0400 Subject: [PATCH 04/23] refactor: replace Bun.serve with Node http.createServer in OAuth handlers (#18327) Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> --- packages/opencode/src/mcp/oauth-callback.ts | 167 ++++++++++---------- packages/opencode/src/plugin/codex.ts | 119 +++++++------- 2 files changed, 147 insertions(+), 139 deletions(-) diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index 3a1ca54044..dd1d886fc1 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -1,4 +1,5 @@ import { createConnection } from "net" +import { createServer } from "http" import { Log } from "../util/log" import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider" @@ -52,7 +53,7 @@ interface PendingAuth { } export namespace McpOAuthCallback { - let server: ReturnType | undefined + let server: ReturnType | undefined const pendingAuths = new Map() // Reverse index: mcpName → oauthState, so cancelPending(mcpName) can // find the right entry in pendingAuths (which is keyed by oauthState). @@ -60,6 +61,80 @@ export namespace McpOAuthCallback { const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes + function cleanupStateIndex(oauthState: string) { + for (const [name, state] of mcpNameToState) { + if (state === oauthState) { + mcpNameToState.delete(name) + break + } + } + } + + function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) { + const url = new URL(req.url || "/", `http://localhost:${OAUTH_CALLBACK_PORT}`) + + if (url.pathname !== OAUTH_CALLBACK_PATH) { + res.writeHead(404) + res.end("Not found") + return + } + + const code = url.searchParams.get("code") + const state = url.searchParams.get("state") + const error = url.searchParams.get("error") + const errorDescription = url.searchParams.get("error_description") + + log.info("received oauth callback", { hasCode: !!code, state, error }) + + // Enforce state parameter presence + if (!state) { + const errorMsg = "Missing required state parameter - potential CSRF attack" + log.error("oauth callback missing state parameter", { url: url.toString() }) + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + if (error) { + const errorMsg = errorDescription || error + if (pendingAuths.has(state)) { + const pending = pendingAuths.get(state)! + clearTimeout(pending.timeout) + pendingAuths.delete(state) + cleanupStateIndex(state) + pending.reject(new Error(errorMsg)) + } + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + if (!code) { + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR("No authorization code provided")) + return + } + + // Validate state parameter + if (!pendingAuths.has(state)) { + const errorMsg = "Invalid or expired state parameter - potential CSRF attack" + log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) }) + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + const pending = pendingAuths.get(state)! + + clearTimeout(pending.timeout) + pendingAuths.delete(state) + cleanupStateIndex(state) + pending.resolve(code) + + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_SUCCESS) + } + export async function ensureRunning(): Promise { if (server) return @@ -69,88 +144,14 @@ export namespace McpOAuthCallback { return } - server = Bun.serve({ - port: OAUTH_CALLBACK_PORT, - fetch(req) { - const url = new URL(req.url) - - if (url.pathname !== OAUTH_CALLBACK_PATH) { - return new Response("Not found", { status: 404 }) - } - - const code = url.searchParams.get("code") - const state = url.searchParams.get("state") - const error = url.searchParams.get("error") - const errorDescription = url.searchParams.get("error_description") - - log.info("received oauth callback", { hasCode: !!code, state, error }) - - // Enforce state parameter presence - if (!state) { - const errorMsg = "Missing required state parameter - potential CSRF attack" - log.error("oauth callback missing state parameter", { url: url.toString() }) - return new Response(HTML_ERROR(errorMsg), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - if (error) { - const errorMsg = errorDescription || error - if (pendingAuths.has(state)) { - const pending = pendingAuths.get(state)! - clearTimeout(pending.timeout) - pendingAuths.delete(state) - for (const [name, s] of mcpNameToState) { - if (s === state) { - mcpNameToState.delete(name) - break - } - } - pending.reject(new Error(errorMsg)) - } - return new Response(HTML_ERROR(errorMsg), { - headers: { "Content-Type": "text/html" }, - }) - } - - if (!code) { - return new Response(HTML_ERROR("No authorization code provided"), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - // Validate state parameter - if (!pendingAuths.has(state)) { - const errorMsg = "Invalid or expired state parameter - potential CSRF attack" - log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) }) - return new Response(HTML_ERROR(errorMsg), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - const pending = pendingAuths.get(state)! - - clearTimeout(pending.timeout) - pendingAuths.delete(state) - // Clean up reverse index - for (const [name, s] of mcpNameToState) { - if (s === state) { - mcpNameToState.delete(name) - break - } - } - pending.resolve(code) - - return new Response(HTML_SUCCESS, { - headers: { "Content-Type": "text/html" }, - }) - }, + server = createServer(handleRequest) + await new Promise((resolve, reject) => { + server!.listen(OAUTH_CALLBACK_PORT, () => { + log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT }) + resolve() + }) + server!.on("error", reject) }) - - log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT }) } export function waitForCallback(oauthState: string, mcpName?: string): Promise { @@ -196,7 +197,7 @@ export namespace McpOAuthCallback { export async function stop(): Promise { if (server) { - server.stop() + await new Promise((resolve) => server!.close(() => resolve())) server = undefined log.info("oauth callback server stopped") } diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index ee42b95171..77c00eb4f0 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -6,6 +6,7 @@ import os from "os" import { ProviderTransform } from "@/provider/transform" import { ModelID, ProviderID } from "@/provider/schema" import { setTimeout as sleep } from "node:timers/promises" +import { createServer } from "http" const log = Log.create({ service: "plugin.codex" }) @@ -241,7 +242,7 @@ interface PendingOAuth { reject: (error: Error) => void } -let oauthServer: ReturnType | undefined +let oauthServer: ReturnType | undefined let pendingOAuth: PendingOAuth | undefined async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> { @@ -249,77 +250,83 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string } return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` } } - oauthServer = Bun.serve({ - port: OAUTH_PORT, - fetch(req) { - const url = new URL(req.url) + oauthServer = createServer((req, res) => { + const url = new URL(req.url || "/", `http://localhost:${OAUTH_PORT}`) - if (url.pathname === "/auth/callback") { - const code = url.searchParams.get("code") - const state = url.searchParams.get("state") - const error = url.searchParams.get("error") - const errorDescription = url.searchParams.get("error_description") + if (url.pathname === "/auth/callback") { + const code = url.searchParams.get("code") + const state = url.searchParams.get("state") + const error = url.searchParams.get("error") + const errorDescription = url.searchParams.get("error_description") - if (error) { - const errorMsg = errorDescription || error - pendingOAuth?.reject(new Error(errorMsg)) - pendingOAuth = undefined - return new Response(HTML_ERROR(errorMsg), { - headers: { "Content-Type": "text/html" }, - }) - } - - if (!code) { - const errorMsg = "Missing authorization code" - pendingOAuth?.reject(new Error(errorMsg)) - pendingOAuth = undefined - return new Response(HTML_ERROR(errorMsg), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - if (!pendingOAuth || state !== pendingOAuth.state) { - const errorMsg = "Invalid state - potential CSRF attack" - pendingOAuth?.reject(new Error(errorMsg)) - pendingOAuth = undefined - return new Response(HTML_ERROR(errorMsg), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - const current = pendingOAuth + if (error) { + const errorMsg = errorDescription || error + pendingOAuth?.reject(new Error(errorMsg)) pendingOAuth = undefined - - exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce) - .then((tokens) => current.resolve(tokens)) - .catch((err) => current.reject(err)) - - return new Response(HTML_SUCCESS, { - headers: { "Content-Type": "text/html" }, - }) + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return } - if (url.pathname === "/cancel") { - pendingOAuth?.reject(new Error("Login cancelled")) + if (!code) { + const errorMsg = "Missing authorization code" + pendingOAuth?.reject(new Error(errorMsg)) pendingOAuth = undefined - return new Response("Login cancelled", { status: 200 }) + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return } - return new Response("Not found", { status: 404 }) - }, + if (!pendingOAuth || state !== pendingOAuth.state) { + const errorMsg = "Invalid state - potential CSRF attack" + pendingOAuth?.reject(new Error(errorMsg)) + pendingOAuth = undefined + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + const current = pendingOAuth + pendingOAuth = undefined + + exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce) + .then((tokens) => current.resolve(tokens)) + .catch((err) => current.reject(err)) + + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_SUCCESS) + return + } + + if (url.pathname === "/cancel") { + pendingOAuth?.reject(new Error("Login cancelled")) + pendingOAuth = undefined + res.writeHead(200) + res.end("Login cancelled") + return + } + + res.writeHead(404) + res.end("Not found") + }) + + await new Promise((resolve, reject) => { + oauthServer!.listen(OAUTH_PORT, () => { + log.info("codex oauth server started", { port: OAUTH_PORT }) + resolve() + }) + oauthServer!.on("error", reject) }) - log.info("codex oauth server started", { port: OAUTH_PORT }) return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` } } function stopOAuthServer() { if (oauthServer) { - oauthServer.stop() + oauthServer.close(() => { + log.info("codex oauth server stopped") + }) oauthServer = undefined - log.info("codex oauth server stopped") } } From 4394e42615386d5246bf8a4ac9d2357f1242c687 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 6 Apr 2026 18:42:05 +0200 Subject: [PATCH 05/23] upgrade opentui to 0.1.97 (#21137) --- bun.lock | 28 ++++++++++++++-------------- packages/opencode/package.json | 4 ++-- packages/plugin/package.json | 8 ++++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/bun.lock b/bun.lock index cfc65b8959..0971a2cc66 100644 --- a/bun.lock +++ b/bun.lock @@ -341,8 +341,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "2.3.3", - "@opentui/core": "0.1.96", - "@opentui/solid": "0.1.96", + "@opentui/core": "0.1.97", + "@opentui/solid": "0.1.97", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -436,16 +436,16 @@ "zod": "catalog:", }, "devDependencies": { - "@opentui/core": "0.1.96", - "@opentui/solid": "0.1.96", + "@opentui/core": "0.1.97", + "@opentui/solid": "0.1.97", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.1.96", - "@opentui/solid": ">=0.1.96", + "@opentui/core": ">=0.1.97", + "@opentui/solid": ">=0.1.97", }, "optionalPeers": [ "@opentui/core", @@ -1506,21 +1506,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.96", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.96", "@opentui/core-darwin-x64": "0.1.96", "@opentui/core-linux-arm64": "0.1.96", "@opentui/core-linux-x64": "0.1.96", "@opentui/core-win32-arm64": "0.1.96", "@opentui/core-win32-x64": "0.1.96", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-VBO5zRiGM6fhibG3AwTMpf0JgbYWG0sXP5AsSJAYw8tQ18OCPj+EDLXGZ1DFmMnJWEi+glKYjmqnIp4yRCqi+Q=="], + "@opentui/core": ["@opentui/core@0.1.97", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.97", "@opentui/core-darwin-x64": "0.1.97", "@opentui/core-linux-arm64": "0.1.97", "@opentui/core-linux-x64": "0.1.97", "@opentui/core-win32-arm64": "0.1.97", "@opentui/core-win32-x64": "0.1.97", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-2ENH0Dc4NUAeHeeQCQhF1lg68RuyntOUP68UvortvDqTz/hqLG0tIwF+DboCKtWi8Nmao4SAQEJ7lfmyQNEDOQ=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.96", "", { "os": "darwin", "cpu": "arm64" }, "sha512-909i75uhLmlUFCK3LK4iICaymiA7QaB45X9IDX94KaDyHL3Y1PgYTzoRZLJlqeOfOBjVfEjMAh/zA5XexWDMpA=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.97", "", { "os": "darwin", "cpu": "arm64" }, "sha512-t7oMGEfMPQsqLEx7/rPqv/UGJ+vqhe4RWHRRQRYcuHuLKssZ2S8P9mSS7MBPtDqGcxg4PosCrh5nHYeZ94EXUw=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.96", "", { "os": "darwin", "cpu": "x64" }, "sha512-qukQjjScKldZAfgY9qVMPv4ZA6Ko7oXjNBUcSMGDgUiOitH6INT1cJQVUnAIu14DY15yEl08MEQ8soLDaSAHcg=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.97", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZuPWAawlVat6ZHb8vaH/CVUeGwI0pI4vd+6zz1ZocZn95ZWJztfyhzNZOJrq1WjHmUROieJ7cOuYUZfvYNuLrg=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.96", "", { "os": "linux", "cpu": "arm64" }, "sha512-9ktmyS24nfSmlFPX0GMWEaEYSjtEPbRn59y4KBhHVhzPsl+YKlzstyHomTBu51IAPu6oL3+t3Lu4gU+k1gFOQQ=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.97", "", { "os": "linux", "cpu": "arm64" }, "sha512-QXxhz654vXgEu2wrFFFFnrSWbyk6/r6nXNnDTcMRWofdMZQLx87NhbcsErNmz9KmFdzoPiQSmlpYubLflKKzqQ=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.96", "", { "os": "linux", "cpu": "x64" }, "sha512-m2pVhIdtqFYO+QSMc2VZgSSCNxRGPL+U+aKYYbvJjPzqCnIkHB9eO0ePU4b3t+V7GaWCcCP3vDCy3g1J5/FreA=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.97", "", { "os": "linux", "cpu": "x64" }, "sha512-v3z0QWpRS3p8blE/A7pTu15hcFMtSndeiYhRxhrjp6zAhQ+UlruQs9DAG1ifSuVO1RJJ0pUKklFivdbu0pMzuw=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.96", "", { "os": "win32", "cpu": "arm64" }, "sha512-OybZ4jvX6H6RKYyGpZqzy3ZrwKaxaXKWwFsmG6pC2J+GRhf5oCIIEy3Y5573h7zy1cq3T9cb225KzBANq9j5BA=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.97", "", { "os": "win32", "cpu": "arm64" }, "sha512-o/m9mD1dvOCwkxOUUyoEILl+d6tzh/85foJc4uqjXYi71NNcwg8u+Eq3/gdHuSKnlT1pusCPKoS1IDuBvZE24A=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.96", "", { "os": "win32", "cpu": "x64" }, "sha512-3YKjg90j14I7dJ94yN0pAYcTf4ogCoohv6ptRdG96XUyzrYhQiDMP398vCIOMjaLBjtMtFmTxSf+W46zm96BCQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.97", "", { "os": "win32", "cpu": "x64" }, "sha512-Rwp7JOwrYm4wtzPHY2vv+2l91LXmKSI7CtbmWN1sSUGhBPtPGSvfwux3W5xaAZQa2KPEXicPjaKJZc+pob3YRg=="], - "@opentui/solid": ["@opentui/solid@0.1.96", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.96", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-NGiVvG1ylswMjF9fzvpSaWLcZKQsPw67KRkIZgsdf4ZIKUZEZ94NktabCA92ti4WVGXhPvyM3SIX5S2+HvnJFg=="], + "@opentui/solid": ["@opentui/solid@0.1.97", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.97", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-ma/uihG38F+6oLJVD8yR7z82FWmR8QhfesNV5SBXbN74riMCRyy6kyQ6SI4xs4ykt9BbZOjrKLq+Xt/0Pd0SJQ=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 024c73e39b..8cf7d76f22 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -106,8 +106,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "2.3.3", - "@opentui/core": "0.1.96", - "@opentui/solid": "0.1.96", + "@opentui/core": "0.1.97", + "@opentui/solid": "0.1.97", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 5710476fd2..0569a0a7cc 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -21,8 +21,8 @@ "zod": "catalog:" }, "peerDependencies": { - "@opentui/core": ">=0.1.96", - "@opentui/solid": ">=0.1.96" + "@opentui/core": ">=0.1.97", + "@opentui/solid": ">=0.1.97" }, "peerDependenciesMeta": { "@opentui/core": { @@ -33,8 +33,8 @@ } }, "devDependencies": { - "@opentui/core": "0.1.96", - "@opentui/solid": "0.1.96", + "@opentui/core": "0.1.97", + "@opentui/solid": "0.1.97", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "typescript": "catalog:", From 535343bf567af41cdecf0f130e6c75e3bae16cd6 Mon Sep 17 00:00:00 2001 From: Dax Date: Mon, 6 Apr 2026 13:24:55 -0400 Subject: [PATCH 06/23] refactor(server): replace Bun serve with Hono node adapters (#18335) Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Brendan Allan --- bun.lock | 39 +++++- package.json | 2 + packages/app/script/e2e-local.ts | 6 +- packages/opencode/package.json | 11 ++ packages/opencode/script/build-node.ts | 15 +++ packages/opencode/script/fix-node-pty.ts | 28 ++++ packages/opencode/src/cli/cmd/acp.ts | 2 +- packages/opencode/src/cli/cmd/serve.ts | 2 +- packages/opencode/src/cli/cmd/web.ts | 2 +- packages/opencode/src/plugin/index.ts | 3 +- packages/opencode/src/pty/index.ts | 30 ++--- packages/opencode/src/pty/pty.bun.ts | 26 ++++ packages/opencode/src/pty/pty.node.ts | 27 ++++ packages/opencode/src/pty/pty.ts | 25 ++++ packages/opencode/src/server/instance.ts | 7 +- packages/opencode/src/server/router.ts | 135 +++++++++---------- packages/opencode/src/server/routes/pty.ts | 13 +- packages/opencode/src/server/server.ts | 143 +++++++++++++-------- 18 files changed, 364 insertions(+), 152 deletions(-) mode change 100644 => 100755 packages/opencode/script/build-node.ts create mode 100755 packages/opencode/script/fix-node-pty.ts create mode 100644 packages/opencode/src/pty/pty.bun.ts create mode 100644 packages/opencode/src/pty/pty.node.ts create mode 100644 packages/opencode/src/pty/pty.ts diff --git a/bun.lock b/bun.lock index 0971a2cc66..7fb683433e 100644 --- a/bun.lock +++ b/bun.lock @@ -329,8 +329,13 @@ "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", "@effect/platform-node": "catalog:", + "@gitlab/gitlab-ai-provider": "3.6.0", + "@gitlab/opencode-gitlab-auth": "1.3.3", + "@hono/node-server": "1.19.11", + "@hono/node-ws": "1.3.0", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", + "@lydell/node-pty": "1.2.0-beta.10", "@modelcontextprotocol/sdk": "1.27.1", "@npmcli/arborist": "9.4.0", "@octokit/graphql": "9.0.2", @@ -1144,6 +1149,10 @@ "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], + "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="], + + "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], + "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], @@ -1156,7 +1165,9 @@ "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], - "@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], + + "@hono/node-ws": ["@hono/node-ws@1.3.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="], "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="], @@ -1348,6 +1359,20 @@ "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], + "@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.10", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.10", "@lydell/node-pty-darwin-x64": "1.2.0-beta.10", "@lydell/node-pty-linux-arm64": "1.2.0-beta.10", "@lydell/node-pty-linux-x64": "1.2.0-beta.10", "@lydell/node-pty-win32-arm64": "1.2.0-beta.10", "@lydell/node-pty-win32-x64": "1.2.0-beta.10" } }, "sha512-Fv+A3+MZVA8qhkBIZsM1E6dCdHNMyXXz22mAYiMWd03LlyK///F3OH6CKPX9mj4id7LUlxpr45yPzyBVy9aDPw=="], + + "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C+eqDyRNHRYvx7RaHj6VVCx6nCpRBPuuxhTcc3JH3GuBMoxTsYeY4GkWH2XOktrgbAq1BG8e/Y8bu/wNQreCEw=="], + + "@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-aZoIK6HtJO5BiT4ELm683U4dyHtt8b7wNgq3NJqYAQwSXrcPv576Z8vY3BIulVxfcFkht/SPLKou9TtdFXdNpg=="], + + "@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-0cKX2iMyXFNBE4fGtGK6B7IkdXcDMZajyEDoGMOgQQs/DDtoI5tSPcBcqNY9VitVrsRQA8+gFt6eKYU9Ye/lUA=="], + + "@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.10", "", { "os": "linux", "cpu": "x64" }, "sha512-J9HnxvSzEeMH748+Ul1VrmCLWMo7iCVJy9EGijRR62+YO/Yk5GaCydUTZ+KzlH0/X5aTrgt5cfiof4vx45tRRg=="], + + "@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-PlDJpJX/pnKyy6OmADKzhf+INZDDnzTBGaI0LT4laVNc6NblZNqUSkCMjLFWbeakeuQp0VG37M49WQSN9FDfeA=="], + + "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.10", "", { "os": "win32", "cpu": "x64" }, "sha512-ExFgWrzyldNAMi45U9PLIOu+g/RatP+f0c/dZxaooifME6yLW32BoHveH26/TtoAjZyJrc2iL0u48pgnR1fzmg=="], + "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], @@ -5194,10 +5219,18 @@ "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], + "@gitlab/gitlab-ai-provider/openai": ["openai@6.33.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="], + + "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@hono/node-ws/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], @@ -5250,6 +5283,8 @@ "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="], + "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "@modelcontextprotocol/sdk/hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="], @@ -6084,6 +6119,8 @@ "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@gitlab/opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], diff --git a/package.json b/package.json index fc73a94d26..e4a2b5844f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "typecheck": "bun turbo typecheck", + "postinstall": "bun run --cwd packages/opencode fix-node-pty", "prepare": "husky", "random": "echo 'Random script'", "hello": "echo 'Hello World!'", @@ -103,6 +104,7 @@ }, "trustedDependencies": [ "esbuild", + "node-pty", "protobufjs", "tree-sitter", "tree-sitter-bash", diff --git a/packages/app/script/e2e-local.ts b/packages/app/script/e2e-local.ts index 70442d0d76..4f0f795a3a 100644 --- a/packages/app/script/e2e-local.ts +++ b/packages/app/script/e2e-local.ts @@ -87,7 +87,7 @@ const runnerEnv = { let seed: ReturnType | undefined let runner: ReturnType | undefined -let server: { stop: () => Promise | void } | undefined +let server: { stop: (close?: boolean) => Promise | void } | undefined let inst: { Instance: { disposeAll: () => Promise | void } } | undefined let cleaned = false @@ -100,7 +100,7 @@ const cleanup = async () => { const jobs = [ inst?.Instance.disposeAll(), - server?.stop(), + typeof server?.stop === "function" ? server.stop() : undefined, keepSandbox ? undefined : fs.rm(sandbox, { recursive: true, force: true }), ].filter(Boolean) await Promise.allSettled(jobs) @@ -158,7 +158,7 @@ try { const servermod = await import("../../opencode/src/server/server") inst = await import("../../opencode/src/project/instance") - server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" }) + server = await servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" }) console.log(`opencode server listening on http://127.0.0.1:${serverPort}`) await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`) diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 8cf7d76f22..cc80842177 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -11,6 +11,7 @@ "test": "bun test --timeout 30000", "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "build": "bun run script/build.ts", + "fix-node-pty": "bun run script/fix-node-pty.ts", "upgrade-opentui": "bun run script/upgrade-opentui.ts", "dev": "bun run --conditions=browser ./src/index.ts", "random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'", @@ -33,6 +34,11 @@ "bun": "./src/storage/db.bun.ts", "node": "./src/storage/db.node.ts", "default": "./src/storage/db.bun.ts" + }, + "#pty": { + "bun": "./src/pty/pty.bun.ts", + "node": "./src/pty/pty.node.ts", + "default": "./src/pty/pty.bun.ts" } }, "devDependencies": { @@ -94,8 +100,13 @@ "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", "@effect/platform-node": "catalog:", + "@gitlab/gitlab-ai-provider": "3.6.0", + "@gitlab/opencode-gitlab-auth": "1.3.3", + "@hono/node-server": "1.19.11", + "@hono/node-ws": "1.3.0", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", + "@lydell/node-pty": "1.2.0-beta.10", "@modelcontextprotocol/sdk": "1.27.1", "@npmcli/arborist": "9.4.0", "@octokit/graphql": "9.0.2", diff --git a/packages/opencode/script/build-node.ts b/packages/opencode/script/build-node.ts old mode 100644 new mode 100755 index fc515a67a6..0e1d5bcf47 --- a/packages/opencode/script/build-node.ts +++ b/packages/opencode/script/build-node.ts @@ -1,5 +1,6 @@ #!/usr/bin/env bun +import { $ } from "bun" import { Script } from "@opencode-ai/script" import fs from "fs" import path from "path" @@ -8,6 +9,15 @@ import { fileURLToPath } from "url" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const dir = path.resolve(__dirname, "..") +const root = path.resolve(dir, "../..") + +function linker(): "hoisted" | "isolated" { + // jsonc-parser is only declared in packages/opencode, so its install location + // tells us whether Bun used a hoisted or isolated workspace layout. + if (fs.existsSync(path.join(dir, "node_modules", "jsonc-parser"))) return "isolated" + if (fs.existsSync(path.join(root, "node_modules", "jsonc-parser"))) return "hoisted" + throw new Error("Could not detect Bun linker from jsonc-parser") +} process.chdir(dir) @@ -41,11 +51,16 @@ const migrations = await Promise.all( ) console.log(`Loaded ${migrations.length} migrations`) +const link = linker() + +await $`bun install --linker=${link} --os="*" --cpu="*" @lydell/node-pty@1.2.0-beta.10` + await Bun.build({ target: "node", entrypoints: ["./src/node.ts"], outdir: "./dist", format: "esm", + sourcemap: "linked", external: ["jsonc-parser"], define: { OPENCODE_MIGRATIONS: JSON.stringify(migrations), diff --git a/packages/opencode/script/fix-node-pty.ts b/packages/opencode/script/fix-node-pty.ts new file mode 100755 index 0000000000..93641adbdf --- /dev/null +++ b/packages/opencode/script/fix-node-pty.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env bun + +import fs from "fs/promises" +import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const dir = path.resolve(__dirname, "..") + +if (process.platform !== "win32") { + const root = path.join(dir, "node_modules", "node-pty", "prebuilds") + const dirs = await fs.readdir(root, { withFileTypes: true }).catch(() => []) + const files = dirs.filter((x) => x.isDirectory()).map((x) => path.join(root, x.name, "spawn-helper")) + const result = await Promise.all( + files.map(async (file) => { + const stat = await fs.stat(file).catch(() => undefined) + if (!stat) return + if ((stat.mode & 0o111) === 0o111) return + await fs.chmod(file, stat.mode | 0o755) + return file + }), + ) + const fixed = result.filter(Boolean) + if (fixed.length) { + console.log(`fixed node-pty permissions for ${fixed.length} helper${fixed.length === 1 ? "" : "s"}`) + } +} diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index 99a9a81ab9..2fb9038b0f 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -23,7 +23,7 @@ export const AcpCommand = cmd({ process.env.OPENCODE_CLIENT = "acp" await bootstrap(process.cwd(), async () => { const opts = await resolveNetworkOptions(args) - const server = Server.listen(opts) + const server = await Server.listen(opts) const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}`, diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index ab51fe8c3e..73e7a18a70 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -15,7 +15,7 @@ export const ServeCommand = cmd({ console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } const opts = await resolveNetworkOptions(args) - const server = Server.listen(opts) + const server = await Server.listen(opts) console.log(`opencode server listening on http://${server.hostname}:${server.port}`) await new Promise(() => {}) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 0fe056f21f..e656c83d9a 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -37,7 +37,7 @@ export const WebCommand = cmd({ UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } const opts = await resolveNetworkOptions(args) - const server = Server.listen(opts) + const server = await Server.listen(opts) UI.empty() UI.println(UI.logo(" ")) UI.empty() diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index df69c8eba7..d84d1cc7b6 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -130,7 +130,8 @@ export namespace Plugin { get serverUrl(): URL { return Server.url ?? new URL("http://localhost:4096") }, - $: Bun.$, + // @ts-expect-error + $: typeof Bun === "undefined" ? undefined : Bun.$, } for (const plugin of INTERNAL_PLUGINS) { diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index a97f3373d1..0321b9800b 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -3,7 +3,7 @@ import { Bus } from "@/bus" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" import { Instance } from "@/project/instance" -import { type IPty } from "bun-pty" +import type { Proc } from "#pty" import z from "zod" import { Log } from "../util/log" import { lazy } from "@opencode-ai/util/lazy" @@ -26,9 +26,11 @@ export namespace Pty { close: (code?: number, reason?: string) => void } + const sock = (ws: Socket) => (ws.data && typeof ws.data === "object" ? ws.data : ws) + type Active = { info: Info - process: IPty + process: Proc buffer: string bufferCursor: number cursor: number @@ -50,10 +52,7 @@ export namespace Pty { return out } - const pty = lazy(async () => { - const { spawn } = await import("bun-pty") - return spawn - }) + const pty = lazy(() => import("#pty")) export const Info = z .object({ @@ -124,9 +123,9 @@ export namespace Pty { try { session.process.kill() } catch {} - for (const [key, ws] of session.subscribers.entries()) { + for (const [sub, ws] of session.subscribers.entries()) { try { - if (ws.data === key) ws.close() + if (sock(ws) === sub) ws.close() } catch {} } session.subscribers.clear() @@ -198,7 +197,7 @@ export namespace Pty { } log.info("creating session", { id, cmd: command, args, cwd }) - const spawn = yield* Effect.promise(() => pty()) + const { spawn } = yield* Effect.promise(() => pty()) const proc = yield* Effect.sync(() => spawn(command, args, { name: "xterm-256color", @@ -234,7 +233,7 @@ export namespace Pty { session.subscribers.delete(key) continue } - if (ws.data !== key) { + if (sock(ws) !== key) { session.subscribers.delete(key) continue } @@ -304,15 +303,12 @@ export namespace Pty { } log.info("client connected to session", { id }) - // Use ws.data as the unique key for this connection lifecycle. - // If ws.data is undefined, fallback to ws object. - const key = ws.data && typeof ws.data === "object" ? ws.data : ws - // Optionally cleanup if the key somehow exists - session.subscribers.delete(key) - session.subscribers.set(key, ws) + const sub = sock(ws) + session.subscribers.delete(sub) + session.subscribers.set(sub, ws) const cleanup = () => { - session.subscribers.delete(key) + session.subscribers.delete(sub) } const start = session.bufferCursor diff --git a/packages/opencode/src/pty/pty.bun.ts b/packages/opencode/src/pty/pty.bun.ts new file mode 100644 index 0000000000..1f8ce8e454 --- /dev/null +++ b/packages/opencode/src/pty/pty.bun.ts @@ -0,0 +1,26 @@ +import { spawn as create } from "bun-pty" +import type { Opts, Proc } from "./pty" + +export type { Disp, Exit, Opts, Proc } from "./pty" + +export function spawn(file: string, args: string[], opts: Opts): Proc { + const pty = create(file, args, opts) + return { + pid: pty.pid, + onData(listener) { + return pty.onData(listener) + }, + onExit(listener) { + return pty.onExit(listener) + }, + write(data) { + pty.write(data) + }, + resize(cols, rows) { + pty.resize(cols, rows) + }, + kill(signal) { + pty.kill(signal) + }, + } +} diff --git a/packages/opencode/src/pty/pty.node.ts b/packages/opencode/src/pty/pty.node.ts new file mode 100644 index 0000000000..b45c5bf509 --- /dev/null +++ b/packages/opencode/src/pty/pty.node.ts @@ -0,0 +1,27 @@ +/** @ts-expect-error */ +import * as pty from "@lydell/node-pty" +import type { Opts, Proc } from "./pty" + +export type { Disp, Exit, Opts, Proc } from "./pty" + +export function spawn(file: string, args: string[], opts: Opts): Proc { + const proc = pty.spawn(file, args, opts) + return { + pid: proc.pid, + onData(listener) { + return proc.onData(listener) + }, + onExit(listener) { + return proc.onExit(listener) + }, + write(data) { + proc.write(data) + }, + resize(cols, rows) { + proc.resize(cols, rows) + }, + kill(signal) { + proc.kill(signal) + }, + } +} diff --git a/packages/opencode/src/pty/pty.ts b/packages/opencode/src/pty/pty.ts new file mode 100644 index 0000000000..fbd1710e52 --- /dev/null +++ b/packages/opencode/src/pty/pty.ts @@ -0,0 +1,25 @@ +export type Disp = { + dispose(): void +} + +export type Exit = { + exitCode: number + signal?: number | string +} + +export type Opts = { + name: string + cols?: number + rows?: number + cwd?: string + env?: Record +} + +export type Proc = { + pid: number + onData(listener: (data: string) => void): Disp + onExit(listener: (event: Exit) => void): Disp + write(data: string): void + resize(cols: number, rows: number): void + kill(signal?: string): void +} diff --git a/packages/opencode/src/server/instance.ts b/packages/opencode/src/server/instance.ts index 0186bf4672..7cc7886b04 100644 --- a/packages/opencode/src/server/instance.ts +++ b/packages/opencode/src/server/instance.ts @@ -1,6 +1,7 @@ import { describeRoute, resolver, validator } from "hono-openapi" import { Hono } from "hono" import { proxy } from "hono/proxy" +import type { UpgradeWebSocket } from "hono/ws" import z from "zod" import { createHash } from "node:crypto" import { Log } from "../util/log" @@ -41,11 +42,11 @@ const DEFAULT_CSP = const csp = (hash = "") => `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:` -export const InstanceRoutes = (app?: Hono) => - (app ?? new Hono()) +export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()) => + app .onError(errorHandler(log)) .route("/project", ProjectRoutes()) - .route("/pty", PtyRoutes()) + .route("/pty", PtyRoutes(upgrade)) .route("/config", ConfigRoutes()) .route("/experimental", ExperimentalRoutes()) .route("/session", SessionRoutes()) diff --git a/packages/opencode/src/server/router.ts b/packages/opencode/src/server/router.ts index f64180892e..b239c62728 100644 --- a/packages/opencode/src/server/router.ts +++ b/packages/opencode/src/server/router.ts @@ -1,4 +1,5 @@ import type { MiddlewareHandler } from "hono" +import type { UpgradeWebSocket } from "hono/ws" import { getAdaptor } from "@/control-plane/adaptors" import { WorkspaceID } from "@/control-plane/schema" import { Workspace } from "@/control-plane/workspace" @@ -24,76 +25,78 @@ function local(method: string, path: string) { return false } -const routes = lazy(() => InstanceRoutes()) +export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler { + const routes = lazy(() => InstanceRoutes(upgrade)) -export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => { - const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() - const directory = Filesystem.resolve( - (() => { - try { - return decodeURIComponent(raw) - } catch { - return raw - } - })(), - ) + return async (c) => { + const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() + const directory = Filesystem.resolve( + (() => { + try { + return decodeURIComponent(raw) + } catch { + return raw + } + })(), + ) - const url = new URL(c.req.url) - const workspaceParam = url.searchParams.get("workspace") + const url = new URL(c.req.url) + const workspaceParam = url.searchParams.get("workspace") - // TODO: If session is being routed, force it to lookup the - // project/workspace + // TODO: If session is being routed, force it to lookup the + // project/workspace - // If no workspace is provided we use the "project" workspace - if (!workspaceParam) { - return Instance.provide({ - directory, - init: InstanceBootstrap, - async fn() { - return routes().fetch(c.req.raw, c.env) - }, + // If no workspace is provided we use the "project" workspace + if (!workspaceParam) { + return Instance.provide({ + directory, + init: InstanceBootstrap, + async fn() { + return routes().fetch(c.req.raw, c.env) + }, + }) + } + + const workspaceID = WorkspaceID.make(workspaceParam) + const workspace = await Workspace.get(workspaceID) + if (!workspace) { + return new Response(`Workspace not found: ${workspaceID}`, { + status: 500, + headers: { + "content-type": "text/plain; charset=utf-8", + }, + }) + } + + // Handle local workspaces directly so we can pass env to `fetch`, + // necessary for websocket upgrades + if (workspace.type === "worktree") { + return Instance.provide({ + directory: workspace.directory!, + init: InstanceBootstrap, + async fn() { + return routes().fetch(c.req.raw, c.env) + }, + }) + } + + // Remote workspaces + + if (local(c.req.method, url.pathname)) { + // No instance provided because we are serving cached data; there + // is no instance to work with + return routes().fetch(c.req.raw, c.env) + } + + const adaptor = await getAdaptor(workspace.type) + const headers = new Headers(c.req.raw.headers) + headers.delete("x-opencode-workspace") + + return adaptor.fetch(workspace, `${url.pathname}${url.search}`, { + method: c.req.method, + body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(), + signal: c.req.raw.signal, + headers, }) } - - const workspaceID = WorkspaceID.make(workspaceParam) - const workspace = await Workspace.get(workspaceID) - if (!workspace) { - return new Response(`Workspace not found: ${workspaceID}`, { - status: 500, - headers: { - "content-type": "text/plain; charset=utf-8", - }, - }) - } - - // Handle local workspaces directly so we can pass env to `fetch`, - // necessary for websocket upgrades - if (workspace.type === "worktree") { - return Instance.provide({ - directory: workspace.directory!, - init: InstanceBootstrap, - async fn() { - return routes().fetch(c.req.raw, c.env) - }, - }) - } - - // Remote workspaces - - if (local(c.req.method, url.pathname)) { - // No instance provided because we are serving cached data; there - // is no instance to work with - return routes().fetch(c.req.raw, c.env) - } - - const adaptor = await getAdaptor(workspace.type) - const headers = new Headers(c.req.raw.headers) - headers.delete("x-opencode-workspace") - - return adaptor.fetch(workspace, `${url.pathname}${url.search}`, { - method: c.req.method, - body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(), - signal: c.req.raw.signal, - headers, - }) } diff --git a/packages/opencode/src/server/routes/pty.ts b/packages/opencode/src/server/routes/pty.ts index de79801e28..c333f4dd69 100644 --- a/packages/opencode/src/server/routes/pty.ts +++ b/packages/opencode/src/server/routes/pty.ts @@ -1,15 +1,14 @@ -import { Hono } from "hono" +import { Hono, type MiddlewareHandler } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" -import { upgradeWebSocket } from "hono/bun" +import type { UpgradeWebSocket } from "hono/ws" import z from "zod" import { Pty } from "@/pty" import { PtyID } from "@/pty/schema" import { NotFoundError } from "../../storage/db" import { errors } from "../error" -import { lazy } from "../../util/lazy" -export const PtyRoutes = lazy(() => - new Hono() +export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { + return new Hono() .get( "/", describeRoute({ @@ -207,5 +206,5 @@ export const PtyRoutes = lazy(() => }, } }), - ), -) + ) +} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index ec245ed59f..3822da71eb 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -4,12 +4,14 @@ import { Hono } from "hono" import { compress } from "hono/compress" import { cors } from "hono/cors" import { basicAuth } from "hono/basic-auth" +import type { UpgradeWebSocket } from "hono/ws" import z from "zod" import { Auth } from "../auth" import { Flag } from "../flag/flag" import { ProviderID } from "../provider/schema" +import { createAdaptorServer, type ServerType } from "@hono/node-server" +import { createNodeWebSocket } from "@hono/node-ws" import { WorkspaceRouterMiddleware } from "./router" -import { websocket } from "hono/bun" import { errors } from "./error" import { GlobalRoutes } from "./routes/global" import { MDNS } from "./mdns" @@ -24,8 +26,14 @@ globalThis.AI_SDK_LOG_WARNINGS = false initProjectors() export namespace Server { - const log = Log.create({ service: "server" }) + export type Listener = { + hostname: string + port: number + url: URL + stop: (close?: boolean) => Promise + } + const log = Log.create({ service: "server" }) const zipped = compress() const skipCompress = (path: string, method: string) => { @@ -34,10 +42,9 @@ export namespace Server { return false } - export const Default = lazy(() => ControlPlaneRoutes()) + export const Default = lazy(() => create({}).app) - export const ControlPlaneRoutes = (opts?: { cors?: string[] }): Hono => { - const app = new Hono() + export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono { return app .onError(errorHandler(log)) .use((c, next) => { @@ -62,9 +69,7 @@ export namespace Server { path: c.req.path, }) await next() - if (!skip) { - timer.stop() - } + if (!skip) timer.stop() }) .use( cors({ @@ -81,15 +86,8 @@ export namespace Server { ) return input - // *.opencode.ai (https only, adjust if needed) - if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) { - return input - } - if (opts?.cors?.includes(input)) { - return input - } - - return + if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input + if (opts?.cors?.includes(input)) return input }, }), ) @@ -234,11 +232,20 @@ export namespace Server { return c.json(true) }, ) - .use(WorkspaceRouterMiddleware) + .use(WorkspaceRouterMiddleware(upgrade)) + } + + function create(opts: { cors?: string[] }) { + const app = new Hono() + const ws = createNodeWebSocket({ app }) + return { + app: ControlPlaneRoutes(ws.upgradeWebSocket, app, opts), + ws, + } } export function createApp(opts: { cors?: string[] }) { - return ControlPlaneRoutes(opts) + return create(opts).app } export async function openapi() { @@ -246,8 +253,8 @@ export namespace Server { // hono-openapi can see describeRoute metadata (`.route()` wraps // handlers when the sub-app has a custom errorHandler, which // strips the metadata symbol). - const app = ControlPlaneRoutes() - InstanceRoutes(app) + const { app, ws } = create({}) + InstanceRoutes(ws.upgradeWebSocket, app) const result = await generateSpecs(app, { documentation: { info: { @@ -261,52 +268,86 @@ export namespace Server { return result } - /** @deprecated do not use this dumb shit */ export let url: URL - export function listen(opts: { + export async function listen(opts: { port: number hostname: string mdns?: boolean mdnsDomain?: string cors?: string[] - }) { - url = new URL(`http://${opts.hostname}:${opts.port}`) - const app = ControlPlaneRoutes({ cors: opts.cors }) - const args = { - hostname: opts.hostname, - idleTimeout: 0, - fetch: app.fetch, - websocket: websocket, - } as const - const tryServe = (port: number) => { - try { - return Bun.serve({ ...args, port }) - } catch { - return undefined - } - } - const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port) - if (!server) throw new Error(`Failed to start server on port ${opts.port}`) + }): Promise { + const built = create(opts) + const start = (port: number) => + new Promise((resolve, reject) => { + const server = createAdaptorServer({ fetch: built.app.fetch }) + built.ws.injectWebSocket(server) + const fail = (err: Error) => { + cleanup() + reject(err) + } + const ready = () => { + cleanup() + resolve(server) + } + const cleanup = () => { + server.off("error", fail) + server.off("listening", ready) + } + server.once("error", fail) + server.once("listening", ready) + server.listen(port, opts.hostname) + }) - const shouldPublishMDNS = + const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port) + const addr = server.address() + if (!addr || typeof addr === "string") { + throw new Error(`Failed to resolve server address for port ${opts.port}`) + } + + const next = new URL("http://localhost") + next.hostname = opts.hostname + next.port = String(addr.port) + url = next + + const mdns = opts.mdns && - server.port && + addr.port && opts.hostname !== "127.0.0.1" && opts.hostname !== "localhost" && opts.hostname !== "::1" - if (shouldPublishMDNS) { - MDNS.publish(server.port!, opts.mdnsDomain) + if (mdns) { + MDNS.publish(addr.port, opts.mdnsDomain) } else if (opts.mdns) { log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") } - const originalStop = server.stop.bind(server) - server.stop = async (closeActiveConnections?: boolean) => { - if (shouldPublishMDNS) MDNS.unpublish() - return originalStop(closeActiveConnections) + let closing: Promise | undefined + return { + hostname: opts.hostname, + port: addr.port, + url: next, + stop(close?: boolean) { + closing ??= new Promise((resolve, reject) => { + if (mdns) MDNS.unpublish() + server.close((err) => { + if (err) { + reject(err) + return + } + resolve() + }) + if (close) { + if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") { + server.closeAllConnections() + } + if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") { + server.closeIdleConnections() + } + } + }) + return closing + }, } - - return server } } From 527b51477da3d07107db71da71e339003d9481ca Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 6 Apr 2026 18:08:04 +0000 Subject: [PATCH 07/23] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index e09f3cf6d1..2ebdd78ac4 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-LRhPPrOKCGUSCEWTpAxPdWKTKVNkg82WrvD25cP3jts=", - "aarch64-linux": "sha256-sbNxkil47n+B7v6ds5EYFybLytXUyRlu0Cpka0ZmDx4=", - "aarch64-darwin": "sha256-5+99gtpIHGygMW3VBAexNhmaORgI8LCxPk/Gf1fW/ds=", - "x86_64-darwin": "sha256-LqnvZGGnQaRxIoowOr5gf6lFgDhbgQhVPiAcRTtU6fE=" + "x86_64-linux": "sha256-weR6jLBRU54GpE7MCQODzOZH3vER2xOqLb4+Dh0ipYY=", + "aarch64-linux": "sha256-NIcstlZn/w8gfrTSqcsEG7HoIjW73a+KKvyNrly8DZg=", + "aarch64-darwin": "sha256-sAOgteyMcWVvJQE+TudCr2seJghwiTismGq9A1OmzZc=", + "x86_64-darwin": "sha256-i7fmGHqKPuQLFT23B5LP78nRkUeIspykQfzvCvBY5EM=" } } From 5a6d10cd5363bd47c8e666bbc63435853a1f25b5 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:12:43 -0700 Subject: [PATCH 08/23] tweak: ensure copilot anthropic models have same reasoning effort model as copilot cli, also fix qwen incorrectly having variants (#21212) --- packages/opencode/src/provider/transform.ts | 8 +++----- packages/opencode/test/session/llm.test.ts | 5 ++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index c1617da40b..f536e04bfa 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -375,8 +375,8 @@ export namespace ProviderTransform { id.includes("glm") || id.includes("mistral") || id.includes("kimi") || - // TODO: Remove this after models.dev data is fixed to use "kimi-k2.5" instead of "k2p5" - id.includes("k2p5") + id.includes("k2p5") || + id.includes("qwen") ) return {} @@ -465,9 +465,7 @@ export namespace ProviderTransform { return {} } if (model.id.includes("claude")) { - return { - thinking: { thinking_budget: 4000 }, - } + return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) } const copilotEfforts = iife(() => { if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3")) diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index bb81aa681c..18b704f4d3 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -289,10 +289,9 @@ describe("session.llm.stream", () => { throw new Error("Server not initialized") } - const providerID = "alibaba" - const modelID = "qwen-plus" + const providerID = "vivgrid" + const modelID = "gemini-3.1-pro-preview" const fixture = await loadFixture(providerID, modelID) - const provider = fixture.provider const model = fixture.model const request = waitRequest( From 40e4cd27a19f41f65e21056dc6eb19fa6399bbff Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:13:30 -0700 Subject: [PATCH 09/23] tweak: adjust chat.params hook to allow altering of the maxOutputTokens (#21220) --- packages/opencode/src/session/llm.ts | 13 +++++++------ packages/plugin/src/index.ts | 8 +++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 1813346cdc..7ef6269b58 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -160,6 +160,11 @@ export namespace LLM { ...input.messages, ] + const maxOutputTokens = + isOpenaiOauth || provider.id.includes("github-copilot") + ? undefined + : ProviderTransform.maxOutputTokens(input.model) + const params = await Plugin.trigger( "chat.params", { @@ -175,6 +180,7 @@ export namespace LLM { : undefined, topP: input.agent.topP ?? ProviderTransform.topP(input.model), topK: ProviderTransform.topK(input.model), + maxOutputTokens, options, }, ) @@ -193,11 +199,6 @@ export namespace LLM { }, ) - const maxOutputTokens = - isOpenaiOauth || provider.id.includes("github-copilot") - ? undefined - : ProviderTransform.maxOutputTokens(input.model) - const tools = await resolveTools(input) // LiteLLM and some Anthropic proxies require the tools parameter to be present @@ -291,7 +292,7 @@ export namespace LLM { activeTools: Object.keys(tools).filter((x) => x !== "invalid"), tools, toolChoice: input.toolChoice, - maxOutputTokens, + maxOutputTokens: params.maxOutputTokens, abortSignal: input.abort, headers: { ...(input.model.providerID.startsWith("opencode") diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 473cac8a9b..1afb55daa7 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -212,7 +212,13 @@ export interface Hooks { */ "chat.params"?: ( input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage }, - output: { temperature: number; topP: number; topK: number; options: Record }, + output: { + temperature: number + topP: number + topK: number + maxOutputTokens: number | undefined + options: Record + }, ) => Promise "chat.headers"?: ( input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage }, From 48c1b6b3387647edfde931c3a50a325c37245b06 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:43:58 -0700 Subject: [PATCH 10/23] tweak: move the max token exclusions to plugins @rekram1-node (#21225) --- packages/opencode/src/plugin/codex.ts | 5 +++++ packages/opencode/src/plugin/github-copilot/copilot.ts | 8 ++++++++ packages/opencode/src/session/llm.ts | 7 +------ packages/opencode/test/session/llm.test.ts | 3 +-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 77c00eb4f0..bdeef9823f 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -599,5 +599,10 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { output.headers["User-Agent"] = `opencode/${Installation.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})` output.headers.session_id = input.sessionID }, + "chat.params": async (input, output) => { + if (input.model.providerID !== "openai") return + // Match codex cli + output.maxOutputTokens = undefined + }, } } diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index ea759b508b..c0425b7efe 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -309,6 +309,14 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { }, ], }, + "chat.params": async (incoming, output) => { + if (!incoming.model.providerID.includes("github-copilot")) return + + // Match github copilot cli, omit maxOutputTokens for gpt models + if (incoming.model.api.id.includes("gpt")) { + output.maxOutputTokens = undefined + } + }, "chat.headers": async (incoming, output) => { if (!incoming.model.providerID.includes("github-copilot")) return diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 7ef6269b58..7f60c02b1d 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -160,11 +160,6 @@ export namespace LLM { ...input.messages, ] - const maxOutputTokens = - isOpenaiOauth || provider.id.includes("github-copilot") - ? undefined - : ProviderTransform.maxOutputTokens(input.model) - const params = await Plugin.trigger( "chat.params", { @@ -180,7 +175,7 @@ export namespace LLM { : undefined, topP: input.agent.topP ?? ProviderTransform.topP(input.model), topK: ProviderTransform.topK(input.model), - maxOutputTokens, + maxOutputTokens: ProviderTransform.maxOutputTokens(input.model), options, }, ) diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 18b704f4d3..82ee8c0810 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -743,8 +743,7 @@ describe("session.llm.stream", () => { expect((body.reasoning as { effort?: string } | undefined)?.effort).toBe("high") const maxTokens = body.max_output_tokens as number | undefined - const expectedMaxTokens = ProviderTransform.maxOutputTokens(resolved) - expect(maxTokens).toBe(expectedMaxTokens) + expect(maxTokens).toBe(undefined) // match codex cli behavior }, }) }) From d1258ac19cc38496d4715d37e9683c4837936a40 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:56:11 -0700 Subject: [PATCH 11/23] fix: bump openrouter ai sdk pkg to fix openrouter issues (#21242) --- bun.lock | 6 ++++-- packages/opencode/package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 7fb683433e..a57f1f0d29 100644 --- a/bun.lock +++ b/bun.lock @@ -345,7 +345,7 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", - "@openrouter/ai-sdk-provider": "2.3.3", + "@openrouter/ai-sdk-provider": "2.4.2", "@opentui/core": "0.1.97", "@opentui/solid": "0.1.97", "@parcel/watcher": "2.5.1", @@ -1527,7 +1527,7 @@ "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], - "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.4.2", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uRQZ4da77gru1I7/lNGJhKbqEIY7o/sPsLlbCM97VY9muGDjM/TaJzuwqIviqKTtXLzF0WDj5qBAi6FhxjvlSg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -5487,6 +5487,8 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="], + "ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index cc80842177..d55933ee58 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -116,7 +116,7 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", - "@openrouter/ai-sdk-provider": "2.3.3", + "@openrouter/ai-sdk-provider": "2.4.2", "@opentui/core": "0.1.97", "@opentui/solid": "0.1.97", "@parcel/watcher": "2.5.1", From 090ad8290e487861697a3e6dcf1026bd5d4f92db Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 6 Apr 2026 23:42:39 +0000 Subject: [PATCH 12/23] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 2ebdd78ac4..e685e5c0ef 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-weR6jLBRU54GpE7MCQODzOZH3vER2xOqLb4+Dh0ipYY=", - "aarch64-linux": "sha256-NIcstlZn/w8gfrTSqcsEG7HoIjW73a+KKvyNrly8DZg=", - "aarch64-darwin": "sha256-sAOgteyMcWVvJQE+TudCr2seJghwiTismGq9A1OmzZc=", - "x86_64-darwin": "sha256-i7fmGHqKPuQLFT23B5LP78nRkUeIspykQfzvCvBY5EM=" + "x86_64-linux": "sha256-Lg7G/1N/pPK7hwitgLTDQ7ShnD2wUReBKm3FdE7bAnk=", + "aarch64-linux": "sha256-SAl8AWpvtLnVu5aAcmgVH3lM98v4jwVqyjNAdLjhB4o=", + "aarch64-darwin": "sha256-TSUro/T5kwBPV0ccfO2098VKRMKAjj0Rsg/Hcrt02IE=", + "x86_64-darwin": "sha256-R4hyhBcrE7d/iXSbALWfCkja8C++SEfl9HZPBHPz0lU=" } } From 31f6f43cfc0f8081b5e9ffbc400bf76f28a9027a Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:53:27 -0700 Subject: [PATCH 13/23] chore: remove ai-sdk/provider-utils patch and update pkg (#21245) --- bun.lock | 99 ++++++++++++++++++- package.json | 3 +- packages/opencode/package.json | 2 +- patches/@ai-sdk%2Fprovider-utils@4.0.21.patch | 61 ------------ 4 files changed, 96 insertions(+), 69 deletions(-) delete mode 100644 patches/@ai-sdk%2Fprovider-utils@4.0.21.patch diff --git a/bun.lock b/bun.lock index a57f1f0d29..096cd6ac3c 100644 --- a/bun.lock +++ b/bun.lock @@ -322,7 +322,7 @@ "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/perplexity": "3.0.26", "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.21", + "@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/togetherai": "2.0.41", "@ai-sdk/vercel": "2.0.39", "@ai-sdk/xai": "3.0.75", @@ -622,7 +622,6 @@ "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch", - "@ai-sdk/provider-utils@4.0.21": "patches/@ai-sdk%2Fprovider-utils@4.0.21.patch", }, "overrides": { "@types/bun": "catalog:", @@ -650,7 +649,7 @@ "@types/node": "22.13.9", "@types/semver": "7.7.1", "@typescript/native-preview": "7.0.0-dev.20251207.1", - "ai": "6.0.138", + "ai": "6.0.149", "cross-spawn": "7.0.6", "diff": "8.0.2", "dompurify": "3.3.1", @@ -733,7 +732,7 @@ "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], "@ai-sdk/togetherai": ["@ai-sdk/togetherai@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-k3p9e3k0/gpDDyTtvafsK4HYR4D/aUQW/kzCwWo1+CzdBU84i4L14gWISC/mv6tgSicMXHcEUd521fPufQwNlg=="], @@ -2371,7 +2370,7 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@6.0.138", "", { "dependencies": { "@ai-sdk/gateway": "3.0.80", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-49OfPe0f5uxJ6jUdA5BBXjIinP6+ZdYfAtpF2aEH64GA5wPcxH2rf/TBUQQ0bbamBz/D+TLMV18xilZqOC+zaA=="], + "ai": ["ai@6.0.149", "", { "dependencies": { "@ai-sdk/gateway": "3.0.91", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3asRb/m3ZGH7H4+VTuTgj8eQYJZ9IJUmV0ljLslY92mQp6Zj+NVn4SmFj0TBr2Y/wFBWC3xgn++47tSGOXxdbw=="], "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], @@ -4985,8 +4984,50 @@ "@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], @@ -5487,6 +5528,8 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.91", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-J39Dh6Gyg6HjG3A7OFKnJMp3QyZ3Eex+XDiX8aFBdRwwZm3jGWaMhkCxQPH7yiQ9kRiErZwHXX/Oexx4SyGGGA=="], + "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="], "ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], @@ -5833,6 +5876,8 @@ "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "vitest/@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="], @@ -5879,6 +5924,48 @@ "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/gateway/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -6467,6 +6554,8 @@ "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], diff --git a/package.json b/package.json index e4a2b5844f..0c75547d1a 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4", "effect": "4.0.0-beta.43", - "ai": "6.0.138", + "ai": "6.0.149", "cross-spawn": "7.0.6", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -119,7 +119,6 @@ "patchedDependencies": { "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", - "@ai-sdk/provider-utils@4.0.21": "patches/@ai-sdk%2Fprovider-utils@4.0.21.patch", "@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch" } } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index d55933ee58..29b53c7b79 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -93,7 +93,7 @@ "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/perplexity": "3.0.26", "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.21", + "@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/togetherai": "2.0.41", "@ai-sdk/vercel": "2.0.39", "@ai-sdk/xai": "3.0.75", diff --git a/patches/@ai-sdk%2Fprovider-utils@4.0.21.patch b/patches/@ai-sdk%2Fprovider-utils@4.0.21.patch deleted file mode 100644 index b93092c466..0000000000 --- a/patches/@ai-sdk%2Fprovider-utils@4.0.21.patch +++ /dev/null @@ -1,61 +0,0 @@ -diff --git a/dist/index.js b/dist/index.js -index 9aa8e83684777e860d905ff7a6895995a7347a4f..820797581ac2a33e731e139da3ebc98b4d93fdcf 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -395,10 +395,13 @@ function validateDownloadUrl(url) { - message: `Invalid URL: ${url}` - }); - } -+ if (parsed.protocol === "data:") { -+ return; -+ } - if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { - throw new DownloadError({ - url, -- message: `URL scheme must be http or https, got ${parsed.protocol}` -+ message: `URL scheme must be http, https, or data, got ${parsed.protocol}` - }); - } - const hostname = parsed.hostname; -diff --git a/dist/index.mjs b/dist/index.mjs -index 095fdc188b1d7f227b42591c78ecb71fe2e2cf8b..ca5227d3b6e358aea8ecd85782a0a2b48130a2c9 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -299,10 +299,13 @@ function validateDownloadUrl(url) { - message: `Invalid URL: ${url}` - }); - } -+ if (parsed.protocol === "data:") { -+ return; -+ } - if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { - throw new DownloadError({ - url, -- message: `URL scheme must be http or https, got ${parsed.protocol}` -+ message: `URL scheme must be http, https, or data, got ${parsed.protocol}` - }); - } - const hostname = parsed.hostname; -diff --git a/src/validate-download-url.ts b/src/validate-download-url.ts -index 7c026ad6b400aef551ce3a424c343e1cedc60997..6a2f11398e58f80a8e11995ac1ce5f4d7c110561 100644 ---- a/src/validate-download-url.ts -+++ b/src/validate-download-url.ts -@@ -18,11 +18,16 @@ export function validateDownloadUrl(url: string): void { - }); - } - -- // Only allow http and https protocols -+ // data: URLs are inline content and do not make network requests. -+ if (parsed.protocol === 'data:') { -+ return; -+ } -+ -+ // Only allow http and https network protocols - if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { - throw new DownloadError({ - url, -- message: `URL scheme must be http or https, got ${parsed.protocol}`, -+ message: `URL scheme must be http, https, or data, got ${parsed.protocol}`, - }); - } - From e64548fb4df1a9e23b13e3ef7ff7a63c84dababb Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 7 Apr 2026 00:30:30 +0000 Subject: [PATCH 14/23] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index e685e5c0ef..3aca064e94 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Lg7G/1N/pPK7hwitgLTDQ7ShnD2wUReBKm3FdE7bAnk=", - "aarch64-linux": "sha256-SAl8AWpvtLnVu5aAcmgVH3lM98v4jwVqyjNAdLjhB4o=", - "aarch64-darwin": "sha256-TSUro/T5kwBPV0ccfO2098VKRMKAjj0Rsg/Hcrt02IE=", - "x86_64-darwin": "sha256-R4hyhBcrE7d/iXSbALWfCkja8C++SEfl9HZPBHPz0lU=" + "x86_64-linux": "sha256-N9VjIpWT7/9mKHGlhVP72tiXeogSrwzcCnEw4fRO7jk=", + "aarch64-linux": "sha256-F3+fg/yifj92BjiCSMhgOZnzNLQIDIKjyG3NghHe8CI=", + "aarch64-darwin": "sha256-/Rtpblyh6xg+lY608scgJDuCwY52HB87l9RVtQv9w8U=", + "x86_64-darwin": "sha256-JU9vVRmWOIJYxihktlNQ4RAopDSfjW8yZipLGK1cyx0=" } } From 3c31d046669ca8df09798f690ef5c9cf17021ddd Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:28:32 -0700 Subject: [PATCH 15/23] chore: bump anthropic ai sdk pkg, delete patch (#21247) --- bun.lock | 5 +- package.json | 3 +- packages/opencode/package.json | 2 +- packages/opencode/test/session/llm.test.ts | 9 +- patches/@ai-sdk%2Fanthropic@3.0.64.patch | 119 --------------------- 5 files changed, 9 insertions(+), 129 deletions(-) delete mode 100644 patches/@ai-sdk%2Fanthropic@3.0.64.patch diff --git a/bun.lock b/bun.lock index 096cd6ac3c..1c6bcd4716 100644 --- a/bun.lock +++ b/bun.lock @@ -308,7 +308,7 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.16.1", "@ai-sdk/amazon-bedrock": "4.0.83", - "@ai-sdk/anthropic": "3.0.64", + "@ai-sdk/anthropic": "3.0.67", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", @@ -621,7 +621,6 @@ "patchedDependencies": { "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch", }, "overrides": { "@types/bun": "catalog:", @@ -5742,6 +5741,8 @@ "nypm/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="], + "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], "opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], diff --git a/package.json b/package.json index 0c75547d1a..4ce36d17ec 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,6 @@ }, "patchedDependencies": { "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", - "@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch" + "solid-js@1.9.10": "patches/solid-js@1.9.10.patch" } } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 29b53c7b79..b88ba974f2 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -79,7 +79,7 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.16.1", "@ai-sdk/amazon-bedrock": "4.0.83", - "@ai-sdk/anthropic": "3.0.64", + "@ai-sdk/anthropic": "3.0.67", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 82ee8c0810..946797da50 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -872,16 +872,15 @@ describe("session.llm.stream", () => { }) }) - test("sends messages API payload for Anthropic models", async () => { + test("sends messages API payload for Anthropic Compatible models", async () => { const server = state.server if (!server) { throw new Error("Server not initialized") } - const providerID = "anthropic" - const modelID = "claude-3-5-sonnet-20241022" + const providerID = "minimax" + const modelID = "MiniMax-M2.5" const fixture = await loadFixture(providerID, modelID) - const provider = fixture.provider const model = fixture.model const chunks = [ @@ -962,7 +961,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.7") }, + model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.5") }, } satisfies MessageV2.User const stream = await LLM.stream({ diff --git a/patches/@ai-sdk%2Fanthropic@3.0.64.patch b/patches/@ai-sdk%2Fanthropic@3.0.64.patch deleted file mode 100644 index b8c2f387d7..0000000000 --- a/patches/@ai-sdk%2Fanthropic@3.0.64.patch +++ /dev/null @@ -1,119 +0,0 @@ ---- a/dist/index.js -+++ b/dist/index.js -@@ -3155,15 +3155,6 @@ - }); - } - baseArgs.max_tokens = maxTokens + (thinkingBudget != null ? thinkingBudget : 0); -- } else { -- if (topP != null && temperature != null) { -- warnings.push({ -- type: "unsupported", -- feature: "topP", -- details: `topP is not supported when temperature is set. topP is ignored.` -- }); -- baseArgs.top_p = void 0; -- } - } - if (isKnownModel && baseArgs.max_tokens > maxOutputTokensForModel) { - if (maxOutputTokens != null) { -@@ -5180,4 +5171,4 @@ - createAnthropic, - forwardAnthropicContainerIdFromLastStep - }); --//# sourceMappingURL=index.js.map -\ No newline at end of file -+//# sourceMappingURL=index.js.map ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -3192,15 +3192,6 @@ - }); - } - baseArgs.max_tokens = maxTokens + (thinkingBudget != null ? thinkingBudget : 0); -- } else { -- if (topP != null && temperature != null) { -- warnings.push({ -- type: "unsupported", -- feature: "topP", -- details: `topP is not supported when temperature is set. topP is ignored.` -- }); -- baseArgs.top_p = void 0; -- } - } - if (isKnownModel && baseArgs.max_tokens > maxOutputTokensForModel) { - if (maxOutputTokens != null) { -@@ -5256,4 +5247,4 @@ - createAnthropic, - forwardAnthropicContainerIdFromLastStep - }; --//# sourceMappingURL=index.mjs.map -\ No newline at end of file -+//# sourceMappingURL=index.mjs.map ---- a/dist/internal/index.js -+++ b/dist/internal/index.js -@@ -3147,15 +3147,6 @@ - }); - } - baseArgs.max_tokens = maxTokens + (thinkingBudget != null ? thinkingBudget : 0); -- } else { -- if (topP != null && temperature != null) { -- warnings.push({ -- type: "unsupported", -- feature: "topP", -- details: `topP is not supported when temperature is set. topP is ignored.` -- }); -- baseArgs.top_p = void 0; -- } - } - if (isKnownModel && baseArgs.max_tokens > maxOutputTokensForModel) { - if (maxOutputTokens != null) { -@@ -5080,4 +5071,4 @@ - anthropicTools, - prepareTools - }); --//# sourceMappingURL=index.js.map -\ No newline at end of file -+//# sourceMappingURL=index.js.map ---- a/dist/internal/index.mjs -+++ b/dist/internal/index.mjs -@@ -3176,15 +3176,6 @@ - }); - } - baseArgs.max_tokens = maxTokens + (thinkingBudget != null ? thinkingBudget : 0); -- } else { -- if (topP != null && temperature != null) { -- warnings.push({ -- type: "unsupported", -- feature: "topP", -- details: `topP is not supported when temperature is set. topP is ignored.` -- }); -- baseArgs.top_p = void 0; -- } - } - if (isKnownModel && baseArgs.max_tokens > maxOutputTokensForModel) { - if (maxOutputTokens != null) { -@@ -5148,4 +5139,4 @@ - anthropicTools, - prepareTools - }; --//# sourceMappingURL=index.mjs.map -\ No newline at end of file -+//# sourceMappingURL=index.mjs.map ---- a/src/anthropic-messages-language-model.ts -+++ b/src/anthropic-messages-language-model.ts -@@ -534,16 +534,6 @@ - - // adjust max tokens to account for thinking: - baseArgs.max_tokens = maxTokens + (thinkingBudget ?? 0); -- } else { -- // Only check temperature/topP mutual exclusivity when thinking is not enabled -- if (topP != null && temperature != null) { -- warnings.push({ -- type: 'unsupported', -- feature: 'topP', -- details: `topP is not supported when temperature is set. topP is ignored.`, -- }); -- baseArgs.top_p = undefined; -- } - } - - // limit to max output tokens for known models to enable model switching without breaking it: From 37883a9f3a8542ab93ccfa85808b51d9b54cfd9f Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 6 Apr 2026 23:19:55 -0400 Subject: [PATCH 16/23] refactor(core): add full http proxy and change workspace adaptor interface (#21239) --- .../src/control-plane/adaptors/worktree.ts | 8 +- packages/opencode/src/control-plane/types.ts | 15 +- .../opencode/src/control-plane/workspace.ts | 18 ++- packages/opencode/src/server/proxy.ts | 130 ++++++++++++++++++ packages/opencode/src/server/router.ts | 31 +++-- 5 files changed, 182 insertions(+), 20 deletions(-) create mode 100644 packages/opencode/src/server/proxy.ts diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts index 719748e3a1..9fb6c74793 100644 --- a/packages/opencode/src/control-plane/adaptors/worktree.ts +++ b/packages/opencode/src/control-plane/adaptors/worktree.ts @@ -32,7 +32,11 @@ export const WorktreeAdaptor: Adaptor = { const config = Config.parse(info) await Worktree.remove({ directory: config.directory }) }, - async fetch(_info, _input: RequestInfo | URL, _init?: RequestInit) { - throw new Error("fetch not implemented") + target(info) { + const config = Config.parse(info) + return { + type: "local", + directory: config.directory, + } }, } diff --git a/packages/opencode/src/control-plane/types.ts b/packages/opencode/src/control-plane/types.ts index ab628a6938..dd17c56d93 100644 --- a/packages/opencode/src/control-plane/types.ts +++ b/packages/opencode/src/control-plane/types.ts @@ -13,9 +13,20 @@ export const WorkspaceInfo = z.object({ }) export type WorkspaceInfo = z.infer +export type Target = + | { + type: "local" + directory: string + } + | { + type: "remote" + url: string | URL + headers?: HeadersInit + } + export type Adaptor = { configure(input: WorkspaceInfo): WorkspaceInfo | Promise - create(input: WorkspaceInfo, from?: WorkspaceInfo): Promise + create(config: WorkspaceInfo, from?: WorkspaceInfo): Promise remove(config: WorkspaceInfo): Promise - fetch(config: WorkspaceInfo, input: RequestInfo | URL, init?: RequestInit): Promise + target(config: WorkspaceInfo): Target | Promise } diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index e5294844b1..bb0fd60020 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -116,17 +116,31 @@ export namespace Workspace { async function workspaceEventLoop(space: Info, stop: AbortSignal) { while (!stop.aborted) { const adaptor = await getAdaptor(space.type) - const res = await adaptor.fetch(space, "/event", { method: "GET", signal: stop }).catch(() => undefined) - if (!res || !res.ok || !res.body) { + const target = await Promise.resolve(adaptor.target(space)) + + if (target.type === "local") { + return + } + + const baseURL = String(target.url).replace(/\/?$/, "/") + + const res = await fetch(new URL(baseURL + "/event"), { + method: "GET", + signal: stop, + }) + + if (!res.ok || !res.body) { await sleep(1000) continue } + await parseSSE(res.body, stop, (event) => { GlobalBus.emit("event", { directory: space.id, payload: event, }) }) + // Wait 250ms and retry if SSE connection fails await sleep(250) } diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts new file mode 100644 index 0000000000..c489c6b42b --- /dev/null +++ b/packages/opencode/src/server/proxy.ts @@ -0,0 +1,130 @@ +import type { Target } from "@/control-plane/types" +import { lazy } from "@/util/lazy" +import { Hono } from "hono" +import { upgradeWebSocket } from "hono/bun" + +const hop = new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "proxy-connection", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "host", +]) + +type Msg = string | ArrayBuffer | Uint8Array + +function headers(req: Request, extra?: HeadersInit) { + const out = new Headers(req.headers) + for (const key of hop) out.delete(key) + out.delete("x-opencode-directory") + out.delete("x-opencode-workspace") + if (!extra) return out + for (const [key, value] of new Headers(extra).entries()) { + out.set(key, value) + } + return out +} + +function protocols(req: Request) { + const value = req.headers.get("sec-websocket-protocol") + if (!value) return [] + return value + .split(",") + .map((item) => item.trim()) + .filter(Boolean) +} + +function socket(url: string | URL) { + const next = new URL(url) + if (next.protocol === "http:") next.protocol = "ws:" + if (next.protocol === "https:") next.protocol = "wss:" + return next.toString() +} + +function send(ws: { send(data: string | ArrayBuffer | Uint8Array): void }, data: any) { + if (data instanceof Blob) { + return data.arrayBuffer().then((x) => ws.send(x)) + } + return ws.send(data) +} + +const app = lazy(() => + new Hono().get( + "/__workspace_ws", + upgradeWebSocket((c) => { + const url = c.req.header("x-opencode-proxy-url") + const queue: Msg[] = [] + let remote: WebSocket | undefined + return { + onOpen(_, ws) { + if (!url) { + ws.close(1011, "missing proxy target") + return + } + remote = new WebSocket(url, protocols(c.req.raw)) + remote.binaryType = "arraybuffer" + remote.onopen = () => { + for (const item of queue) remote?.send(item) + queue.length = 0 + } + remote.onmessage = (event) => { + send(ws, event.data) + } + remote.onerror = () => { + ws.close(1011, "proxy error") + } + remote.onclose = (event) => { + ws.close(event.code, event.reason) + } + }, + onMessage(event) { + const data = event.data + if (typeof data !== "string" && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer)) return + if (remote?.readyState === WebSocket.OPEN) { + remote.send(data) + return + } + queue.push(data) + }, + onClose(event) { + remote?.close(event.code, event.reason) + }, + } + }), + ), +) + +export namespace ServerProxy { + export function http(target: Extract, req: Request) { + return fetch( + new Request(target.url, { + method: req.method, + headers: headers(req, target.headers), + body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body, + redirect: "manual", + signal: req.signal, + }), + ) + } + + export function websocket(target: Extract, req: Request, env: unknown) { + const url = new URL(req.url) + url.pathname = "/__workspace_ws" + url.search = "" + const next = new Headers(req.headers) + next.set("x-opencode-proxy-url", socket(target.url)) + return app().fetch( + new Request(url, { + method: req.method, + headers: next, + signal: req.signal, + }), + env as never, + ) + } +} diff --git a/packages/opencode/src/server/router.ts b/packages/opencode/src/server/router.ts index b239c62728..b6f99ec73b 100644 --- a/packages/opencode/src/server/router.ts +++ b/packages/opencode/src/server/router.ts @@ -3,6 +3,7 @@ import type { UpgradeWebSocket } from "hono/ws" import { getAdaptor } from "@/control-plane/adaptors" import { WorkspaceID } from "@/control-plane/schema" import { Workspace } from "@/control-plane/workspace" +import { ServerProxy } from "./proxy" import { lazy } from "@/util/lazy" import { Filesystem } from "@/util/filesystem" import { Instance } from "@/project/instance" @@ -41,7 +42,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware ) const url = new URL(c.req.url) - const workspaceParam = url.searchParams.get("workspace") + const workspaceParam = url.searchParams.get("workspace") || c.req.header("x-opencode-workspace") // TODO: If session is being routed, force it to lookup the // project/workspace @@ -68,11 +69,12 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware }) } - // Handle local workspaces directly so we can pass env to `fetch`, - // necessary for websocket upgrades - if (workspace.type === "worktree") { + const adaptor = await getAdaptor(workspace.type) + const target = await adaptor.target(workspace) + + if (target.type === "local") { return Instance.provide({ - directory: workspace.directory!, + directory: target.directory, init: InstanceBootstrap, async fn() { return routes().fetch(c.req.raw, c.env) @@ -80,23 +82,24 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware }) } - // Remote workspaces - if (local(c.req.method, url.pathname)) { // No instance provided because we are serving cached data; there // is no instance to work with return routes().fetch(c.req.raw, c.env) } - const adaptor = await getAdaptor(workspace.type) + if (c.req.header("upgrade")?.toLowerCase() === "websocket") { + return ServerProxy.websocket(target, c.req.raw, c.env) + } + const headers = new Headers(c.req.raw.headers) headers.delete("x-opencode-workspace") - return adaptor.fetch(workspace, `${url.pathname}${url.search}`, { - method: c.req.method, - body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(), - signal: c.req.raw.signal, - headers, - }) + return ServerProxy.http( + target, + new Request(c.req.raw, { + headers, + }), + ) } } From f4975ef32a38fc4b605db7d1a87cd85045b4d958 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 7 Apr 2026 00:16:32 -0400 Subject: [PATCH 17/23] go: add mimo --- .../console/app/src/routes/workspace/[id]/go/lite-section.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index 2f8ad8aba4..20042ca718 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -287,6 +287,8 @@ export function LiteSection() {
  • Kimi K2.5
  • GLM-5
  • +
  • Mimo-V2-Pro
  • +
  • Mimo-V2-Omni
  • MiniMax M2.5
  • MiniMax M2.7
From 885df8eb54e268e7720242406228e2de54bf62c6 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:30:05 -0500 Subject: [PATCH 18/23] feat: add --dangerously-skip-permissions flag to opencode run (#21266) --- packages/opencode/src/cli/cmd/run.ts | 31 ++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 92b6156ca7..05fb3a5798 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -302,6 +302,11 @@ export const RunCommand = cmd({ describe: "show thinking blocks", default: false, }) + .option("dangerously-skip-permissions", { + type: "boolean", + describe: "auto-approve permissions that are not explicitly denied (dangerous!)", + default: false, + }) }, handler: async (args) => { let message = [...args.message, ...(args["--"] || [])] @@ -544,15 +549,23 @@ export const RunCommand = cmd({ if (event.type === "permission.asked") { const permission = event.properties if (permission.sessionID !== sessionID) continue - UI.println( - UI.Style.TEXT_WARNING_BOLD + "!", - UI.Style.TEXT_NORMAL + - `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`, - ) - await sdk.permission.reply({ - requestID: permission.id, - reply: "reject", - }) + + if (args["dangerously-skip-permissions"]) { + await sdk.permission.reply({ + requestID: permission.id, + reply: "once", + }) + } else { + UI.println( + UI.Style.TEXT_WARNING_BOLD + "!", + UI.Style.TEXT_NORMAL + + `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`, + ) + await sdk.permission.reply({ + requestID: permission.id, + reply: "reject", + }) + } } } } From 3ea641340761818f1ed0394635681bd0e20cc3a5 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 7 Apr 2026 04:38:49 +0000 Subject: [PATCH 19/23] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 3aca064e94..0b8e34e786 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-N9VjIpWT7/9mKHGlhVP72tiXeogSrwzcCnEw4fRO7jk=", - "aarch64-linux": "sha256-F3+fg/yifj92BjiCSMhgOZnzNLQIDIKjyG3NghHe8CI=", - "aarch64-darwin": "sha256-/Rtpblyh6xg+lY608scgJDuCwY52HB87l9RVtQv9w8U=", - "x86_64-darwin": "sha256-JU9vVRmWOIJYxihktlNQ4RAopDSfjW8yZipLGK1cyx0=" + "x86_64-linux": "sha256-r1+AehuOGIOaaxfXkQGracT/6OdFRn5Ub8s7H+MeKFY=", + "aarch64-linux": "sha256-WkMSRF/ZJLyzxNBjpiMR459C9G0NVOEw31tm8roPneA=", + "aarch64-darwin": "sha256-Z127cxFpTl8Ml7PB3CG9TcCU08oYCPuk0FECK2MQ2CI=", + "x86_64-darwin": "sha256-pkRoFtnVjyl+5fm+rrFyRnEwvptxylnFxPAcEv4ZOCg=" } } From 3c96bf84688fa5e56977a1ff95a0b920f1749983 Mon Sep 17 00:00:00 2001 From: gitpush-gitpaid <149759805+gitpush-gitpaid@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:39:59 -0400 Subject: [PATCH 20/23] feat(opencode): Add PDF attachment Drag and Drop (#16926) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline --- .../cli/cmd/tui/component/prompt/index.tsx | 36 +++++++++++++------ .../tui/feature-plugins/home/tips-view.tsx | 2 +- 2 files changed, 26 insertions(+), 12 deletions(-) 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 087742a979..2fef184f57 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -2,6 +2,7 @@ import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteB import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" import "opentui-spinner/solid" import path from "path" +import { fileURLToPath } from "url" import { Filesystem } from "@/util/filesystem" import { useLocal } from "@tui/context/local" import { useTheme } from "@tui/context/theme" @@ -248,7 +249,7 @@ export function Prompt(props: PromptProps) { onSelect: async () => { const content = await Clipboard.read() if (content?.mime.startsWith("image/")) { - await pasteImage({ + await pasteAttachment({ filename: "clipboard", mime: content.mime, content: content.data, @@ -771,11 +772,16 @@ export function Prompt(props: PromptProps) { ) } - async function pasteImage(file: { filename?: string; content: string; mime: string }) { + async function pasteAttachment(file: { filename?: string; filepath?: string; content: string; mime: string }) { const currentOffset = input.visualCursor.offset const extmarkStart = currentOffset - const count = store.prompt.parts.filter((x) => x.type === "file" && x.mime.startsWith("image/")).length - const virtualText = `[Image ${count + 1}]` + const pdf = file.mime === "application/pdf" + const count = store.prompt.parts.filter((x) => { + if (x.type !== "file") return false + if (pdf) return x.mime === "application/pdf" + return x.mime.startsWith("image/") + }).length + const virtualText = pdf ? `[PDF ${count + 1}]` : `[Image ${count + 1}]` const extmarkEnd = extmarkStart + virtualText.length const textToInsert = virtualText + " " @@ -796,7 +802,7 @@ export function Prompt(props: PromptProps) { url: `data:${file.mime};base64,${file.content}`, source: { type: "file", - path: file.filename ?? "", + path: file.filepath ?? file.filename ?? "", text: { start: extmarkStart, end: extmarkEnd, @@ -926,7 +932,7 @@ export function Prompt(props: PromptProps) { const content = await Clipboard.read() if (content?.mime.startsWith("image/")) { e.preventDefault() - await pasteImage({ + await pasteAttachment({ filename: "clipboard", mime: content.mime, content: content.data, @@ -1012,9 +1018,16 @@ export function Prompt(props: PromptProps) { return } - // trim ' from the beginning and end of the pasted content. just - // ' and nothing else - const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ") + const filepath = iife(() => { + const raw = pastedContent.replace(/^['"]+|['"]+$/g, "") + if (raw.startsWith("file://")) { + try { + return fileURLToPath(raw) + } catch {} + } + if (process.platform === "win32") return raw + return raw.replace(/\\(.)/g, "$1") + }) const isUrl = /^(https?):\/\//.test(filepath) if (!isUrl) { try { @@ -1029,14 +1042,15 @@ export function Prompt(props: PromptProps) { return } } - if (mime.startsWith("image/")) { + if (mime.startsWith("image/") || mime === "application/pdf") { event.preventDefault() const content = await Filesystem.readArrayBuffer(filepath) .then((buffer) => Buffer.from(buffer).toString("base64")) .catch(() => {}) if (content) { - await pasteImage({ + await pasteAttachment({ filename, + filepath, mime, content, }) diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx index a87e4ed2b7..1a9d907bb9 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx @@ -55,7 +55,7 @@ const TIPS = [ "Use {highlight}/undo{/highlight} to revert the last message and file changes", "Use {highlight}/redo{/highlight} to restore previously undone messages and file changes", "Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai", - "Drag and drop images into the terminal to add them as context", + "Drag and drop images or PDFs into the terminal to add them as context", "Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard into the prompt", "Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor", "Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase", From 3a1ec27feba5129623f2c916f6de31ddb43e535e Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Tue, 7 Apr 2026 15:45:22 +0530 Subject: [PATCH 21/23] feat(app): show full names on composer attachment chips (#21306) --- .../prompt-input/image-attachments.tsx | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/app/src/components/prompt-input/image-attachments.tsx b/packages/app/src/components/prompt-input/image-attachments.tsx index 835fddc307..dd8138e5a4 100644 --- a/packages/app/src/components/prompt-input/image-attachments.tsx +++ b/packages/app/src/components/prompt-input/image-attachments.tsx @@ -1,5 +1,6 @@ import { Component, For, Show } from "solid-js" import { Icon } from "@opencode-ai/ui/icon" +import { Tooltip } from "@opencode-ai/ui/tooltip" import type { ImageAttachmentPart } from "@/context/prompt" type PromptImageAttachmentsProps = { @@ -22,34 +23,36 @@ export const PromptImageAttachments: Component = (p
{(attachment) => ( -
- - -
- } - > - {attachment.filename} props.onOpen(attachment)} - /> - - -
- {attachment.filename} + +
+ + +
+ } + > + {attachment.filename} props.onOpen(attachment)} + /> + + +
+ {attachment.filename} +
-
+ )} From c2d2ca352252a922354c89bfbdb135cf1abfe1b6 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Tue, 7 Apr 2026 16:17:53 +0530 Subject: [PATCH 22/23] style(app): redesign jump-to-bottom button per figma spec (#21313) --- packages/app/src/pages/session/message-timeline.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index cbabbda72d..bc211303a6 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -577,10 +577,18 @@ export function MessageTimeline(props: { }} > Date: Tue, 7 Apr 2026 16:30:13 +0530 Subject: [PATCH 23/23] Move auto-accept permissions to settings (#21308) --- packages/app/e2e/prompt/prompt-shell.spec.ts | 15 ++++--- .../e2e/session/session-composer-dock.spec.ts | 21 +++++----- packages/app/src/components/prompt-input.tsx | 39 +------------------ .../app/src/components/settings-general.tsx | 39 +++++++++++++++++++ 4 files changed, 59 insertions(+), 55 deletions(-) diff --git a/packages/app/e2e/prompt/prompt-shell.spec.ts b/packages/app/e2e/prompt/prompt-shell.spec.ts index 28fa02dcd3..81af4cb1bc 100644 --- a/packages/app/e2e/prompt/prompt-shell.spec.ts +++ b/packages/app/e2e/prompt/prompt-shell.spec.ts @@ -1,6 +1,6 @@ import type { ToolPart } from "@opencode-ai/sdk/v2/client" import { test, expect } from "../fixtures" -import { withSession } from "../actions" +import { closeDialog, openSettings, withSession } from "../actions" import { promptModelSelector, promptSelector, promptVariantSelector } from "../selectors" const isBash = (part: unknown): part is ToolPart => { @@ -19,12 +19,15 @@ test("shell mode runs a command in the project directory", async ({ page, projec await withSession(project.sdk, `e2e shell ${Date.now()}`, async (session) => { project.trackSession(session.id) await project.gotoSession(session.id) - const button = page.locator('[data-action="prompt-permissions"]').first() - await expect(button).toBeVisible() - if ((await button.getAttribute("aria-pressed")) !== "true") { - await button.click() - await expect(button).toHaveAttribute("aria-pressed", "true") + const dialog = await openSettings(page) + const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first() + const input = toggle.locator('[data-slot="switch-input"]').first() + await expect(toggle).toBeVisible() + if ((await input.getAttribute("aria-checked")) !== "true") { + await toggle.locator('[data-slot="switch-control"]').click() + await expect(input).toHaveAttribute("aria-checked", "true") } + await closeDialog(page, dialog) await project.shell(cmd) await expect diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts index 8eeac5b1a1..ecacea83dc 100644 --- a/packages/app/e2e/session/session-composer-dock.spec.ts +++ b/packages/app/e2e/session/session-composer-dock.spec.ts @@ -5,7 +5,7 @@ import { type ComposerProbeState, type ComposerWindow, } from "../../src/testing/session-composer" -import { cleanupSession, clearSessionDockSeed, seedSessionQuestion } from "../actions" +import { cleanupSession, clearSessionDockSeed, closeDialog, openSettings, seedSessionQuestion } from "../actions" import { permissionDockSelector, promptSelector, @@ -65,12 +65,14 @@ async function clearPermissionDock(page: any, label: RegExp) { } async function setAutoAccept(page: any, enabled: boolean) { - const button = page.locator('[data-action="prompt-permissions"]').first() - await expect(button).toBeVisible() - const pressed = (await button.getAttribute("aria-pressed")) === "true" - if (pressed === enabled) return - await button.click() - await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false") + const dialog = await openSettings(page) + const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first() + const input = toggle.locator('[data-slot="switch-input"]').first() + await expect(toggle).toBeVisible() + const checked = (await input.getAttribute("aria-checked")) === "true" + if (checked !== enabled) await toggle.locator('[data-slot="switch-control"]').click() + await expect(input).toHaveAttribute("aria-checked", enabled ? "true" : "false") + await closeDialog(page, dialog) } async function expectQuestionBlocked(page: any) { @@ -277,6 +279,7 @@ test("default dock shows prompt input", async ({ page, project }) => { await expect(page.locator(sessionComposerDockSelector)).toBeVisible() await expect(page.locator(promptSelector)).toBeVisible() + await expect(page.locator('[data-action="prompt-permissions"]')).toHaveCount(0) await expect(page.locator(questionDockSelector)).toHaveCount(0) await expect(page.locator(permissionDockSelector)).toHaveCount(0) @@ -290,10 +293,6 @@ test("default dock shows prompt input", async ({ page, project }) => { test("auto-accept toggle works before first submit", async ({ page, project }) => { await project.open() - const button = page.locator('[data-action="prompt-permissions"]').first() - await expect(button).toBeVisible() - await expect(button).toHaveAttribute("aria-pressed", "false") - await setAutoAccept(page, true) await setAutoAccept(page, false) }) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index e9049ae7e2..eedbc91cfd 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1079,17 +1079,6 @@ export const PromptInput: Component = (props) => { if (!id) return permission.isAutoAcceptingDirectory(sdk.directory) return permission.isAutoAccepting(id, sdk.directory) }) - const acceptLabel = createMemo(() => - language.t(accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable"), - ) - const toggleAccept = () => { - if (!params.id) { - permission.toggleAutoAcceptDirectory(sdk.directory) - return - } - - permission.toggleAutoAccept(params.id, sdk.directory) - } const { abort, handleSubmit } = createPromptSubmit({ info, @@ -1333,11 +1322,7 @@ export const PromptInput: Component = (props) => { onMouseDown={(e) => { const target = e.target if (!(target instanceof HTMLElement)) return - if ( - target.closest( - '[data-action="prompt-attach"], [data-action="prompt-submit"], [data-action="prompt-permissions"]', - ) - ) { + if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) { return } editorRef?.focus() @@ -1597,28 +1582,6 @@ export const PromptInput: Component = (props) => { - - - diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 8a4d498866..b4ac061df4 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -8,7 +8,9 @@ import { TextField } from "@opencode-ai/ui/text-field" import { Tooltip } from "@opencode-ai/ui/tooltip" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" import { showToast } from "@opencode-ai/ui/toast" +import { useParams } from "@solidjs/router" import { useLanguage } from "@/context/language" +import { usePermission } from "@/context/permission" import { usePlatform } from "@/context/platform" import { monoDefault, @@ -19,6 +21,7 @@ import { sansInput, useSettings, } from "@/context/settings" +import { decode64 } from "@/utils/base64" import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" import { Link } from "./link" import { SettingsList } from "./settings-list" @@ -64,7 +67,9 @@ const playDemoSound = (id: string | undefined) => { export const SettingsGeneral: Component = () => { const theme = useTheme() const language = useLanguage() + const permission = usePermission() const platform = usePlatform() + const params = useParams() const settings = useSettings() onMount(() => { @@ -76,6 +81,31 @@ export const SettingsGeneral: Component = () => { }) const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux") + const dir = createMemo(() => decode64(params.dir)) + const accepting = createMemo(() => { + const value = dir() + if (!value) return false + if (!params.id) return permission.isAutoAcceptingDirectory(value) + return permission.isAutoAccepting(params.id, value) + }) + + const toggleAccept = (checked: boolean) => { + const value = dir() + if (!value) return + + if (!params.id) { + if (permission.isAutoAcceptingDirectory(value) === checked) return + permission.toggleAutoAcceptDirectory(value) + return + } + + if (checked) { + permission.enableAutoAccept(params.id, value) + return + } + + permission.disableAutoAccept(params.id, value) + } const check = () => { if (!platform.checkUpdate) return @@ -201,6 +231,15 @@ export const SettingsGeneral: Component = () => { /> + +
+ +
+
+