From adaee663641e0a7e4a8f1bc3f0ca1bf1b9439a40 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 5 Mar 2026 18:39:17 -0500 Subject: [PATCH 001/104] zen: gpt 5.4 pro --- packages/web/src/content/docs/zen.mdx | 86 ++++++++++++++------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 883893a642..330f90014d 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -64,6 +64,7 @@ You can also access our models through the following API endpoints. | Model | Model ID | Endpoint | AI SDK Package | | ------------------- | ------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -119,48 +120,49 @@ https://opencode.ai/zen/v1/models We support a pay-as-you-go model. Below are the prices **per 1M tokens**. -| Model | Input | Output | Cached Read | Cached Write | -| --------------------------------- | ------ | ------ | ----------- | ------------ | -| Big Pickle | Free | Free | Free | - | -| MiniMax M2.5 Free | Free | Free | Free | - | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | -| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - | -| GLM 5 | $1.00 | $3.20 | $0.20 | - | -| GLM 4.7 | $0.60 | $2.20 | $0.10 | - | -| GLM 4.6 | $0.60 | $2.20 | $0.10 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | -| Kimi K2 Thinking | $0.40 | $2.50 | - | - | -| Kimi K2 | $0.40 | $2.50 | - | - | -| Qwen3 Coder 480B | $0.45 | $1.50 | - | - | -| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 | -| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | -| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | -| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | -| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | -| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | -| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | -| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | -| GPT 5.3 Codex Spark | $1.75 | $14.00 | $0.175 | - | -| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - | -| GPT 5.2 | $1.75 | $14.00 | $0.175 | - | -| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - | -| GPT 5.1 | $1.07 | $8.50 | $0.107 | - | -| GPT 5.1 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5.1 Codex Max | $1.25 | $10.00 | $0.125 | - | -| GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | -| GPT 5 | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| Model | Input | Output | Cached Read | Cached Write | +| --------------------------------- | ------ | ------- | ----------- | ------------ | +| Big Pickle | Free | Free | Free | - | +| MiniMax M2.5 Free | Free | Free | Free | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | +| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - | +| GLM 5 | $1.00 | $3.20 | $0.20 | - | +| GLM 4.7 | $0.60 | $2.20 | $0.10 | - | +| GLM 4.6 | $0.60 | $2.20 | $0.10 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | +| Kimi K2 Thinking | $0.40 | $2.50 | - | - | +| Kimi K2 | $0.40 | $2.50 | - | - | +| Qwen3 Coder 480B | $0.45 | $1.50 | - | - | +| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 | +| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 | +| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | +| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | +| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | +| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | +| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | +| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | +| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | +| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | +| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | +| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | +| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | +| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | +| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | +| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | +| GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.3 Codex Spark | $1.75 | $14.00 | $0.175 | - | +| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - | +| GPT 5.2 | $1.75 | $14.00 | $0.175 | - | +| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - | +| GPT 5.1 | $1.07 | $8.50 | $0.107 | - | +| GPT 5.1 Codex | $1.07 | $8.50 | $0.107 | - | +| GPT 5.1 Codex Max | $1.25 | $10.00 | $0.125 | - | +| GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | +| GPT 5 | $1.07 | $8.50 | $0.107 | - | +| GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | +| GPT 5 Nano | Free | Free | Free | - | You might notice _Claude Haiku 3.5_ in your usage history. This is a [low cost model](/docs/config/#models) that's used to generate the titles of your sessions. From 39691e51741c753852a2ebdc21424075e666671b Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 5 Mar 2026 23:43:19 +0000 Subject: [PATCH 002/104] tui: remove keyboard shortcut tooltips from new session and new workspace buttons in the sidebar --- packages/app/src/pages/layout.tsx | 34 ++++++++++--------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2019ca4e5a..bd0315efbf 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -22,7 +22,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Dialog } from "@opencode-ai/ui/dialog" import { getFilename } from "@opencode-ai/util/path" @@ -1937,20 +1937,14 @@ export default function Layout(props: ParentProps) { fallback={ <>
- navigateWithSidebarReset(`/${base64Encode(p.worktree)}/session`)} > - - + {language.t("command.session.new")} +
<>
- - - +
Date: Fri, 6 Mar 2026 01:23:48 +0100 Subject: [PATCH 003/104] fix(app): stale show (#16236) --- packages/app/src/pages/session/message-timeline.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 7a3b72ae4e..f320a2ebbf 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -1,4 +1,4 @@ -import { For, createEffect, createMemo, on, onCleanup, Show, startTransition, Index, type JSX } from "solid-js" +import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js" import { createStore, produce } from "solid-js/store" import { useNavigate, useParams } from "@solidjs/router" import { Button } from "@opencode-ai/ui/button" @@ -160,7 +160,7 @@ function createTimelineStaging(input: TimelineStageInput) { } const currentTotal = input.messages().length count = Math.min(currentTotal, count + input.config.batch) - startTransition(() => setState("count", count)) + setState("count", count) if (count >= currentTotal) { setState({ completedSession: sessionKey, activeSession: "" }) frame = undefined From 3ebba02d04d17efaa8339d94950d1467ac5c956f Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 5 Mar 2026 21:54:06 -0500 Subject: [PATCH 004/104] refactor: replace Bun.sleep with node timers (#15013) --- github/index.ts | 3 ++- packages/opencode/src/cli/cmd/auth.ts | 3 ++- packages/opencode/src/cli/cmd/debug/lsp.ts | 3 ++- packages/opencode/src/cli/cmd/github.ts | 5 +++-- packages/opencode/src/cli/cmd/tui/worker.ts | 5 +++-- packages/opencode/src/plugin/codex.ts | 3 ++- packages/opencode/src/plugin/copilot.ts | 7 ++++--- packages/opencode/src/shell/shell.ts | 5 +++-- packages/opencode/test/preload.ts | 3 ++- packages/opencode/test/pty/pty-output-isolation.test.ts | 7 ++++--- packages/opencode/test/session/retry.test.ts | 3 ++- 11 files changed, 29 insertions(+), 18 deletions(-) diff --git a/github/index.ts b/github/index.ts index da310178a7..1a0a992622 100644 --- a/github/index.ts +++ b/github/index.ts @@ -8,6 +8,7 @@ import type { Context as GitHubContext } from "@actions/github/lib/context" import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types" import { createOpencodeClient } from "@opencode-ai/sdk" import { spawn } from "node:child_process" +import { setTimeout as sleep } from "node:timers/promises" type GitHubAuthor = { login: string @@ -281,7 +282,7 @@ async function assertOpencodeConnected() { connected = true break } catch (e) {} - await Bun.sleep(300) + await sleep(300) } while (retry++ < 30) if (!connected) { diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index a7300e0293..38fba0ce70 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -13,6 +13,7 @@ import { Instance } from "../../project/instance" import type { Hooks } from "@opencode-ai/plugin" import { Process } from "../../util/process" import { text } from "node:stream/consumers" +import { setTimeout as sleep } from "node:timers/promises" type PluginAuth = NonNullable @@ -47,7 +48,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, const method = plugin.auth.methods[index] // Handle prompts for all auth types - await Bun.sleep(10) + await sleep(10) const inputs: Record = {} if (method.prompts) { for (const prompt of method.prompts) { diff --git a/packages/opencode/src/cli/cmd/debug/lsp.ts b/packages/opencode/src/cli/cmd/debug/lsp.ts index d83c4ed8a4..4b8a3e7d45 100644 --- a/packages/opencode/src/cli/cmd/debug/lsp.ts +++ b/packages/opencode/src/cli/cmd/debug/lsp.ts @@ -3,6 +3,7 @@ import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" import { Log } from "../../../util/log" import { EOL } from "os" +import { setTimeout as sleep } from "node:timers/promises" export const LSPCommand = cmd({ command: "lsp", @@ -19,7 +20,7 @@ const DiagnosticsCommand = cmd({ async handler(args) { await bootstrap(process.cwd(), async () => { await LSP.touchFile(args.file, true) - await Bun.sleep(1000) + await sleep(1000) process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL) }) }, diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 672e73d49a..2491abc567 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -28,6 +28,7 @@ import { Bus } from "../../bus" import { MessageV2 } from "../../session/message-v2" import { SessionPrompt } from "@/session/prompt" import { $ } from "bun" +import { setTimeout as sleep } from "node:timers/promises" type GitHubAuthor = { login: string @@ -353,7 +354,7 @@ export const GithubInstallCommand = cmd({ } retries++ - await Bun.sleep(1000) + await sleep(1000) } while (true) s.stop("Installed GitHub app") @@ -1372,7 +1373,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"` } catch (e) { if (retries > 0) { console.log(`Retrying after ${delayMs}ms...`) - await Bun.sleep(delayMs) + await sleep(delayMs) return withRetry(fn, retries - 1, delayMs) } throw e diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index e63f10ba80..4452d6d764 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -10,6 +10,7 @@ import { GlobalBus } from "@/bus/global" import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2" import type { BunWebSocketData } from "hono/bun" import { Flag } from "@/flag/flag" +import { setTimeout as sleep } from "node:timers/promises" await Log.init({ print: process.argv.includes("--print-logs"), @@ -75,7 +76,7 @@ const startEventStream = (directory: string) => { ).catch(() => undefined) if (!events) { - await Bun.sleep(250) + await sleep(250) continue } @@ -84,7 +85,7 @@ const startEventStream = (directory: string) => { } if (!signal.aborted) { - await Bun.sleep(250) + await sleep(250) } } })().catch((error) => { diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 56931b2ed6..c9afd2a864 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -4,6 +4,7 @@ import { Installation } from "../installation" import { Auth, OAUTH_DUMMY_KEY } from "../auth" import os from "os" import { ProviderTransform } from "@/provider/transform" +import { setTimeout as sleep } from "node:timers/promises" const log = Log.create({ service: "plugin.codex" }) @@ -602,7 +603,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { return { type: "failed" as const } } - await Bun.sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS) + await sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS) } }, } diff --git a/packages/opencode/src/plugin/copilot.ts b/packages/opencode/src/plugin/copilot.ts index 39ea0d00d2..3945c63ce2 100644 --- a/packages/opencode/src/plugin/copilot.ts +++ b/packages/opencode/src/plugin/copilot.ts @@ -1,6 +1,7 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import { Installation } from "@/installation" import { iife } from "@/util/iife" +import { setTimeout as sleep } from "node:timers/promises" const CLIENT_ID = "Ov23li8tweQw6odWQebz" // Add a small safety buffer when polling to avoid hitting the server @@ -270,7 +271,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { } if (data.error === "authorization_pending") { - await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS) + await sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS) continue } @@ -286,13 +287,13 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { newInterval = serverInterval * 1000 } - await Bun.sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS) + await sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS) continue } if (data.error) return { type: "failed" as const } - await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS) + await sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS) continue } }, diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index e7b7cdb3e4..4779cfef75 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -3,6 +3,7 @@ import { lazy } from "@/util/lazy" import { Filesystem } from "@/util/filesystem" import path from "path" import { spawn, type ChildProcess } from "child_process" +import { setTimeout as sleep } from "node:timers/promises" const SIGKILL_TIMEOUT_MS = 200 @@ -22,13 +23,13 @@ export namespace Shell { try { process.kill(-pid, "SIGTERM") - await Bun.sleep(SIGKILL_TIMEOUT_MS) + await sleep(SIGKILL_TIMEOUT_MS) if (!opts?.exited?.()) { process.kill(-pid, "SIGKILL") } } catch (_e) { proc.kill("SIGTERM") - await Bun.sleep(SIGKILL_TIMEOUT_MS) + await sleep(SIGKILL_TIMEOUT_MS) if (!opts?.exited?.()) { proc.kill("SIGKILL") } diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 41028633e8..caac3bb0de 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -3,6 +3,7 @@ import os from "os" import path from "path" import fs from "fs/promises" +import { setTimeout as sleep } from "node:timers/promises" import { afterAll } from "bun:test" // Set XDG env vars FIRST, before any src/ imports @@ -15,7 +16,7 @@ afterAll(async () => { typeof error === "object" && error !== null && "code" in error && error.code === "EBUSY" const rm = async (left: number): Promise => { Bun.gc(true) - await Bun.sleep(100) + await sleep(100) return fs.rm(dir, { recursive: true, force: true }).catch((error) => { if (!busy(error)) throw error if (left <= 1) throw error diff --git a/packages/opencode/test/pty/pty-output-isolation.test.ts b/packages/opencode/test/pty/pty-output-isolation.test.ts index 44858a0ed2..ec1bbd4690 100644 --- a/packages/opencode/test/pty/pty-output-isolation.test.ts +++ b/packages/opencode/test/pty/pty-output-isolation.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test" import { Instance } from "../../src/project/instance" import { Pty } from "../../src/pty" import { tmpdir } from "../fixture/fixture" +import { setTimeout as sleep } from "node:timers/promises" describe("pty", () => { test("does not leak output when websocket objects are reused", async () => { @@ -43,7 +44,7 @@ describe("pty", () => { // Output from a must never show up in b. Pty.write(a.id, "AAA\n") - await Bun.sleep(100) + await sleep(100) expect(outB.join("")).not.toContain("AAA") } finally { @@ -88,7 +89,7 @@ describe("pty", () => { } Pty.write(a.id, "AAA\n") - await Bun.sleep(100) + await sleep(100) expect(outB.join("")).not.toContain("AAA") } finally { @@ -128,7 +129,7 @@ describe("pty", () => { ctx.connId = 2 Pty.write(a.id, "AAA\n") - await Bun.sleep(100) + await sleep(100) expect(out.join("")).toContain("AAA") } finally { diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 6768e72d95..eba4a99505 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import type { NamedError } from "@opencode-ai/util/error" import { APICallError } from "ai" +import { setTimeout as sleep } from "node:timers/promises" import { SessionRetry } from "../../src/session/retry" import { MessageV2 } from "../../src/session/message-v2" @@ -135,7 +136,7 @@ describe("session.message-v2.fromError", () => { new ReadableStream({ async pull(controller) { controller.enqueue("Hello,") - await Bun.sleep(10000) + await sleep(10000) controller.enqueue(" World!") controller.close() }, From 6e89d3e597c31fc160306763a37ab368ca2232d1 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 5 Mar 2026 21:56:41 -0500 Subject: [PATCH 005/104] refactor: replace Bun.write/file with Filesystem utilities in snapshot --- packages/opencode/src/snapshot/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index cf254b4cef..1acbdba092 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -1,6 +1,7 @@ import { $ } from "bun" import path from "path" import fs from "fs/promises" +import { Filesystem } from "../util/filesystem" import { Log } from "../util/log" import { Flag } from "../flag/flag" import { Global } from "../global" @@ -271,13 +272,12 @@ export namespace Snapshot { const target = path.join(git, "info", "exclude") await fs.mkdir(path.join(git, "info"), { recursive: true }) if (!file) { - await Bun.write(target, "") + await Filesystem.write(target, "") return } - const text = await Bun.file(file) - .text() - .catch(() => "") - await Bun.write(target, text) + const text = await Filesystem.readText(file).catch(() => "") + + await Filesystem.write(target, text) } async function excludes() { From dad248832d31521d978a4ed24d9ade5e19abc160 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 5 Mar 2026 21:59:20 -0500 Subject: [PATCH 006/104] refactor: replace Bun.write with Filesystem.write in config files --- packages/opencode/src/config/config.ts | 4 +++- packages/opencode/src/config/migrate-tui-config.ts | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 28c5b239a4..6b4242a225 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1240,7 +1240,7 @@ export namespace Config { if (!parsed.data.$schema && isFile) { parsed.data.$schema = "https://opencode.ai/config.json" const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://opencode.ai/config.json",') - await Bun.write(options.path, updated).catch(() => {}) + await Filesystem.write(options.path, updated).catch(() => {}) } const data = parsed.data if (data.plugin && isFile) { @@ -1401,3 +1401,5 @@ export namespace Config { return state().then((x) => x.directories) } } +Filesystem.write +Filesystem.write diff --git a/packages/opencode/src/config/migrate-tui-config.ts b/packages/opencode/src/config/migrate-tui-config.ts index b426e4fbd1..dbe33ffb42 100644 --- a/packages/opencode/src/config/migrate-tui-config.ts +++ b/packages/opencode/src/config/migrate-tui-config.ts @@ -70,7 +70,7 @@ export async function migrateTuiConfig(input: MigrateInput) { if (extracted.keybinds !== undefined) payload.keybinds = extracted.keybinds if (tui) Object.assign(payload, tui) - const wrote = await Bun.write(target, JSON.stringify(payload, null, 2)) + const wrote = await Filesystem.write(target, JSON.stringify(payload, null, 2)) .then(() => true) .catch((error) => { log.warn("failed to write tui migration target", { from: file, to: target, error }) @@ -104,7 +104,7 @@ async function backupAndStripLegacy(file: string, source: string) { const hasBackup = await Filesystem.exists(backup) const backed = hasBackup ? true - : await Bun.write(backup, source) + : await Filesystem.write(backup, source) .then(() => true) .catch((error) => { log.warn("failed to backup source config during tui migration", { path: file, backup, error }) @@ -123,7 +123,7 @@ async function backupAndStripLegacy(file: string, source: string) { return applyEdits(acc, edits) }, source) - return Bun.write(file, text) + return Filesystem.write(file, text) .then(() => { log.info("stripped tui keys from server config", { path: file, backup }) return true From a9bf1c050544bbf8e7975e15f1cdcf1dbde23ce0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 5 Mar 2026 22:03:24 -0500 Subject: [PATCH 007/104] refactor: replace Bun.hash with Hash.fast using xxhash3-xxh64 --- packages/opencode/src/acp/agent.ts | 3 ++- packages/opencode/src/provider/provider.ts | 7 ++++--- packages/opencode/src/util/hash.ts | 7 +++++++ 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/src/util/hash.ts diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 8b338f1b57..d518dd12a1 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -31,6 +31,7 @@ import { import { Log } from "../util/log" import { pathToFileURL } from "bun" import { Filesystem } from "../util/filesystem" +import { Hash } from "../util/hash" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { Provider } from "../provider/provider" @@ -281,7 +282,7 @@ export namespace ACP { const output = this.bashOutput(part) const content: ToolCallContent[] = [] if (output) { - const hash = String(Bun.hash(output)) + const hash = Hash.fast(output) if (part.tool === "bash") { if (this.bashSnapshots.get(part.callID) === hash) { await this.connection diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 8170383652..b4836ae047 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -6,9 +6,10 @@ import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" import { Log } from "../util/log" import { BunProc } from "../bun" +import { Hash } from "../util/hash" import { Plugin } from "../plugin" -import { ModelsDev } from "./models" import { NamedError } from "@opencode-ai/util/error" +import { ModelsDev } from "./models" import { Auth } from "../auth" import { Env } from "../env" import { Instance } from "../project/instance" @@ -795,7 +796,7 @@ export namespace Provider { const modelLoaders: { [providerID: string]: CustomModelLoader } = {} - const sdk = new Map() + const sdk = new Map() log.info("init") @@ -1085,7 +1086,7 @@ export namespace Provider { ...model.headers, } - const key = Bun.hash.xxHash32(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options })) + const key = Hash.fast(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options })) const existing = s.sdk.get(key) if (existing) return existing diff --git a/packages/opencode/src/util/hash.ts b/packages/opencode/src/util/hash.ts new file mode 100644 index 0000000000..648148f93d --- /dev/null +++ b/packages/opencode/src/util/hash.ts @@ -0,0 +1,7 @@ +import { createHash } from "crypto" + +export namespace Hash { + export function fast(input: string | Buffer): string { + return createHash("xxhash3-xxh64").update(input).digest("hex") + } +} From ae5c9ed3dd7a393e540643f3ecc79b9dea793ecc Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 5 Mar 2026 22:04:20 -0500 Subject: [PATCH 008/104] refactor: replace Bun.stdin.text with Node.js stream reading --- packages/opencode/src/cli/cmd/run.ts | 9 ++++++++- packages/opencode/src/cli/cmd/tui/thread.ts | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 61bc609bb7..be546beff8 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -337,7 +337,14 @@ export const RunCommand = cmd({ } } - if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text()) + if (!process.stdin.isTTY) { + const stdinText = await new Promise((resolve) => { + const chunks: Buffer[] = [] + process.stdin.on("data", (chunk) => chunks.push(chunk)) + process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8"))) + }) + message += "\n" + stdinText + } if (message.trim().length === 0 && !args.command) { UI.error("You must provide a message or a command") diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index f53cc39255..cca96dd8c6 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -53,7 +53,13 @@ async function target() { } async function input(value?: string) { - const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text() + const piped = process.stdin.isTTY + ? undefined + : await new Promise((resolve) => { + const chunks: Buffer[] = [] + process.stdin.on("data", (chunk) => chunks.push(chunk)) + process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8"))) + }) if (!value) return piped if (!piped) return value return piped + "\n" + value From 7e280983654f7943086263b2fe352c05d24fdb4e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 5 Mar 2026 22:08:50 -0500 Subject: [PATCH 009/104] refactor: use node:stream/consumers for stdin reading --- packages/opencode/src/cli/cmd/run.ts | 10 ++-------- packages/opencode/src/cli/cmd/tui/thread.ts | 9 ++------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index be546beff8..d74eb2aa4c 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -6,6 +6,7 @@ import { cmd } from "./cmd" import { Flag } from "../../flag/flag" import { bootstrap } from "../bootstrap" import { EOL } from "os" +import { text as streamText } from "node:stream/consumers" import { Filesystem } from "../../util/filesystem" import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" @@ -337,14 +338,7 @@ export const RunCommand = cmd({ } } - if (!process.stdin.isTTY) { - const stdinText = await new Promise((resolve) => { - const chunks: Buffer[] = [] - process.stdin.on("data", (chunk) => chunks.push(chunk)) - process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8"))) - }) - message += "\n" + stdinText - } + if (!process.stdin.isTTY) message += "\n" + (await streamText(process.stdin)) if (message.trim().length === 0 && !args.command) { UI.error("You must provide a message or a command") diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index cca96dd8c6..57acfd199e 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -3,6 +3,7 @@ import { tui } from "./app" import { Rpc } from "@/util/rpc" import { type rpc } from "./worker" import path from "path" +import { text as streamText } from "node:stream/consumers" import { fileURLToPath } from "url" import { UI } from "@/cli/ui" import { Log } from "@/util/log" @@ -53,13 +54,7 @@ async function target() { } async function input(value?: string) { - const piped = process.stdin.isTTY - ? undefined - : await new Promise((resolve) => { - const chunks: Buffer[] = [] - process.stdin.on("data", (chunk) => chunks.push(chunk)) - process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8"))) - }) + const piped = process.stdin.isTTY ? undefined : await streamText(process.stdin) if (!value) return piped if (!piped) return value return piped + "\n" + value From 6733a5a822f9927081f8bccfc41de3842b29858d Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 5 Mar 2026 22:12:10 -0500 Subject: [PATCH 010/104] fix: use sha1 for hash instead of unsupported xxhash3-xxh64 --- packages/opencode/src/util/hash.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/util/hash.ts b/packages/opencode/src/util/hash.ts index 648148f93d..680e0f40bc 100644 --- a/packages/opencode/src/util/hash.ts +++ b/packages/opencode/src/util/hash.ts @@ -2,6 +2,6 @@ import { createHash } from "crypto" export namespace Hash { export function fast(input: string | Buffer): string { - return createHash("xxhash3-xxh64").update(input).digest("hex") + return createHash("sha1").update(input).digest("hex") } } From bf35a865ba78dcae56297270cf0afecf1476e786 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 5 Mar 2026 22:17:08 -0500 Subject: [PATCH 011/104] refactor: replace Bun.connect with net.createConnection --- packages/opencode/src/mcp/oauth-callback.ts | 22 +++++++-------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index bb3b56f2e9..db8e621d6c 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -1,3 +1,4 @@ +import { createConnection } from "net" import { Log } from "../util/log" import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider" @@ -160,21 +161,12 @@ export namespace McpOAuthCallback { export async function isPortInUse(): Promise { return new Promise((resolve) => { - Bun.connect({ - hostname: "127.0.0.1", - port: OAUTH_CALLBACK_PORT, - socket: { - open(socket) { - socket.end() - resolve(true) - }, - error() { - resolve(false) - }, - data() {}, - close() {}, - }, - }).catch(() => { + const socket = createConnection(OAUTH_CALLBACK_PORT, "127.0.0.1") + socket.on("connect", () => { + socket.destroy() + resolve(true) + }) + socket.on("error", () => { resolve(false) }) }) From d68afcaa55e82e0d9d4d093db7988abee7c27752 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 5 Mar 2026 22:20:16 -0500 Subject: [PATCH 012/104] refactor: replace Bun.stderr and Bun.color with Node.js equivalents --- packages/opencode/src/cli/ui.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index f242a77f6c..39396997c6 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -25,12 +25,12 @@ export namespace UI { export function println(...message: string[]) { print(...message) - Bun.stderr.write(EOL) + process.stderr.write(EOL) } export function print(...message: string[]) { blank = false - Bun.stderr.write(message.join(" ")) + process.stderr.write(message.join(" ")) } let blank = false @@ -44,7 +44,7 @@ export namespace UI { const result: string[] = [] const reset = "\x1b[0m" const left = { - fg: Bun.color("gray", "ansi") ?? "", + fg: "\x1b[90m", shadow: "\x1b[38;5;235m", bg: "\x1b[48;5;235m", } From 46d7d2fdc06bec2aa08ce17b03fb73a973495764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sad=C4=B1k?= <118531001+msadiks@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:03:01 +0300 Subject: [PATCH 013/104] feat: add "gpt-5.4" to codex allowed models list (#16274) --- packages/opencode/src/plugin/codex.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index c9afd2a864..5c0140e570 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -362,6 +362,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { "gpt-5.1-codex-max", "gpt-5.1-codex-mini", "gpt-5.2", + "gpt-5.4", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.1-codex", From cb411248bf0898b351ae282a7c0b8b9b6d6fa340 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 6 Mar 2026 04:29:10 +0000 Subject: [PATCH 014/104] release: v1.2.19 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 325eeb15f5..2638e6b722 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -76,7 +76,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -110,7 +110,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -137,7 +137,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -161,7 +161,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -185,7 +185,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -218,7 +218,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -248,7 +248,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -277,7 +277,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -293,7 +293,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.18", + "version": "1.2.19", "bin": { "opencode": "./bin/opencode", }, @@ -407,7 +407,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -427,7 +427,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.18", + "version": "1.2.19", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -438,7 +438,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -473,7 +473,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -519,7 +519,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "zod": "catalog:", }, @@ -530,7 +530,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 37ccd9b53a..809a285374 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.18", + "version": "1.2.19", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 67a7eafa7c..269b005a86 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.18", + "version": "1.2.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 37c94aecbb..199b5d9bd6 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.18", + "version": "1.2.19", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index bb7caccc58..e771aae844 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.18", + "version": "1.2.19", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 00e7378fe5..ef8d7c5994 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 41791066ea..e683d185bc 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.2.18", + "version": "1.2.19", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 49699ff85e..10e6df26b1 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.18", + "version": "1.2.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 065015bc50..ef56add880 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.18", + "version": "1.2.19", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index efd12836ee..1637c58a4d 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.18" +version = "1.2.19" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.18/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.18/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.18/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.18/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.18/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 162b564ad9..efaae65f53 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.18", + "version": "1.2.19", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 5edfca7e19..30af3c347c 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.18", + "version": "1.2.19", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index e57e83d051..d8911922ec 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.18", + "version": "1.2.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 22993464e3..fe3ce393d6 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.18", + "version": "1.2.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 1407c0e4a9..d37e140581 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.18", + "version": "1.2.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 0adc4c57f1..48c668c6fb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.18", + "version": "1.2.19", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 87cf20591e..bc84be6360 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.18", + "version": "1.2.19", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 4e7ac7cd8b..c85ba1a0b9 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.18", + "version": "1.2.19", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 64307f053d..215143ac99 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.18", + "version": "1.2.19", "publisher": "sst-dev", "repository": { "type": "git", From 74effa8eec01b3cb816178a37921333420fe2d82 Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 6 Mar 2026 00:18:29 -0500 Subject: [PATCH 015/104] refactor(opencode): replace Bun.which with npm which (#15012) --- bun.lock | 16 +++- packages/opencode/package.json | 2 + packages/opencode/src/cli/cmd/session.ts | 5 +- .../src/cli/cmd/tui/util/clipboard.ts | 9 +- packages/opencode/src/file/ripgrep.ts | 3 +- packages/opencode/src/format/formatter.ts | 43 ++++----- packages/opencode/src/lsp/server.ts | 89 ++++++++++--------- packages/opencode/src/project/project.ts | 3 +- packages/opencode/src/shell/shell.ts | 5 +- packages/opencode/src/util/which.ts | 10 +++ packages/opencode/test/util/which.test.ts | 82 +++++++++++++++++ 11 files changed, 190 insertions(+), 77 deletions(-) create mode 100644 packages/opencode/src/util/which.ts create mode 100644 packages/opencode/test/util/which.test.ts diff --git a/bun.lock b/bun.lock index 2638e6b722..6539faa830 100644 --- a/bun.lock +++ b/bun.lock @@ -373,6 +373,7 @@ "ulid": "catalog:", "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", + "which": "6.0.1", "xdg-basedir": "5.1.0", "yargs": "18.0.0", "zod": "catalog:", @@ -395,6 +396,7 @@ "@types/bun": "catalog:", "@types/mime-types": "3.0.1", "@types/turndown": "5.0.5", + "@types/which": "3.0.4", "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", "drizzle-kit": "1.0.0-beta.12-a5629fb", @@ -2120,6 +2122,8 @@ "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], @@ -3236,7 +3240,7 @@ "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], - "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], @@ -4586,7 +4590,7 @@ "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], - "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + "which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -5202,6 +5206,8 @@ "app-builder-lib/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="], + "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -5388,6 +5394,8 @@ "node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], + "node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], @@ -5918,6 +5926,8 @@ "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + "archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "archiver-utils/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -6000,6 +6010,8 @@ "node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], + "node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], "opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 30af3c347c..fde3ace92f 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -43,6 +43,7 @@ "@types/mime-types": "3.0.1", "@types/turndown": "5.0.5", "@types/yargs": "17.0.33", + "@types/which": "3.0.4", "@typescript/native-preview": "catalog:", "drizzle-kit": "1.0.0-beta.12-a5629fb", "drizzle-orm": "1.0.0-beta.12-a5629fb", @@ -127,6 +128,7 @@ "ulid": "catalog:", "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", + "which": "6.0.1", "xdg-basedir": "5.1.0", "yargs": "18.0.0", "zod": "catalog:", diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 7fb5fda97b..84840392a6 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -9,6 +9,7 @@ import { Filesystem } from "../../util/filesystem" import { Process } from "../../util/process" import { EOL } from "os" import path from "path" +import { which } from "../../util/which" function pagerCmd(): string[] { const lessOptions = ["-R", "-S"] @@ -17,7 +18,7 @@ function pagerCmd(): string[] { } // user could have less installed via other options - const lessOnPath = Bun.which("less") + const lessOnPath = which("less") if (lessOnPath) { if (Filesystem.stat(lessOnPath)?.size) return [lessOnPath, ...lessOptions] } @@ -27,7 +28,7 @@ function pagerCmd(): string[] { if (Filesystem.stat(less)?.size) return [less, ...lessOptions] } - const git = Bun.which("git") + const git = which("git") if (git) { const less = path.join(git, "..", "..", "usr", "bin", "less.exe") if (Filesystem.stat(less)?.size) return [less, ...lessOptions] diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 1a8197bf4e..412ec654ff 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -6,6 +6,7 @@ import { tmpdir } from "os" import path from "path" import { Filesystem } from "../../../../util/filesystem" import { Process } from "../../../../util/process" +import { which } from "../../../../util/which" /** * Writes text to clipboard via OSC 52 escape sequence. @@ -76,7 +77,7 @@ export namespace Clipboard { const getCopyMethod = lazy(() => { const os = platform() - if (os === "darwin" && Bun.which("osascript")) { + if (os === "darwin" && which("osascript")) { console.log("clipboard: using osascript") return async (text: string) => { const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"') @@ -85,7 +86,7 @@ export namespace Clipboard { } if (os === "linux") { - if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) { + if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) { console.log("clipboard: using wl-copy") return async (text: string) => { const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" }) @@ -95,7 +96,7 @@ export namespace Clipboard { await proc.exited.catch(() => {}) } } - if (Bun.which("xclip")) { + if (which("xclip")) { console.log("clipboard: using xclip") return async (text: string) => { const proc = Process.spawn(["xclip", "-selection", "clipboard"], { @@ -109,7 +110,7 @@ export namespace Clipboard { await proc.exited.catch(() => {}) } } - if (Bun.which("xsel")) { + if (which("xsel")) { console.log("clipboard: using xsel") return async (text: string) => { const proc = Process.spawn(["xsel", "--clipboard", "--input"], { diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 9c4e9cf028..09fef453c9 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -8,6 +8,7 @@ import { lazy } from "../util/lazy" import { $ } from "bun" import { Filesystem } from "../util/filesystem" import { Process } from "../util/process" +import { which } from "../util/which" import { text } from "node:stream/consumers" import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js" @@ -126,7 +127,7 @@ export namespace Ripgrep { ) const state = lazy(async () => { - const system = Bun.which("rg") + const system = which("rg") if (system) { const stat = await fs.stat(system).catch(() => undefined) if (stat?.isFile()) return { filepath: system } diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 19b9e2cbe9..9e96b2305c 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -3,6 +3,7 @@ import { BunProc } from "../bun" import { Instance } from "../project/instance" import { Filesystem } from "../util/filesystem" import { Process } from "../util/process" +import { which } from "../util/which" import { Flag } from "@/flag/flag" export interface Info { @@ -18,7 +19,7 @@ export const gofmt: Info = { command: ["gofmt", "-w", "$FILE"], extensions: [".go"], async enabled() { - return Bun.which("gofmt") !== null + return which("gofmt") !== null }, } @@ -27,7 +28,7 @@ export const mix: Info = { command: ["mix", "format", "$FILE"], extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"], async enabled() { - return Bun.which("mix") !== null + return which("mix") !== null }, } @@ -152,7 +153,7 @@ export const zig: Info = { command: ["zig", "fmt", "$FILE"], extensions: [".zig", ".zon"], async enabled() { - return Bun.which("zig") !== null + return which("zig") !== null }, } @@ -171,7 +172,7 @@ export const ktlint: Info = { command: ["ktlint", "-F", "$FILE"], extensions: [".kt", ".kts"], async enabled() { - return Bun.which("ktlint") !== null + return which("ktlint") !== null }, } @@ -180,7 +181,7 @@ export const ruff: Info = { command: ["ruff", "format", "$FILE"], extensions: [".py", ".pyi"], async enabled() { - if (!Bun.which("ruff")) return false + if (!which("ruff")) return false const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"] for (const config of configs) { const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree) @@ -210,7 +211,7 @@ export const rlang: Info = { command: ["air", "format", "$FILE"], extensions: [".R"], async enabled() { - const airPath = Bun.which("air") + const airPath = which("air") if (airPath == null) return false try { @@ -239,7 +240,7 @@ export const uvformat: Info = { extensions: [".py", ".pyi"], async enabled() { if (await ruff.enabled()) return false - if (Bun.which("uv") !== null) { + if (which("uv") !== null) { const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" }) const code = await proc.exited return code === 0 @@ -253,7 +254,7 @@ export const rubocop: Info = { command: ["rubocop", "--autocorrect", "$FILE"], extensions: [".rb", ".rake", ".gemspec", ".ru"], async enabled() { - return Bun.which("rubocop") !== null + return which("rubocop") !== null }, } @@ -262,7 +263,7 @@ export const standardrb: Info = { command: ["standardrb", "--fix", "$FILE"], extensions: [".rb", ".rake", ".gemspec", ".ru"], async enabled() { - return Bun.which("standardrb") !== null + return which("standardrb") !== null }, } @@ -271,7 +272,7 @@ export const htmlbeautifier: Info = { command: ["htmlbeautifier", "$FILE"], extensions: [".erb", ".html.erb"], async enabled() { - return Bun.which("htmlbeautifier") !== null + return which("htmlbeautifier") !== null }, } @@ -280,7 +281,7 @@ export const dart: Info = { command: ["dart", "format", "$FILE"], extensions: [".dart"], async enabled() { - return Bun.which("dart") !== null + return which("dart") !== null }, } @@ -289,7 +290,7 @@ export const ocamlformat: Info = { command: ["ocamlformat", "-i", "$FILE"], extensions: [".ml", ".mli"], async enabled() { - if (!Bun.which("ocamlformat")) return false + if (!which("ocamlformat")) return false const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree) return items.length > 0 }, @@ -300,7 +301,7 @@ export const terraform: Info = { command: ["terraform", "fmt", "$FILE"], extensions: [".tf", ".tfvars"], async enabled() { - return Bun.which("terraform") !== null + return which("terraform") !== null }, } @@ -309,7 +310,7 @@ export const latexindent: Info = { command: ["latexindent", "-w", "-s", "$FILE"], extensions: [".tex"], async enabled() { - return Bun.which("latexindent") !== null + return which("latexindent") !== null }, } @@ -318,7 +319,7 @@ export const gleam: Info = { command: ["gleam", "format", "$FILE"], extensions: [".gleam"], async enabled() { - return Bun.which("gleam") !== null + return which("gleam") !== null }, } @@ -327,7 +328,7 @@ export const shfmt: Info = { command: ["shfmt", "-w", "$FILE"], extensions: [".sh", ".bash"], async enabled() { - return Bun.which("shfmt") !== null + return which("shfmt") !== null }, } @@ -336,7 +337,7 @@ export const nixfmt: Info = { command: ["nixfmt", "$FILE"], extensions: [".nix"], async enabled() { - return Bun.which("nixfmt") !== null + return which("nixfmt") !== null }, } @@ -345,7 +346,7 @@ export const rustfmt: Info = { command: ["rustfmt", "$FILE"], extensions: [".rs"], async enabled() { - return Bun.which("rustfmt") !== null + return which("rustfmt") !== null }, } @@ -372,7 +373,7 @@ export const ormolu: Info = { command: ["ormolu", "-i", "$FILE"], extensions: [".hs"], async enabled() { - return Bun.which("ormolu") !== null + return which("ormolu") !== null }, } @@ -381,7 +382,7 @@ export const cljfmt: Info = { command: ["cljfmt", "fix", "--quiet", "$FILE"], extensions: [".clj", ".cljs", ".cljc", ".edn"], async enabled() { - return Bun.which("cljfmt") !== null + return which("cljfmt") !== null }, } @@ -390,6 +391,6 @@ export const dfmt: Info = { command: ["dfmt", "-i", "$FILE"], extensions: [".d"], async enabled() { - return Bun.which("dfmt") !== null + return which("dfmt") !== null }, } diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index afd297a5ed..e09fbc97fe 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -12,6 +12,7 @@ import { Instance } from "../project/instance" import { Flag } from "../flag/flag" import { Archive } from "../util/archive" import { Process } from "../util/process" +import { which } from "../util/which" export namespace LSPServer { const log = Log.create({ service: "lsp.server" }) @@ -75,7 +76,7 @@ export namespace LSPServer { }, extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"], async spawn(root) { - const deno = Bun.which("deno") + const deno = which("deno") if (!deno) { log.info("deno not found, please install deno first") return @@ -122,7 +123,7 @@ export namespace LSPServer { extensions: [".vue"], root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), async spawn(root) { - let binary = Bun.which("vue-language-server") + let binary = which("vue-language-server") const args: string[] = [] if (!binary) { const js = path.join( @@ -260,7 +261,7 @@ export namespace LSPServer { let lintBin = await resolveBin(lintTarget) if (!lintBin) { - const found = Bun.which("oxlint") + const found = which("oxlint") if (found) lintBin = found } @@ -281,7 +282,7 @@ export namespace LSPServer { let serverBin = await resolveBin(serverTarget) if (!serverBin) { - const found = Bun.which("oxc_language_server") + const found = which("oxc_language_server") if (found) serverBin = found } if (serverBin) { @@ -332,7 +333,7 @@ export namespace LSPServer { let bin: string | undefined if (await Filesystem.exists(localBin)) bin = localBin if (!bin) { - const found = Bun.which("biome") + const found = which("biome") if (found) bin = found } @@ -368,11 +369,11 @@ export namespace LSPServer { }, extensions: [".go"], async spawn(root) { - let bin = Bun.which("gopls", { + let bin = which("gopls", { PATH: process.env["PATH"] + path.delimiter + Global.Path.bin, }) if (!bin) { - if (!Bun.which("go")) return + if (!which("go")) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return log.info("installing gopls") @@ -405,12 +406,12 @@ export namespace LSPServer { root: NearestRoot(["Gemfile"]), extensions: [".rb", ".rake", ".gemspec", ".ru"], async spawn(root) { - let bin = Bun.which("rubocop", { + let bin = which("rubocop", { PATH: process.env["PATH"] + path.delimiter + Global.Path.bin, }) if (!bin) { - const ruby = Bun.which("ruby") - const gem = Bun.which("gem") + const ruby = which("ruby") + const gem = which("gem") if (!ruby || !gem) { log.info("Ruby not found, please install Ruby first") return @@ -457,7 +458,7 @@ export namespace LSPServer { return undefined } - let binary = Bun.which("ty") + let binary = which("ty") const initialization: Record = {} @@ -509,7 +510,7 @@ export namespace LSPServer { extensions: [".py", ".pyi"], root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]), async spawn(root) { - let binary = Bun.which("pyright-langserver") + let binary = which("pyright-langserver") const args = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js") @@ -563,7 +564,7 @@ export namespace LSPServer { extensions: [".ex", ".exs"], root: NearestRoot(["mix.exs", "mix.lock"]), async spawn(root) { - let binary = Bun.which("elixir-ls") + let binary = which("elixir-ls") if (!binary) { const elixirLsPath = path.join(Global.Path.bin, "elixir-ls") binary = path.join( @@ -574,7 +575,7 @@ export namespace LSPServer { ) if (!(await Filesystem.exists(binary))) { - const elixir = Bun.which("elixir") + const elixir = which("elixir") if (!elixir) { log.error("elixir is required to run elixir-ls") return @@ -625,12 +626,12 @@ export namespace LSPServer { extensions: [".zig", ".zon"], root: NearestRoot(["build.zig"]), async spawn(root) { - let bin = Bun.which("zls", { + let bin = which("zls", { PATH: process.env["PATH"] + path.delimiter + Global.Path.bin, }) if (!bin) { - const zig = Bun.which("zig") + const zig = which("zig") if (!zig) { log.error("Zig is required to use zls. Please install Zig first.") return @@ -737,11 +738,11 @@ export namespace LSPServer { root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]), extensions: [".cs"], async spawn(root) { - let bin = Bun.which("csharp-ls", { + let bin = which("csharp-ls", { PATH: process.env["PATH"] + path.delimiter + Global.Path.bin, }) if (!bin) { - if (!Bun.which("dotnet")) { + if (!which("dotnet")) { log.error(".NET SDK is required to install csharp-ls") return } @@ -776,11 +777,11 @@ export namespace LSPServer { root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]), extensions: [".fs", ".fsi", ".fsx", ".fsscript"], async spawn(root) { - let bin = Bun.which("fsautocomplete", { + let bin = which("fsautocomplete", { PATH: process.env["PATH"] + path.delimiter + Global.Path.bin, }) if (!bin) { - if (!Bun.which("dotnet")) { + if (!which("dotnet")) { log.error(".NET SDK is required to install fsautocomplete") return } @@ -817,7 +818,7 @@ export namespace LSPServer { async spawn(root) { // Check if sourcekit-lsp is available in the PATH // This is installed with the Swift toolchain - const sourcekit = Bun.which("sourcekit-lsp") + const sourcekit = which("sourcekit-lsp") if (sourcekit) { return { process: spawn(sourcekit, { @@ -828,7 +829,7 @@ export namespace LSPServer { // If sourcekit-lsp not found, check if xcrun is available // This is specific to macOS where sourcekit-lsp is typically installed with Xcode - if (!Bun.which("xcrun")) return + if (!which("xcrun")) return const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow() @@ -877,7 +878,7 @@ export namespace LSPServer { }, extensions: [".rs"], async spawn(root) { - const bin = Bun.which("rust-analyzer") + const bin = which("rust-analyzer") if (!bin) { log.info("rust-analyzer not found in path, please install it") return @@ -896,7 +897,7 @@ export namespace LSPServer { extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"], async spawn(root) { const args = ["--background-index", "--clang-tidy"] - const fromPath = Bun.which("clangd") + const fromPath = which("clangd") if (fromPath) { return { process: spawn(fromPath, args, { @@ -1041,7 +1042,7 @@ export namespace LSPServer { extensions: [".svelte"], root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), async spawn(root) { - let binary = Bun.which("svelteserver") + let binary = which("svelteserver") const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js") @@ -1088,7 +1089,7 @@ export namespace LSPServer { } const tsdk = path.dirname(tsserver) - let binary = Bun.which("astro-ls") + let binary = which("astro-ls") const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js") @@ -1132,7 +1133,7 @@ export namespace LSPServer { root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]), extensions: [".java"], async spawn(root) { - const java = Bun.which("java") + const java = which("java") if (!java) { log.error("Java 21 or newer is required to run the JDTLS. Please install it first.") return @@ -1324,7 +1325,7 @@ export namespace LSPServer { extensions: [".yaml", ".yml"], root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), async spawn(root) { - let binary = Bun.which("yaml-language-server") + let binary = which("yaml-language-server") const args: string[] = [] if (!binary) { const js = path.join( @@ -1380,7 +1381,7 @@ export namespace LSPServer { ]), extensions: [".lua"], async spawn(root) { - let bin = Bun.which("lua-language-server", { + let bin = which("lua-language-server", { PATH: process.env["PATH"] + path.delimiter + Global.Path.bin, }) @@ -1512,7 +1513,7 @@ export namespace LSPServer { extensions: [".php"], root: NearestRoot(["composer.json", "composer.lock", ".php-version"]), async spawn(root) { - let binary = Bun.which("intelephense") + let binary = which("intelephense") const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js") @@ -1556,7 +1557,7 @@ export namespace LSPServer { extensions: [".prisma"], root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]), async spawn(root) { - const prisma = Bun.which("prisma") + const prisma = which("prisma") if (!prisma) { log.info("prisma not found, please install prisma") return @@ -1574,7 +1575,7 @@ export namespace LSPServer { extensions: [".dart"], root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]), async spawn(root) { - const dart = Bun.which("dart") + const dart = which("dart") if (!dart) { log.info("dart not found, please install dart first") return @@ -1592,7 +1593,7 @@ export namespace LSPServer { extensions: [".ml", ".mli"], root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]), async spawn(root) { - const bin = Bun.which("ocamllsp") + const bin = which("ocamllsp") if (!bin) { log.info("ocamllsp not found, please install ocaml-lsp-server") return @@ -1609,7 +1610,7 @@ export namespace LSPServer { extensions: [".sh", ".bash", ".zsh", ".ksh"], root: async () => Instance.directory, async spawn(root) { - let binary = Bun.which("bash-language-server") + let binary = which("bash-language-server") const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js") @@ -1648,7 +1649,7 @@ export namespace LSPServer { extensions: [".tf", ".tfvars"], root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]), async spawn(root) { - let bin = Bun.which("terraform-ls", { + let bin = which("terraform-ls", { PATH: process.env["PATH"] + path.delimiter + Global.Path.bin, }) @@ -1731,7 +1732,7 @@ export namespace LSPServer { extensions: [".tex", ".bib"], root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]), async spawn(root) { - let bin = Bun.which("texlab", { + let bin = which("texlab", { PATH: process.env["PATH"] + path.delimiter + Global.Path.bin, }) @@ -1821,7 +1822,7 @@ export namespace LSPServer { extensions: [".dockerfile", "Dockerfile"], root: async () => Instance.directory, async spawn(root) { - let binary = Bun.which("docker-langserver") + let binary = which("docker-langserver") const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js") @@ -1860,7 +1861,7 @@ export namespace LSPServer { extensions: [".gleam"], root: NearestRoot(["gleam.toml"]), async spawn(root) { - const gleam = Bun.which("gleam") + const gleam = which("gleam") if (!gleam) { log.info("gleam not found, please install gleam first") return @@ -1878,9 +1879,9 @@ export namespace LSPServer { extensions: [".clj", ".cljs", ".cljc", ".edn"], root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]), async spawn(root) { - let bin = Bun.which("clojure-lsp") + let bin = which("clojure-lsp") if (!bin && process.platform === "win32") { - bin = Bun.which("clojure-lsp.exe") + bin = which("clojure-lsp.exe") } if (!bin) { log.info("clojure-lsp not found, please install clojure-lsp first") @@ -1909,7 +1910,7 @@ export namespace LSPServer { return Instance.directory }, async spawn(root) { - const nixd = Bun.which("nixd") + const nixd = which("nixd") if (!nixd) { log.info("nixd not found, please install nixd first") return @@ -1930,7 +1931,7 @@ export namespace LSPServer { extensions: [".typ", ".typc"], root: NearestRoot(["typst.toml"]), async spawn(root) { - let bin = Bun.which("tinymist", { + let bin = which("tinymist", { PATH: process.env["PATH"] + path.delimiter + Global.Path.bin, }) @@ -2024,7 +2025,7 @@ export namespace LSPServer { extensions: [".hs", ".lhs"], root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]), async spawn(root) { - const bin = Bun.which("haskell-language-server-wrapper") + const bin = which("haskell-language-server-wrapper") if (!bin) { log.info("haskell-language-server-wrapper not found, please install haskell-language-server") return @@ -2042,7 +2043,7 @@ export namespace LSPServer { extensions: [".jl"], root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]), async spawn(root) { - const julia = Bun.which("julia") + const julia = which("julia") if (!julia) { log.info("julia not found, please install julia first (https://julialang.org/downloads/)") return diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index a75a0a02e7..9cc12a0a4f 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -14,6 +14,7 @@ import { GlobalBus } from "@/bus/global" import { existsSync } from "fs" import { git } from "../util/git" import { Glob } from "../util/glob" +import { which } from "../util/which" export namespace Project { const log = Log.create({ service: "project" }) @@ -97,7 +98,7 @@ export namespace Project { if (dotgit) { let sandbox = path.dirname(dotgit) - const gitBinary = Bun.which("git") + const gitBinary = which("git") // cached id calculation let id = await Filesystem.readText(path.join(dotgit, "opencode")) diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index 4779cfef75..60ae46f5ee 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -1,6 +1,7 @@ import { Flag } from "@/flag/flag" import { lazy } from "@/util/lazy" import { Filesystem } from "@/util/filesystem" +import { which } from "@/util/which" import path from "path" import { spawn, type ChildProcess } from "child_process" import { setTimeout as sleep } from "node:timers/promises" @@ -40,7 +41,7 @@ export namespace Shell { function fallback() { if (process.platform === "win32") { if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH - const git = Bun.which("git") + const git = which("git") if (git) { // git.exe is typically at: C:\Program Files\Git\cmd\git.exe // bash.exe is at: C:\Program Files\Git\bin\bash.exe @@ -50,7 +51,7 @@ export namespace Shell { return process.env.COMSPEC || "cmd.exe" } if (process.platform === "darwin") return "/bin/zsh" - const bash = Bun.which("bash") + const bash = which("bash") if (bash) return bash return "/bin/sh" } diff --git a/packages/opencode/src/util/which.ts b/packages/opencode/src/util/which.ts new file mode 100644 index 0000000000..78e651e8e8 --- /dev/null +++ b/packages/opencode/src/util/which.ts @@ -0,0 +1,10 @@ +import whichPkg from "which" + +export function which(cmd: string, env?: NodeJS.ProcessEnv) { + const result = whichPkg.sync(cmd, { + nothrow: true, + path: env?.PATH, + pathExt: env?.PATHEXT, + }) + return typeof result === "string" ? result : null +} diff --git a/packages/opencode/test/util/which.test.ts b/packages/opencode/test/util/which.test.ts new file mode 100644 index 0000000000..323173b181 --- /dev/null +++ b/packages/opencode/test/util/which.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, test } from "bun:test" +import fs from "fs/promises" +import path from "path" +import { which } from "../../src/util/which" +import { tmpdir } from "../fixture/fixture" + +async function cmd(dir: string, name: string, exec = true) { + const ext = process.platform === "win32" ? ".cmd" : "" + const file = path.join(dir, name + ext) + const body = process.platform === "win32" ? "@echo off\r\n" : "#!/bin/sh\n" + await fs.writeFile(file, body) + if (process.platform !== "win32") { + await fs.chmod(file, exec ? 0o755 : 0o644) + } + return file +} + +function env(PATH: string): NodeJS.ProcessEnv { + return { + PATH, + PATHEXT: process.env["PATHEXT"], + } +} + +function same(a: string | null, b: string) { + if (process.platform === "win32") { + expect(a?.toLowerCase()).toBe(b.toLowerCase()) + return + } + + expect(a).toBe(b) +} + +describe("util.which", () => { + test("returns null when command is missing", () => { + expect(which("opencode-missing-command-for-test")).toBeNull() + }) + + test("finds a command from PATH override", async () => { + await using tmp = await tmpdir() + const bin = path.join(tmp.path, "bin") + await fs.mkdir(bin) + const file = await cmd(bin, "tool") + + same(which("tool", env(bin)), file) + }) + + test("uses first PATH match", async () => { + await using tmp = await tmpdir() + const a = path.join(tmp.path, "a") + const b = path.join(tmp.path, "b") + await fs.mkdir(a) + await fs.mkdir(b) + const first = await cmd(a, "dupe") + await cmd(b, "dupe") + + same(which("dupe", env([a, b].join(path.delimiter))), first) + }) + + test("returns null for non-executable file on unix", async () => { + if (process.platform === "win32") return + + await using tmp = await tmpdir() + const bin = path.join(tmp.path, "bin") + await fs.mkdir(bin) + await cmd(bin, "noexec", false) + + expect(which("noexec", env(bin))).toBeNull() + }) + + test("uses PATHEXT on windows", async () => { + if (process.platform !== "win32") return + + await using tmp = await tmpdir() + const bin = path.join(tmp.path, "bin") + await fs.mkdir(bin) + const file = path.join(bin, "pathext.CMD") + await fs.writeFile(file, "@echo off\r\n") + + expect(which("pathext", { PATH: bin, PATHEXT: ".CMD" })).toBe(file) + }) +}) From c04da45be51994496f44e4fe238fbffbec89cb37 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Mar 2026 05:31:01 +0000 Subject: [PATCH 016/104] 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 47e3e240bb..326cc98a66 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-v83hWzYVg/g4zJiBpGsQ71wTdndPk3BQVZ2mjMApUIQ=", - "aarch64-linux": "sha256-inpMwkQqwBFP2wL8w/pTOP7q3fg1aOqvE0wgzVd3/B8=", - "aarch64-darwin": "sha256-r42LGrQWqDyIy62mBSU5Nf3M22dJ3NNo7mjN/1h8d8Y=", - "x86_64-darwin": "sha256-J6XrrdK5qBK3sQBQOO/B3ZluOnsAf5f65l4q/K1nDTI=" + "x86_64-linux": "sha256-pBTIT8Pgdm3272YhBjiAZsmj0SSpHTklh6lGc8YcMoE=", + "aarch64-linux": "sha256-prt039++d5UZgtldAN6+RVOR557ifIeusiy5XpzN8QU=", + "aarch64-darwin": "sha256-Y3f+cXcIGLqz6oyc5fG22t6CLD4wGkvwqO6RNXjFriQ=", + "x86_64-darwin": "sha256-BjbBBhQUgGhrlP56skABcrObvutNUZSWnrnPCg1OTKE=" } } From aec6ca71fa544ca8b5e4997f5475abc72e6e924d Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:42:08 +1000 Subject: [PATCH 017/104] fix(git): stop leaking fsmonitor daemons e.g. 60GB+ of commited memory after running tests (#16249) --- packages/app/e2e/actions.ts | 6 +- packages/opencode/src/file/index.ts | 34 ++++++---- packages/opencode/src/worktree/index.ts | 9 ++- packages/opencode/test/file/fsmonitor.test.ts | 62 +++++++++++++++++++ .../opencode/test/fixture/fixture.test.ts | 26 ++++++++ packages/opencode/test/fixture/fixture.ts | 32 +++++++++- .../test/project/worktree-remove.test.ts | 31 ++++++++++ 7 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 packages/opencode/test/file/fsmonitor.test.ts create mode 100644 packages/opencode/test/fixture/fixture.test.ts diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index a7ccba6175..fbb13008b2 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -197,6 +197,7 @@ export async function createTestProject() { await fs.writeFile(path.join(root, "README.md"), "# e2e\n") execSync("git init", { cwd: root, stdio: "ignore" }) + execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" }) execSync("git add -A", { cwd: root, stdio: "ignore" }) execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', { cwd: root, @@ -207,7 +208,10 @@ export async function createTestProject() { } export async function cleanupTestProject(directory: string) { - await fs.rm(directory, { recursive: true, force: true }).catch(() => undefined) + try { + execSync("git fsmonitor--daemon stop", { cwd: directory, stdio: "ignore" }) + } catch {} + await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined) } export function sessionIDFromUrl(url: string) { diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index b7daddc5fb..01f07c9afa 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -418,7 +418,7 @@ export namespace File { const project = Instance.project if (project.vcs !== "git") return [] - const diffOutput = await $`git -c core.quotepath=false diff --numstat HEAD` + const diffOutput = await $`git -c core.fsmonitor=false -c core.quotepath=false diff --numstat HEAD` .cwd(Instance.directory) .quiet() .nothrow() @@ -439,11 +439,12 @@ export namespace File { } } - const untrackedOutput = await $`git -c core.quotepath=false ls-files --others --exclude-standard` - .cwd(Instance.directory) - .quiet() - .nothrow() - .text() + const untrackedOutput = + await $`git -c core.fsmonitor=false -c core.quotepath=false ls-files --others --exclude-standard` + .cwd(Instance.directory) + .quiet() + .nothrow() + .text() if (untrackedOutput.trim()) { const untrackedFiles = untrackedOutput.trim().split("\n") @@ -464,11 +465,12 @@ export namespace File { } // Get deleted files - const deletedOutput = await $`git -c core.quotepath=false diff --name-only --diff-filter=D HEAD` - .cwd(Instance.directory) - .quiet() - .nothrow() - .text() + const deletedOutput = + await $`git -c core.fsmonitor=false -c core.quotepath=false diff --name-only --diff-filter=D HEAD` + .cwd(Instance.directory) + .quiet() + .nothrow() + .text() if (deletedOutput.trim()) { const deletedFiles = deletedOutput.trim().split("\n") @@ -539,8 +541,14 @@ export namespace File { const content = (await Filesystem.readText(full).catch(() => "")).trim() if (project.vcs === "git") { - let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text() - if (!diff.trim()) diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text() + let diff = await $`git -c core.fsmonitor=false diff ${file}`.cwd(Instance.directory).quiet().nothrow().text() + if (!diff.trim()) { + diff = await $`git -c core.fsmonitor=false diff --staged ${file}` + .cwd(Instance.directory) + .quiet() + .nothrow() + .text() + } if (diff.trim()) { const original = await $`git show HEAD:${file}`.cwd(Instance.directory).quiet().nothrow().text() const patch = structuredPatch(file, file, original, content, "old", "new", { diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 2267322494..15efcea386 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -474,6 +474,11 @@ export namespace Worktree { throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" }) }) + const stop = async (target: string) => { + if (!(await exists(target))) return + await $`git fsmonitor--daemon stop`.quiet().nothrow().cwd(target) + } + const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree) if (list.exitCode !== 0) { throw new RemoveFailedError({ message: errorText(list) || "Failed to read git worktrees" }) @@ -484,11 +489,13 @@ export namespace Worktree { if (!entry?.path) { const directoryExists = await exists(directory) if (directoryExists) { + await stop(directory) await clean(directory) } return true } + await stop(entry.path) const removed = await $`git worktree remove --force ${entry.path}`.quiet().nothrow().cwd(Instance.worktree) if (removed.exitCode !== 0) { const next = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree) @@ -637,7 +644,7 @@ export namespace Worktree { throw new ResetFailedError({ message: errorText(subClean) || "Failed to clean submodules" }) } - const status = await $`git status --porcelain=v1`.quiet().nothrow().cwd(worktreePath) + const status = await $`git -c core.fsmonitor=false status --porcelain=v1`.quiet().nothrow().cwd(worktreePath) if (status.exitCode !== 0) { throw new ResetFailedError({ message: errorText(status) || "Failed to read git status" }) } diff --git a/packages/opencode/test/file/fsmonitor.test.ts b/packages/opencode/test/file/fsmonitor.test.ts new file mode 100644 index 0000000000..8cdde014db --- /dev/null +++ b/packages/opencode/test/file/fsmonitor.test.ts @@ -0,0 +1,62 @@ +import { $ } from "bun" +import { describe, expect, test } from "bun:test" +import fs from "fs/promises" +import path from "path" +import { File } from "../../src/file" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" + +const wintest = process.platform === "win32" ? test : test.skip + +describe("file fsmonitor", () => { + wintest("status does not start fsmonitor for readonly git checks", async () => { + await using tmp = await tmpdir({ git: true }) + const target = path.join(tmp.path, "tracked.txt") + + await fs.writeFile(target, "base\n") + await $`git add tracked.txt`.cwd(tmp.path).quiet() + await $`git commit -m init`.cwd(tmp.path).quiet() + await $`git config core.fsmonitor true`.cwd(tmp.path).quiet() + await $`git fsmonitor--daemon stop`.cwd(tmp.path).quiet().nothrow() + await fs.writeFile(target, "next\n") + await fs.writeFile(path.join(tmp.path, "new.txt"), "new\n") + + const before = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow() + expect(before.exitCode).not.toBe(0) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await File.status() + }, + }) + + const after = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow() + expect(after.exitCode).not.toBe(0) + }) + + wintest("read does not start fsmonitor for git diffs", async () => { + await using tmp = await tmpdir({ git: true }) + const target = path.join(tmp.path, "tracked.txt") + + await fs.writeFile(target, "base\n") + await $`git add tracked.txt`.cwd(tmp.path).quiet() + await $`git commit -m init`.cwd(tmp.path).quiet() + await $`git config core.fsmonitor true`.cwd(tmp.path).quiet() + await $`git fsmonitor--daemon stop`.cwd(tmp.path).quiet().nothrow() + await fs.writeFile(target, "next\n") + + const before = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow() + expect(before.exitCode).not.toBe(0) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await File.read("tracked.txt") + }, + }) + + const after = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow() + expect(after.exitCode).not.toBe(0) + }) +}) diff --git a/packages/opencode/test/fixture/fixture.test.ts b/packages/opencode/test/fixture/fixture.test.ts new file mode 100644 index 0000000000..153276a283 --- /dev/null +++ b/packages/opencode/test/fixture/fixture.test.ts @@ -0,0 +1,26 @@ +import { $ } from "bun" +import { describe, expect, test } from "bun:test" +import fs from "fs/promises" +import { tmpdir } from "./fixture" + +describe("tmpdir", () => { + test("disables fsmonitor for git fixtures", async () => { + await using tmp = await tmpdir({ git: true }) + + const value = (await $`git config core.fsmonitor`.cwd(tmp.path).quiet().text()).trim() + expect(value).toBe("false") + }) + + test("removes directories on dispose", async () => { + const tmp = await tmpdir({ git: true }) + const dir = tmp.path + + await tmp[Symbol.asyncDispose]() + + const exists = await fs + .stat(dir) + .then(() => true) + .catch(() => false) + expect(exists).toBe(false) + }) +}) diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index ed8c5e344a..63f93bcafe 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -9,6 +9,27 @@ function sanitizePath(p: string): string { return p.replace(/\0/g, "") } +function exists(dir: string) { + return fs + .stat(dir) + .then(() => true) + .catch(() => false) +} + +function clean(dir: string) { + return fs.rm(dir, { + recursive: true, + force: true, + maxRetries: 5, + retryDelay: 100, + }) +} + +async function stop(dir: string) { + if (!(await exists(dir))) return + await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow() +} + type TmpDirOptions = { git?: boolean config?: Partial @@ -20,6 +41,7 @@ export async function tmpdir(options?: TmpDirOptions) { await fs.mkdir(dirpath, { recursive: true }) if (options?.git) { await $`git init`.cwd(dirpath).quiet() + await $`git config core.fsmonitor false`.cwd(dirpath).quiet() await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet() } if (options?.config) { @@ -31,12 +53,16 @@ export async function tmpdir(options?: TmpDirOptions) { }), ) } - const extra = await options?.init?.(dirpath) const realpath = sanitizePath(await fs.realpath(dirpath)) + const extra = await options?.init?.(realpath) const result = { [Symbol.asyncDispose]: async () => { - await options?.dispose?.(dirpath) - // await fs.rm(dirpath, { recursive: true, force: true }) + try { + await options?.dispose?.(realpath) + } finally { + if (options?.git) await stop(realpath).catch(() => undefined) + await clean(realpath).catch(() => undefined) + } }, path: realpath, extra: extra as T, diff --git a/packages/opencode/test/project/worktree-remove.test.ts b/packages/opencode/test/project/worktree-remove.test.ts index e17a5392bc..a6b5bb7c34 100644 --- a/packages/opencode/test/project/worktree-remove.test.ts +++ b/packages/opencode/test/project/worktree-remove.test.ts @@ -7,6 +7,8 @@ import { Worktree } from "../../src/worktree" import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" +const wintest = process.platform === "win32" ? test : test.skip + describe("Worktree.remove", () => { test("continues when git remove exits non-zero after detaching", async () => { await using tmp = await tmpdir({ git: true }) @@ -62,4 +64,33 @@ describe("Worktree.remove", () => { const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow() expect(ref.exitCode).not.toBe(0) }) + + wintest("stops fsmonitor before removing a worktree", async () => { + await using tmp = await tmpdir({ git: true }) + const root = tmp.path + const name = `remove-fsmonitor-${Date.now().toString(36)}` + const branch = `opencode/${name}` + const dir = path.join(root, "..", name) + + await $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet() + await $`git reset --hard`.cwd(dir).quiet() + await $`git config core.fsmonitor true`.cwd(dir).quiet() + await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow() + await Bun.write(path.join(dir, "tracked.txt"), "next\n") + await $`git diff`.cwd(dir).quiet() + + const before = await $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow() + expect(before.exitCode).toBe(0) + + const ok = await Instance.provide({ + directory: root, + fn: () => Worktree.remove({ directory: dir }), + }) + + expect(ok).toBe(true) + expect(await Filesystem.exists(dir)).toBe(false) + + const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow() + expect(ref.exitCode).not.toBe(0) + }) }) From 326c70184da31bfb19fec936ec07a27bea461bbe Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 6 Mar 2026 02:08:10 -0500 Subject: [PATCH 018/104] fix: restore Bun stdin reads for prompt input (#16300) --- packages/opencode/src/cli/cmd/run.ts | 3 +-- packages/opencode/src/cli/cmd/tui/thread.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index d74eb2aa4c..61bc609bb7 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -6,7 +6,6 @@ import { cmd } from "./cmd" import { Flag } from "../../flag/flag" import { bootstrap } from "../bootstrap" import { EOL } from "os" -import { text as streamText } from "node:stream/consumers" import { Filesystem } from "../../util/filesystem" import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" @@ -338,7 +337,7 @@ export const RunCommand = cmd({ } } - if (!process.stdin.isTTY) message += "\n" + (await streamText(process.stdin)) + if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text()) if (message.trim().length === 0 && !args.command) { UI.error("You must provide a message or a command") diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 57acfd199e..f53cc39255 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -3,7 +3,6 @@ import { tui } from "./app" import { Rpc } from "@/util/rpc" import { type rpc } from "./worker" import path from "path" -import { text as streamText } from "node:stream/consumers" import { fileURLToPath } from "url" import { UI } from "@/cli/ui" import { Log } from "@/util/log" @@ -54,7 +53,7 @@ async function target() { } async function input(value?: string) { - const piped = process.stdin.isTTY ? undefined : await streamText(process.stdin) + const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text() if (!value) return piped if (!piped) return value return piped + "\n" + value From 6c7d968c4423a0cd6c85099c9377a6066313fa0a Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 6 Mar 2026 07:29:34 +0000 Subject: [PATCH 019/104] release: v1.2.20 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 6539faa830..fcc0fcbbb0 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -76,7 +76,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -110,7 +110,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -137,7 +137,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -161,7 +161,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -185,7 +185,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -218,7 +218,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -248,7 +248,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -277,7 +277,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -293,7 +293,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.19", + "version": "1.2.20", "bin": { "opencode": "./bin/opencode", }, @@ -409,7 +409,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -429,7 +429,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.19", + "version": "1.2.20", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -440,7 +440,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -475,7 +475,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -521,7 +521,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "zod": "catalog:", }, @@ -532,7 +532,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 809a285374..c91a91383d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.19", + "version": "1.2.20", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 269b005a86..4d20c1b8bc 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.19", + "version": "1.2.20", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 199b5d9bd6..408e2a7aca 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.19", + "version": "1.2.20", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index e771aae844..8df6594d0b 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.19", + "version": "1.2.20", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index ef8d7c5994..591c4bd369 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index e683d185bc..31321c92a5 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.2.19", + "version": "1.2.20", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 10e6df26b1..da4d51bcc7 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.19", + "version": "1.2.20", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index ef56add880..2531cf34fb 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.19", + "version": "1.2.20", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 1637c58a4d..d45bec2301 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.19" +version = "1.2.20" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.20/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.20/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.20/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.20/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.20/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index efaae65f53..adc3bfcb05 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.19", + "version": "1.2.20", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index fde3ace92f..f502382ba7 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.19", + "version": "1.2.20", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d8911922ec..7419f4ecca 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.19", + "version": "1.2.20", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index fe3ce393d6..ced7f6c05f 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.19", + "version": "1.2.20", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index d37e140581..fbdf202fdf 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.19", + "version": "1.2.20", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 48c668c6fb..db585bb008 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.19", + "version": "1.2.20", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index bc84be6360..b6ee1c3bf2 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.19", + "version": "1.2.20", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index c85ba1a0b9..e386f581f5 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.19", + "version": "1.2.20", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 215143ac99..f900f1099c 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.19", + "version": "1.2.20", "publisher": "sst-dev", "repository": { "type": "git", From b7605add5803becb0a1abd9fb5110cb636cc6d01 Mon Sep 17 00:00:00 2001 From: Luis Felipe Cordeiro Sena <61807189+luisfelipesena@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:49:12 -0300 Subject: [PATCH 020/104] fix(app): enable auto-accept keybind regardless of permission config (#16259) --- packages/app/src/components/prompt-input.tsx | 12 +--- .../context/permission-auto-respond.test.ts | 41 ++++++++++- .../src/context/permission-auto-respond.ts | 12 +++- packages/app/src/context/permission.tsx | 70 ++++++++++++++++++- .../pages/session/use-session-commands.tsx | 29 +++++--- packages/opencode/script/build.ts | 6 +- 6 files changed, 145 insertions(+), 25 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index c9c8bc6b44..40104bceb9 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -244,7 +244,6 @@ export const PromptInput: Component = (props) => { draggingType: "image" | "@mention" | null mode: "normal" | "shell" applyingHistory: boolean - pendingAutoAccept: boolean }>({ popover: null, historyIndex: -1, @@ -253,7 +252,6 @@ export const PromptInput: Component = (props) => { draggingType: null, mode: "normal", applyingHistory: false, - pendingAutoAccept: false, }) const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) @@ -306,12 +304,6 @@ export const PromptInput: Component = (props) => { }), ) - createEffect( - on(sessionKey, () => { - setStore("pendingAutoAccept", false) - }), - ) - const historyComments = () => { const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const)) return prompt.context.items().flatMap((item) => { @@ -961,7 +953,7 @@ export const PromptInput: Component = (props) => { const variants = createMemo(() => ["default", ...local.model.variant.list()]) const accepting = createMemo(() => { const id = params.id - if (!id) return store.pendingAutoAccept + if (!id) return permission.isAutoAcceptingDirectory(sdk.directory) return permission.isAutoAccepting(id, sdk.directory) }) @@ -1336,7 +1328,7 @@ export const PromptInput: Component = (props) => { variant="ghost" onClick={() => { if (!params.id) { - setStore("pendingAutoAccept", (value) => !value) + permission.toggleAutoAcceptDirectory(sdk.directory) return } permission.toggleAutoAccept(params.id, sdk.directory) diff --git a/packages/app/src/context/permission-auto-respond.test.ts b/packages/app/src/context/permission-auto-respond.test.ts index 2e4cf4fafb..7556113005 100644 --- a/packages/app/src/context/permission-auto-respond.test.ts +++ b/packages/app/src/context/permission-auto-respond.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client" import { base64Encode } from "@opencode-ai/util/encode" -import { autoRespondsPermission } from "./permission-auto-respond" +import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond" const session = (input: { id: string; parentID?: string }) => ({ @@ -60,4 +60,43 @@ describe("autoRespondsPermission", () => { expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true) }) + + test("falls back to directory-level auto-accept", () => { + const directory = "/tmp/project" + const sessions = [session({ id: "root" })] + const autoAccept = { + [`${base64Encode(directory)}/*`]: true, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(true) + }) + + test("session-level override takes precedence over directory-level", () => { + const directory = "/tmp/project" + const sessions = [session({ id: "root" })] + const autoAccept = { + [`${base64Encode(directory)}/*`]: true, + [`${base64Encode(directory)}/root`]: false, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(false) + }) +}) + +describe("isDirectoryAutoAccepting", () => { + test("returns true when directory key is set", () => { + const directory = "/tmp/project" + const autoAccept = { [`${base64Encode(directory)}/*`]: true } + expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(true) + }) + + test("returns false when directory key is not set", () => { + expect(isDirectoryAutoAccepting({}, "/tmp/project")).toBe(false) + }) + + test("returns false when directory key is explicitly false", () => { + const directory = "/tmp/project" + const autoAccept = { [`${base64Encode(directory)}/*`]: false } + expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false) + }) }) diff --git a/packages/app/src/context/permission-auto-respond.ts b/packages/app/src/context/permission-auto-respond.ts index 727ccc9375..b206deedff 100644 --- a/packages/app/src/context/permission-auto-respond.ts +++ b/packages/app/src/context/permission-auto-respond.ts @@ -5,9 +5,19 @@ export function acceptKey(sessionID: string, directory?: string) { return `${base64Encode(directory)}/${sessionID}` } +export function directoryAcceptKey(directory: string) { + return `${base64Encode(directory)}/*` +} + function accepted(autoAccept: Record, sessionID: string, directory?: string) { const key = acceptKey(sessionID, directory) - return autoAccept[key] ?? autoAccept[sessionID] + const directoryKey = directory ? directoryAcceptKey(directory) : undefined + return autoAccept[key] ?? autoAccept[sessionID] ?? (directoryKey ? autoAccept[directoryKey] : undefined) +} + +export function isDirectoryAutoAccepting(autoAccept: Record, directory: string) { + const key = directoryAcceptKey(directory) + return autoAccept[key] ?? false } function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) { diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx index 73ee08c9ac..8208e46b94 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -1,4 +1,4 @@ -import { createMemo, onCleanup } from "solid-js" +import { createEffect, createMemo, onCleanup } from "solid-js" import { createStore, produce } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import type { PermissionRequest } from "@opencode-ai/sdk/v2/client" @@ -7,7 +7,7 @@ import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "./global-sync" import { useParams } from "@solidjs/router" import { decode64 } from "@/utils/base64" -import { acceptKey, autoRespondsPermission } from "./permission-auto-respond" +import { acceptKey, directoryAcceptKey, isDirectoryAutoAccepting, autoRespondsPermission } from "./permission-auto-respond" type PermissionRespondFn = (input: { sessionID: string @@ -76,6 +76,25 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple }), ) + // When config has permission: "allow", auto-enable directory-level auto-accept + createEffect(() => { + if (!ready()) return + const directory = decode64(params.dir) + if (!directory) return + const [childStore] = globalSync.child(directory) + const perm = childStore.config.permission + if (typeof perm === "string" && perm === "allow") { + const key = directoryAcceptKey(directory) + if (store.autoAccept[key] === undefined) { + setStore( + produce((draft) => { + draft.autoAccept[key] = true + }), + ) + } + } + }) + const MAX_RESPONDED = 1000 const RESPONDED_TTL_MS = 60 * 60 * 1000 const responded = new Map() @@ -119,6 +138,10 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory) } + function isAutoAcceptingDirectory(directory: string) { + return isDirectoryAutoAccepting(store.autoAccept, directory) + } + function shouldAutoRespond(permission: PermissionRequest, directory?: string) { const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : [] return autoRespondsPermission(store.autoAccept, session, permission, directory) @@ -142,6 +165,36 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple }) onCleanup(unsubscribe) + function enableDirectory(directory: string) { + const key = directoryAcceptKey(directory) + setStore( + produce((draft) => { + draft.autoAccept[key] = true + }), + ) + + globalSDK.client.permission + .list({ directory }) + .then((x) => { + if (!isAutoAcceptingDirectory(directory)) return + for (const perm of x.data ?? []) { + if (!perm?.id) continue + if (!shouldAutoRespond(perm, directory)) continue + respondOnce(perm, directory) + } + }) + .catch(() => undefined) + } + + function disableDirectory(directory: string) { + const key = directoryAcceptKey(directory) + setStore( + produce((draft) => { + draft.autoAccept[key] = false + }), + ) + } + function enable(sessionID: string, directory: string) { const key = acceptKey(sessionID, directory) const version = bumpEnableVersion(sessionID, directory) @@ -185,6 +238,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple return shouldAutoRespond(permission, directory) }, isAutoAccepting, + isAutoAcceptingDirectory, toggleAutoAccept(sessionID: string, directory: string) { if (isAutoAccepting(sessionID, directory)) { disable(sessionID, directory) @@ -193,6 +247,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple enable(sessionID, directory) }, + toggleAutoAcceptDirectory(directory: string) { + if (isAutoAcceptingDirectory(directory)) { + disableDirectory(directory) + return + } + enableDirectory(directory) + }, enableAutoAccept(sessionID: string, directory: string) { if (isAutoAccepting(sessionID, directory)) return enable(sessionID, directory) @@ -201,6 +262,11 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple disable(sessionID, directory) }, permissionsEnabled, + isPermissionAllowAll(directory: string) { + const [childStore] = globalSync.child(directory) + const perm = childStore.config.permission + return typeof perm === "string" && perm === "allow" + }, } }, }) diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 461351878b..b8ddeda823 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -261,24 +261,35 @@ export const useSessionCommands = (actions: SessionCommandContext) => { }), ]) + const isAutoAcceptActive = () => { + const sessionID = params.id + if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory) + return permission.isAutoAcceptingDirectory(sdk.directory) + } + const permissionCommands = createMemo(() => [ permissionsCommand({ id: "permissions.autoaccept", - title: - params.id && permission.isAutoAccepting(params.id, sdk.directory) - ? language.t("command.permissions.autoaccept.disable") - : language.t("command.permissions.autoaccept.enable"), + title: isAutoAcceptActive() + ? language.t("command.permissions.autoaccept.disable") + : language.t("command.permissions.autoaccept.enable"), keybind: "mod+shift+a", - disabled: !params.id || !permission.permissionsEnabled(), + disabled: false, onSelect: () => { const sessionID = params.id - if (!sessionID) return - permission.toggleAutoAccept(sessionID, sdk.directory) + if (sessionID) { + permission.toggleAutoAccept(sessionID, sdk.directory) + } else { + permission.toggleAutoAcceptDirectory(sdk.directory) + } + const active = sessionID + ? permission.isAutoAccepting(sessionID, sdk.directory) + : permission.isAutoAcceptingDirectory(sdk.directory) showToast({ - title: permission.isAutoAccepting(sessionID, sdk.directory) + title: active ? language.t("toast.permissions.autoaccept.on.title") : language.t("toast.permissions.autoaccept.off.title"), - description: permission.isAutoAccepting(sessionID, sdk.directory) + description: active ? language.t("toast.permissions.autoaccept.on.description") : language.t("toast.permissions.autoaccept.off.description"), }) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 34e80d71a0..b5d6e8adfb 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -4,7 +4,7 @@ import { $ } from "bun" import fs from "fs" import path from "path" import { fileURLToPath } from "url" -import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin" +import solidPlugin from "@opentui/solid/bun-plugin" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -161,7 +161,9 @@ for (const item of targets) { console.log(`building ${name}`) await $`mkdir -p dist/${name}/bin` - const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js")) + const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js") + const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js") + const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath) const workerPath = "./src/cli/cmd/tui/worker.ts" // Use platform-specific bunfs root path based on target OS From d4d1292a0e7c80b3f24d76e7d3bc71aa235967f0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Mar 2026 10:50:00 +0000 Subject: [PATCH 021/104] chore: generate --- packages/app/src/context/permission.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx index 8208e46b94..672f84f82a 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -7,7 +7,12 @@ import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "./global-sync" import { useParams } from "@solidjs/router" import { decode64 } from "@/utils/base64" -import { acceptKey, directoryAcceptKey, isDirectoryAutoAccepting, autoRespondsPermission } from "./permission-auto-respond" +import { + acceptKey, + directoryAcceptKey, + isDirectoryAutoAccepting, + autoRespondsPermission, +} from "./permission-auto-respond" type PermissionRespondFn = (input: { sessionID: string From eb9eb5e3349a76489f4779aefd78cffd3a42d262 Mon Sep 17 00:00:00 2001 From: D <47968994+imdzx@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:46:53 +0700 Subject: [PATCH 022/104] =?UTF-8?q?feat:=20Add=20Vietnamese=20README=20and?= =?UTF-8?q?=20update=20all=20language=20navigation=20links=20=E2=80=A6=20(?= =?UTF-8?q?#16322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.ar.md | 3 +- README.bn.md | 3 +- README.br.md | 4 +- README.bs.md | 3 +- README.da.md | 4 +- README.de.md | 4 +- README.es.md | 4 +- README.fr.md | 4 +- README.gr.md | 3 +- README.it.md | 4 +- README.ja.md | 4 +- README.ko.md | 4 +- README.md | 3 +- README.no.md | 4 +- README.pl.md | 4 +- README.ru.md | 4 +- README.th.md | 4 +- README.tr.md | 4 +- README.uk.md | 3 +- README.vi.md | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.zh.md | 4 +- README.zht.md | 4 +- 22 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 README.vi.md diff --git a/README.ar.md b/README.ar.md index 865fecb22b..beb44589e6 100644 --- a/README.ar.md +++ b/README.ar.md @@ -35,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.bn.md b/README.bn.md index 24c083e79e..c7abc7346a 100644 --- a/README.bn.md +++ b/README.bn.md @@ -35,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.br.md b/README.br.md index f7e82fa09d..6d1de21562 100644 --- a/README.br.md +++ b/README.br.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.bs.md b/README.bs.md index 5bba870859..2cff8e0279 100644 --- a/README.bs.md +++ b/README.bs.md @@ -35,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.da.md b/README.da.md index d1e686d7d7..ac522f29c4 100644 --- a/README.da.md +++ b/README.da.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.de.md b/README.de.md index 7a3572324a..87a670f3fc 100644 --- a/README.de.md +++ b/README.de.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.es.md b/README.es.md index b454182328..9e456af1c0 100644 --- a/README.es.md +++ b/README.es.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.fr.md b/README.fr.md index 02e66e5e87..c1fca23376 100644 --- a/README.fr.md +++ b/README.fr.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.gr.md b/README.gr.md index 976eab5cc3..2b2c2679d8 100644 --- a/README.gr.md +++ b/README.gr.md @@ -35,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.it.md b/README.it.md index b0d7247415..3e516a9027 100644 --- a/README.it.md +++ b/README.it.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ja.md b/README.ja.md index e381fbc603..144dc7b6f8 100644 --- a/README.ja.md +++ b/README.ja.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ko.md b/README.ko.md index 63b9fb4091..32defc0a5e 100644 --- a/README.ko.md +++ b/README.ko.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.md b/README.md index 8d92450374..79ccf8b349 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.no.md b/README.no.md index 1ccefaa760..c3348286b2 100644 --- a/README.no.md +++ b/README.no.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.pl.md b/README.pl.md index 0b246d5d5a..4c5a076656 100644 --- a/README.pl.md +++ b/README.pl.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ru.md b/README.ru.md index ff30d380fd..e507be70e6 100644 --- a/README.ru.md +++ b/README.ru.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.th.md b/README.th.md index 6a9a956a88..4a4ea62c95 100644 --- a/README.th.md +++ b/README.th.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.tr.md b/README.tr.md index 9deedfb3c6..e88b40f875 100644 --- a/README.tr.md +++ b/README.tr.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.uk.md b/README.uk.md index dfd8fa8d75..a1a0259b6d 100644 --- a/README.uk.md +++ b/README.uk.md @@ -35,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.vi.md b/README.vi.md new file mode 100644 index 0000000000..d54d4ab417 --- /dev/null +++ b/README.vi.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

Trợ lý lập trình AI mã nguồn mở.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Cài đặt + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Các trình quản lý gói (Package managers) +npm i -g opencode-ai@latest # hoặc bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS và Linux (khuyên dùng, luôn cập nhật) +brew install opencode # macOS và Linux (công thức brew chính thức, ít cập nhật hơn) +sudo pacman -S opencode # Arch Linux (Bản ổn định) +paru -S opencode-bin # Arch Linux (Bản mới nhất từ AUR) +mise use -g opencode # Mọi hệ điều hành +nix run nixpkgs#opencode # hoặc github:anomalyco/opencode cho nhánh dev mới nhất +``` + +> [!TIP] +> Hãy xóa các phiên bản cũ hơn 0.1.x trước khi cài đặt. + +### Ứng dụng Desktop (BETA) + +OpenCode cũng có sẵn dưới dạng ứng dụng desktop. Tải trực tiếp từ [trang releases](https://github.com/anomalyco/opencode/releases) hoặc [opencode.ai/download](https://opencode.ai/download). + +| Nền tảng | Tải xuống | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, hoặc AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Thư mục cài đặt + +Tập lệnh cài đặt tuân theo thứ tự ưu tiên sau cho đường dẫn cài đặt: + +1. `$OPENCODE_INSTALL_DIR` - Thư mục cài đặt tùy chỉnh +2. `$XDG_BIN_DIR` - Đường dẫn tuân thủ XDG Base Directory Specification +3. `$HOME/bin` - Thư mục nhị phân tiêu chuẩn của người dùng (nếu tồn tại hoặc có thể tạo) +4. `$HOME/.opencode/bin` - Mặc định dự phòng + +```bash +# Ví dụ +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents (Đại diện) + +OpenCode bao gồm hai agent được tích hợp sẵn mà bạn có thể chuyển đổi bằng phím `Tab`. + +- **build** - Agent mặc định, có toàn quyền truy cập cho công việc lập trình +- **plan** - Agent chỉ đọc dùng để phân tích và khám phá mã nguồn + - Mặc định từ chối việc chỉnh sửa tệp + - Hỏi quyền trước khi chạy các lệnh bash + - Lý tưởng để khám phá các codebase lạ hoặc lên kế hoạch thay đổi + +Ngoài ra còn có một subagent **general** dùng cho các tìm kiếm phức tạp và tác vụ nhiều bước. +Agent này được sử dụng nội bộ và có thể gọi bằng cách dùng `@general` trong tin nhắn. + +Tìm hiểu thêm về [agents](https://opencode.ai/docs/agents). + +### Tài liệu + +Để biết thêm thông tin về cách cấu hình OpenCode, [**hãy truy cập tài liệu của chúng tôi**](https://opencode.ai/docs). + +### Đóng góp + +Nếu bạn muốn đóng góp cho OpenCode, vui lòng đọc [tài liệu hướng dẫn đóng góp](./CONTRIBUTING.md) trước khi gửi pull request. + +### Xây dựng trên nền tảng OpenCode + +Nếu bạn đang làm việc trên một dự án liên quan đến OpenCode và sử dụng "opencode" như một phần của tên dự án, ví dụ "opencode-dashboard" hoặc "opencode-mobile", vui lòng thêm một ghi chú vào README của bạn để làm rõ rằng dự án đó không được xây dựng bởi đội ngũ OpenCode và không liên kết với chúng tôi dưới bất kỳ hình thức nào. + +### Các câu hỏi thường gặp (FAQ) + +#### OpenCode khác biệt thế nào so với Claude Code? + +Về mặt tính năng, nó rất giống Claude Code. Dưới đây là những điểm khác biệt chính: + +- 100% mã nguồn mở +- Không bị ràng buộc với bất kỳ nhà cung cấp nào. Mặc dù chúng tôi khuyên dùng các mô hình được cung cấp qua [OpenCode Zen](https://opencode.ai/zen), OpenCode có thể được sử dụng với Claude, OpenAI, Google, hoặc thậm chí các mô hình chạy cục bộ. Khi các mô hình phát triển, khoảng cách giữa chúng sẽ thu hẹp lại và giá cả sẽ giảm, vì vậy việc không phụ thuộc vào nhà cung cấp là rất quan trọng. +- Hỗ trợ LSP ngay từ đầu +- Tập trung vào TUI (Giao diện người dùng dòng lệnh). OpenCode được xây dựng bởi những người dùng neovim và đội ngũ tạo ra [terminal.shop](https://terminal.shop); chúng tôi sẽ đẩy giới hạn của những gì có thể làm được trên terminal lên mức tối đa. +- Kiến trúc client/server. Chẳng hạn, điều này cho phép OpenCode chạy trên máy tính của bạn trong khi bạn điều khiển nó từ xa qua một ứng dụng di động, nghĩa là frontend TUI chỉ là một trong những client có thể dùng. + +--- + +**Tham gia cộng đồng của chúng tôi** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.zh.md b/README.zh.md index 9a1e1b2fb6..b11d9857c9 100644 --- a/README.zh.md +++ b/README.zh.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.zht.md b/README.zht.md index 238f11289f..573ca85ab4 100644 --- a/README.zht.md +++ b/README.zht.md @@ -27,6 +27,7 @@ 日本語 | Polski | Русский | + Bosanski | العربية | Norsk | Português (Brasil) | @@ -34,7 +35,8 @@ Türkçe | Українська | বাংলা | - Ελληνικά + Ελληνικά | + Tiếng Việt

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) From f64bb91257c6381e6c2fbd099ca36ef726c46704 Mon Sep 17 00:00:00 2001 From: Kirill Tregubov Date: Fri, 6 Mar 2026 06:47:39 -0500 Subject: [PATCH 023/104] fix(app): add english to locale matchers (#16280) --- packages/app/src/context/language.tsx | 2 ++ packages/desktop-electron/src/renderer/i18n/index.ts | 1 + packages/desktop/src/i18n/index.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx index be1a1769bf..b1edd541c3 100644 --- a/packages/app/src/context/language.tsx +++ b/packages/app/src/context/language.tsx @@ -146,6 +146,7 @@ const DICT: Record = { } const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [ + { locale: "en", match: (language) => language.startsWith("en") }, { locale: "zht", match: (language) => language.startsWith("zh") && language.includes("hant") }, { locale: "zh", match: (language) => language.startsWith("zh") }, { locale: "ko", match: (language) => language.startsWith("ko") }, @@ -217,6 +218,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont ) const locale = createMemo(() => normalizeLocale(store.locale)) + console.log("locale", locale()) const intl = createMemo(() => INTL[locale()]) const dict = createMemo(() => DICT[locale()]) diff --git a/packages/desktop-electron/src/renderer/i18n/index.ts b/packages/desktop-electron/src/renderer/i18n/index.ts index 81158ad244..be87f94f91 100644 --- a/packages/desktop-electron/src/renderer/i18n/index.ts +++ b/packages/desktop-electron/src/renderer/i18n/index.ts @@ -76,6 +76,7 @@ function detectLocale(): Locale { const languages = navigator.languages?.length ? navigator.languages : [navigator.language] for (const language of languages) { if (!language) continue + if (language.toLowerCase().startsWith("en")) return "en" if (language.toLowerCase().startsWith("zh")) { if (language.toLowerCase().includes("hant")) return "zht" return "zh" diff --git a/packages/desktop/src/i18n/index.ts b/packages/desktop/src/i18n/index.ts index 7b1ebfe696..e1c1e63d97 100644 --- a/packages/desktop/src/i18n/index.ts +++ b/packages/desktop/src/i18n/index.ts @@ -77,6 +77,7 @@ function detectLocale(): Locale { const languages = navigator.languages?.length ? navigator.languages : [navigator.language] for (const language of languages) { if (!language) continue + if (language.toLowerCase().startsWith("en")) return "en" if (language.toLowerCase().startsWith("zh")) { if (language.toLowerCase().includes("hant")) return "zht" return "zh" From e1cf761d298dd704814312657a25b4130288644d Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Mar 2026 11:48:31 +0000 Subject: [PATCH 024/104] chore: generate --- README.vi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.vi.md b/README.vi.md index d54d4ab417..0932c50f78 100644 --- a/README.vi.md +++ b/README.vi.md @@ -68,7 +68,7 @@ nix run nixpkgs#opencode # hoặc github:anomalyco/opencode cho nhánh OpenCode cũng có sẵn dưới dạng ứng dụng desktop. Tải trực tiếp từ [trang releases](https://github.com/anomalyco/opencode/releases) hoặc [opencode.ai/download](https://opencode.ai/download). -| Nền tảng | Tải xuống | +| Nền tảng | Tải xuống | | --------------------- | ------------------------------------- | | macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | | macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | From 2094e8b255fb5406c7c2af5ce0ae8673c43b5c0b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:04:39 -0600 Subject: [PATCH 025/104] Revert "fix(app): stale keyed show errors" This reverts commit 7665b8e30def396059516ef6d870830baa1faedd. --- .../app/src/pages/session/message-timeline.tsx | 6 +++--- .../app/src/pages/session/session-side-panel.tsx | 4 +--- .../app/src/pages/session/terminal-panel.tsx | 16 ++++++++-------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index f320a2ebbf..ce6a01378c 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -611,7 +611,7 @@ export function MessageTimeline(props: {
- + {(id) => (
@@ -646,12 +646,12 @@ export function MessageTimeline(props: { > {language.t("common.rename")} - void archiveSession(id)}> + void archiveSession(id())}> {language.t("common.archive")} dialog.show(() => )} + onSelect={() => dialog.show(() => )} > {language.t("common.delete")} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 55c1607a09..ad802d15d1 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -331,9 +331,7 @@ export function SessionSidePanel(props: { const path = createMemo(() => file.pathFromTab(tab)) return (
- - {(p) => } - + {(p) => }
) }} diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index c8bfc14053..cc4c17ee21 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -191,8 +191,8 @@ export function TerminalPanel() { {(id) => ( - - {(pty) => } + + {(pty) => } )} @@ -217,10 +217,10 @@ export function TerminalPanel() {
{(id) => ( - + {(pty) => (
- terminal.clone(id)} /> + terminal.clone(id)} />
)}
@@ -229,14 +229,14 @@ export function TerminalPanel() {
- + {(draggedId) => ( - + {(t) => (
{terminalTabLabel({ - title: t.title, - titleNumber: t.titleNumber, + title: t().title, + titleNumber: t().titleNumber, t: language.t as (key: string, vars?: Record) => string, })}
From eeeb21ff8638eddd960afbd8f522c87d850d6183 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:08:49 -0600 Subject: [PATCH 026/104] Revert "fix(app): stale read error" This reverts commit 152df2428df84a1029be2b472b4a840cd94c966d. --- packages/app/src/pages/layout.tsx | 47 ++++++++++++++----------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index bd0315efbf..f6165461b1 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -10,9 +10,8 @@ import { ParentProps, Show, untrack, - type JSX, } from "solid-js" -import { A, useNavigate, useParams } from "@solidjs/router" +import { useNavigate, useParams } from "@solidjs/router" import { useLayout, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { Persist, persisted } from "@/utils/persist" @@ -20,7 +19,6 @@ import { base64Encode } from "@opencode-ai/util/encode" import { decode64 } from "@/utils/base64" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" @@ -59,7 +57,6 @@ import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" import { useLanguage, type Locale } from "@/context/language" import { - childMapByParent, displayName, effectiveWorkspaceOrder, errorMessage, @@ -1846,7 +1843,7 @@ export default function Layout(props: ParentProps) { }} style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }} > - + {(p) => ( <>
@@ -1855,7 +1852,7 @@ export default function Layout(props: ParentProps) { renameProject(p, next)} + onSave={(next) => renameProject(p(), next)} class="text-14-medium text-text-strong truncate" displayClass="text-14-medium text-text-strong truncate" stopPropagation @@ -1864,7 +1861,7 @@ export default function Layout(props: ParentProps) { - {p.worktree.replace(homedir(), "~")} + {p().worktree.replace(homedir(), "~")}
@@ -1883,7 +1880,7 @@ export default function Layout(props: ParentProps) { icon="dot-grid" variant="ghost" data-action="project-menu" - data-project={base64Encode(p.worktree)} + data-project={base64Encode(p().worktree)} class="shrink-0 size-6 rounded-md data-[expanded]:bg-surface-base-active" classList={{ "opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100": !panelProps.mobile, @@ -1892,24 +1889,24 @@ export default function Layout(props: ParentProps) { /> - showEditProjectDialog(p)}> + showEditProjectDialog(p())}> {language.t("common.edit")} toggleProjectWorkspaces(p)} + data-project={base64Encode(p().worktree)} + disabled={p().vcs !== "git" && !layout.sidebar.workspaces(p().worktree)()} + onSelect={() => toggleProjectWorkspaces(p())} > - {layout.sidebar.workspaces(p.worktree)() + {layout.sidebar.workspaces(p().worktree)() ? language.t("sidebar.workspaces.disable") : language.t("sidebar.workspaces.enable")} @@ -1920,8 +1917,8 @@ export default function Layout(props: ParentProps) { closeProject(p.worktree)} + data-project={base64Encode(p().worktree)} + onSelect={() => closeProject(p().worktree)} > {language.t("common.close")} @@ -1941,7 +1938,7 @@ export default function Layout(props: ParentProps) { size="large" icon="plus-small" class="w-full" - onClick={() => navigateWithSidebarReset(`/${base64Encode(p.worktree)}/session`)} + onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)} > {language.t("command.session.new")} @@ -1949,7 +1946,7 @@ export default function Layout(props: ParentProps) {
@@ -1959,7 +1956,7 @@ export default function Layout(props: ParentProps) { > <>
-
@@ -1984,7 +1981,7 @@ export default function Layout(props: ParentProps) { @@ -2096,11 +2093,9 @@ export default function Layout(props: ParentProps) { />
- {(worktree) => ( -
- -
- )} +
+ +
Date: Fri, 6 Mar 2026 18:19:15 +0530 Subject: [PATCH 027/104] share: speed up share loads (#16165) --- packages/enterprise/src/core/share.ts | 140 +++++++++++------- .../enterprise/src/routes/api/[...path].ts | 1 + .../enterprise/src/routes/share/[shareID].tsx | 90 ++--------- packages/enterprise/test/core/share.test.ts | 30 +++- packages/opencode/src/share/share-next.ts | 32 +++- packages/ui/src/components/markdown.tsx | 17 ++- packages/ui/src/components/session-review.tsx | 2 +- 7 files changed, 167 insertions(+), 145 deletions(-) diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index d7f5c8b8d5..c6291b75d2 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -1,10 +1,8 @@ import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2" import { fn } from "@opencode-ai/util/fn" import { iife } from "@opencode-ai/util/iife" -import { Identifier } from "@opencode-ai/util/identifier" import z from "zod" import { Storage } from "./storage" -import { Binary } from "@opencode-ai/util/binary" export namespace Share { export const Info = z.object({ @@ -38,6 +36,81 @@ export namespace Share { ]) export type Data = z.infer + type Snapshot = { + data: Data[] + } + + type Compaction = { + event?: string + data: Data[] + } + + function key(item: Data) { + switch (item.type) { + case "session": + return "session" + case "message": + return `message/${item.data.id}` + case "part": + return `part/${item.data.messageID}/${item.data.id}` + case "session_diff": + return "session_diff" + case "model": + return "model" + } + } + + function merge(...items: Data[][]) { + const map = new Map() + for (const list of items) { + for (const item of list) { + map.set(key(item), item) + } + } + return Array.from(map.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([, item]) => item) + } + + async function readSnapshot(shareID: string) { + return (await Storage.read(["share_snapshot", shareID]))?.data + } + + async function writeSnapshot(shareID: string, data: Data[]) { + await Storage.write(["share_snapshot", shareID], { data }) + } + + async function legacy(shareID: string) { + const compaction: Compaction = (await Storage.read(["share_compaction", shareID])) ?? { + data: [], + event: undefined, + } + const list = await Storage.list({ + prefix: ["share_event", shareID], + before: compaction.event, + }).then((x) => x.toReversed()) + if (list.length === 0) { + if (compaction.data.length > 0) await writeSnapshot(shareID, compaction.data) + return compaction.data + } + + const next = merge( + compaction.data, + await Promise.all(list.map(async (event) => await Storage.read(event))).then((x) => + x.flatMap((item) => item ?? []), + ), + ) + + await Promise.all([ + Storage.write(["share_compaction", shareID], { + event: list.at(-1)?.at(-1), + data: next, + }), + writeSnapshot(shareID, next), + ]) + return next + } + export const create = fn(z.object({ sessionID: z.string() }), async (body) => { const isTest = process.env.NODE_ENV === "test" || body.sessionID.startsWith("test_") const info: Info = { @@ -47,7 +120,7 @@ export namespace Share { } const exists = await get(info.id) if (exists) throw new Errors.AlreadyExists(info.id) - await Storage.write(["share", info.id], info) + await Promise.all([Storage.write(["share", info.id], info), writeSnapshot(info.id, [])]) return info }) @@ -60,8 +133,13 @@ export namespace Share { if (!share) throw new Errors.NotFound(body.id) if (share.secret !== body.secret) throw new Errors.InvalidSecret(body.id) await Storage.remove(["share", body.id]) - const list = await Storage.list({ prefix: ["share_data", body.id] }) - for (const item of list) { + const groups = await Promise.all([ + Storage.list({ prefix: ["share_snapshot", body.id] }), + Storage.list({ prefix: ["share_compaction", body.id] }), + Storage.list({ prefix: ["share_event", body.id] }), + Storage.list({ prefix: ["share_data", body.id] }), + ]) + for (const item of groups.flat()) { await Storage.remove(item) } }) @@ -75,59 +153,13 @@ export namespace Share { const share = await get(input.share.id) if (!share) throw new Errors.NotFound(input.share.id) if (share.secret !== input.share.secret) throw new Errors.InvalidSecret(input.share.id) - await Storage.write(["share_event", input.share.id, Identifier.descending()], input.data) + const data = (await readSnapshot(input.share.id)) ?? (await legacy(input.share.id)) + await writeSnapshot(input.share.id, merge(data, input.data)) }, ) - type Compaction = { - event?: string - data: Data[] - } - export async function data(shareID: string) { - console.log("reading compaction") - const compaction: Compaction = (await Storage.read(["share_compaction", shareID])) ?? { - data: [], - event: undefined, - } - console.log("reading pending events") - const list = await Storage.list({ - prefix: ["share_event", shareID], - before: compaction.event, - }).then((x) => x.toReversed()) - - console.log("compacting", list.length) - - if (list.length > 0) { - const data = await Promise.all(list.map(async (event) => await Storage.read(event))).then((x) => x.flat()) - for (const item of data) { - if (!item) continue - const key = (item: Data) => { - switch (item.type) { - case "session": - return "session" - case "message": - return `message/${item.data.id}` - case "part": - return `${item.data.messageID}/${item.data.id}` - case "session_diff": - return "session_diff" - case "model": - return "model" - } - } - const id = key(item) - const result = Binary.search(compaction.data, id, key) - if (result.found) { - compaction.data[result.index] = item - } else { - compaction.data.splice(result.index, 0, item) - } - } - compaction.event = list.at(-1)?.at(-1) - await Storage.write(["share_compaction", shareID], compaction) - } - return compaction.data + return (await readSnapshot(shareID)) ?? legacy(shareID) } export const syncOld = fn( diff --git a/packages/enterprise/src/routes/api/[...path].ts b/packages/enterprise/src/routes/api/[...path].ts index e77c00de92..f97788bd03 100644 --- a/packages/enterprise/src/routes/api/[...path].ts +++ b/packages/enterprise/src/routes/api/[...path].ts @@ -108,6 +108,7 @@ app validator("param", z.object({ shareID: z.string() })), async (c) => { const { shareID } = c.req.valid("param") + c.header("Cache-Control", "public, max-age=30, s-maxage=300, stale-while-revalidate=86400") return c.json(await Share.data(shareID)) }, ) diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 007b4c268d..e755ea75a1 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -5,12 +5,11 @@ import { DataProvider } from "@opencode-ai/ui/context" import { FileComponentProvider } from "@opencode-ai/ui/context/file" import { WorkerPoolProvider } from "@opencode-ai/ui/context/worker-pool" import { createAsync, query, useParams } from "@solidjs/router" -import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js" +import { createMemo, createSignal, ErrorBoundary, For, Match, Show, Switch } from "solid-js" import { Share } from "~/core/share" import { Logo, Mark } from "@opencode-ai/ui/logo" import { IconButton } from "@opencode-ai/ui/icon-button" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { createDefaultOptions } from "@opencode-ai/ui/pierre" import { iife } from "@opencode-ai/util/iife" import { Binary } from "@opencode-ai/util/binary" import { NamedError } from "@opencode-ai/util/error" @@ -20,11 +19,11 @@ import z from "zod" import NotFound from "../[...404]" import { Tabs } from "@opencode-ai/ui/tabs" import { MessageNav } from "@opencode-ai/ui/message-nav" -import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { FileSSR } from "@opencode-ai/ui/file-ssr" import { clientOnly } from "@solidjs/start" import { Meta, Title } from "@solidjs/meta" import { Base64 } from "js-base64" +import { getRequestEvent } from "solid-js/web" const ClientOnlyWorkerPoolProvider = clientOnly(() => import("@opencode-ai/ui/pierre/worker").then((m) => ({ @@ -54,12 +53,6 @@ const getData = query(async (shareID) => { session_diff: { [sessionID: string]: FileDiff[] } - session_diff_preload: { - [sessionID: string]: PreloadMultiFileDiffResult[] - } - session_diff_preload_split: { - [sessionID: string]: PreloadMultiFileDiffResult[] - } session_status: { [sessionID: string]: SessionStatus } @@ -79,12 +72,6 @@ const getData = query(async (shareID) => { session_diff: { [share.sessionID]: [], }, - session_diff_preload: { - [share.sessionID]: [], - }, - session_diff_preload_split: { - [share.sessionID]: [], - }, session_status: { [share.sessionID]: { type: "idle", @@ -101,28 +88,6 @@ const getData = query(async (shareID) => { break case "session_diff": result.session_diff[share.sessionID] = item.data - await Promise.all([ - Promise.all( - item.data.map(async (diff) => - preloadMultiFileDiff({ - oldFile: { name: diff.file, contents: diff.before }, - newFile: { name: diff.file, contents: diff.after }, - options: createDefaultOptions("unified"), - // annotations, - }), - ), - ).then((r) => (result.session_diff_preload[share.sessionID] = r)), - Promise.all( - item.data.map(async (diff) => - preloadMultiFileDiff({ - oldFile: { name: diff.file, contents: diff.before }, - newFile: { name: diff.file, contents: diff.after }, - options: createDefaultOptions("split"), - // annotations, - }), - ), - ).then((r) => (result.session_diff_preload_split[share.sessionID] = r)), - ]) break case "message": result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? [] @@ -143,17 +108,15 @@ const getData = query(async (shareID) => { }, "getShareData") export default function () { + getRequestEvent()?.response.headers.set( + "Cache-Control", + "public, max-age=30, s-maxage=300, stale-while-revalidate=86400", + ) + const params = useParams() const data = createAsync(async () => { if (!params.shareID) throw new Error("Missing shareID") - const now = Date.now() - const data = getData(params.shareID) - console.log("getData", Date.now() - now) - return data - }) - - createEffect(() => { - console.log(data()) + return getData(params.shareID) }) return ( @@ -241,22 +204,8 @@ export default function () { const provider = createMemo(() => activeMessage()?.model?.providerID) const modelID = createMemo(() => activeMessage()?.model?.modelID) const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID())) - const diffs = createMemo(() => { - const diffs = data().session_diff[data().sessionID] ?? [] - const preloaded = data().session_diff_preload[data().sessionID] ?? [] - return diffs.map((diff) => ({ - ...diff, - preloaded: preloaded.find((d) => d.newFile.name === diff.file), - })) - }) - const splitDiffs = createMemo(() => { - const diffs = data().session_diff[data().sessionID] ?? [] - const preloaded = data().session_diff_preload_split[data().sessionID] ?? [] - return diffs.map((diff) => ({ - ...diff, - preloaded: preloaded.find((d) => d.newFile.name === diff.file), - })) - }) + const diffs = createMemo(() => data().session_diff[data().sessionID] ?? []) + const [diffStyle, setDiffStyle] = createSignal<"unified" | "split">("unified") const title = () => (
@@ -380,18 +329,9 @@ export default function () { 0}>
-