From 29671c1397b0ecfb9510186a0aae89696896da2a Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:59:44 -0500 Subject: [PATCH 001/756] fix: token substitution in OPENCODE_CONFIG_CONTENT (#13384) --- packages/opencode/src/config/config.ts | 8 ++- packages/opencode/src/flag/flag.ts | 13 +++- packages/opencode/test/config/config.test.ts | 65 ++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 8f0f583ea3..f4d7a840fe 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -175,8 +175,14 @@ export namespace Config { } // Inline config content overrides all non-managed config sources. + // Route through load() to enable {env:} and {file:} token substitution. + // Use a path within Instance.directory so relative {file:} paths resolve correctly. + // The filename "OPENCODE_CONFIG_CONTENT" appears in error messages for clarity. if (Flag.OPENCODE_CONFIG_CONTENT) { - result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT)) + result = mergeConfigConcatArrays( + result, + await load(Flag.OPENCODE_CONFIG_CONTENT, path.join(Instance.directory, "OPENCODE_CONFIG_CONTENT")), + ) log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index dfcb88bc51..641cb3325b 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -8,7 +8,7 @@ export namespace Flag { export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"] export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"] export declare const OPENCODE_CONFIG_DIR: string | undefined - export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"] + export declare const OPENCODE_CONFIG_CONTENT: string | undefined export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE") export const OPENCODE_DISABLE_PRUNE = truthy("OPENCODE_DISABLE_PRUNE") export const OPENCODE_DISABLE_TERMINAL_TITLE = truthy("OPENCODE_DISABLE_TERMINAL_TITLE") @@ -94,3 +94,14 @@ Object.defineProperty(Flag, "OPENCODE_CLIENT", { enumerable: true, configurable: false, }) + +// Dynamic getter for OPENCODE_CONFIG_CONTENT +// This must be evaluated at access time, not module load time, +// because external tooling may set this env var at runtime +Object.defineProperty(Flag, "OPENCODE_CONFIG_CONTENT", { + get() { + return process.env["OPENCODE_CONFIG_CONTENT"] + }, + enumerable: true, + configurable: false, +}) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 91b87f6498..331e05d5a7 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1800,3 +1800,68 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { } }) }) + +// OPENCODE_CONFIG_CONTENT should support {env:} and {file:} token substitution +// just like file-based config sources do. +describe("OPENCODE_CONFIG_CONTENT token substitution", () => { + test("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", async () => { + const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] + const originalTestVar = process.env["TEST_CONFIG_VAR"] + process.env["TEST_CONFIG_VAR"] = "test_api_key_12345" + process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ + $schema: "https://opencode.ai/config.json", + theme: "{env:TEST_CONFIG_VAR}", + }) + + try { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("test_api_key_12345") + }, + }) + } finally { + if (originalEnv !== undefined) { + process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv + } else { + delete process.env["OPENCODE_CONFIG_CONTENT"] + } + if (originalTestVar !== undefined) { + process.env["TEST_CONFIG_VAR"] = originalTestVar + } else { + delete process.env["TEST_CONFIG_VAR"] + } + } + }) + + test("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", async () => { + const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] + + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "api_key.txt"), "secret_key_from_file") + process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ + $schema: "https://opencode.ai/config.json", + theme: "{file:./api_key.txt}", + }) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("secret_key_from_file") + }, + }) + } finally { + if (originalEnv !== undefined) { + process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv + } else { + delete process.env["OPENCODE_CONFIG_CONTENT"] + } + } + }) +}) From 76db218674496f9ca9e91b49e5718eabf6df7cc0 Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 12 Feb 2026 23:18:40 +0000 Subject: [PATCH 002/756] release: v1.1.64 --- bun.lock | 30 +++++++++++++------------- 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/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 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index f3590f53d2..3fe8a4ca07 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -73,7 +73,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -107,7 +107,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -134,7 +134,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -158,7 +158,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -182,7 +182,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -215,7 +215,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -244,7 +244,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -260,7 +260,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.63", + "version": "1.1.64", "bin": { "opencode": "./bin/opencode", }, @@ -366,7 +366,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -386,7 +386,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.63", + "version": "1.1.64", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -397,7 +397,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -410,7 +410,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -452,7 +452,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "zod": "catalog:", }, @@ -463,7 +463,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.63", + "version": "1.1.64", "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 90b5a9c300..ebd1a4b35b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.63", + "version": "1.1.64", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index d80de55a24..c5556a4431 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.63", + "version": "1.1.64", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index ccc11ba3a0..498270b952 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.1.63", + "version": "1.1.64", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index b612f54308..d2117dffb2 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.63", + "version": "1.1.64", "$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 864c233820..f632ab92fe 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.63", + "version": "1.1.64", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 526610e6eb..da89d36a88 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.63", + "version": "1.1.64", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index a86a549495..31b62e12b1 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.63", + "version": "1.1.64", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 475e6a870d..22aca32bae 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.1.63" +version = "1.1.64" 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.1.63/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.64/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.63/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.64/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.63/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.64/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.1.63/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.64/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.1.63/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.64/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index f2e8e5dc5d..ae9a6d7b3c 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.63", + "version": "1.1.64", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 99a69c3357..f58a3d2fe9 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.63", + "version": "1.1.64", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 7040059f33..d88d5a7ba3 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.1.63", + "version": "1.1.64", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 7a47fbfa66..13d0b549ba 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.1.63", + "version": "1.1.64", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index abede0f9d2..7f0eaff83f 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.63", + "version": "1.1.64", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 34720215f1..6d20e3dfdc 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.63", + "version": "1.1.64", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 6bc354049b..078adbe142 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.63", + "version": "1.1.64", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 0d04a5adfe..6f5fe726f5 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.1.63", + "version": "1.1.64", "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 61ccf91b44..30c07d3139 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.1.63", + "version": "1.1.64", "publisher": "sst-dev", "repository": { "type": "git", From 991496a753545f2705072d4da537c175dca357e6 Mon Sep 17 00:00:00 2001 From: projectArtur <155688912+ASidorenkoCode@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:20:00 +0100 Subject: [PATCH 003/756] fix: resolve ACP hanging indefinitely in thinking state on Windows (#13222) Co-authored-by: Claude Opus 4.6 Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline --- packages/opencode/src/project/project.ts | 46 ++++++------- packages/opencode/src/snapshot/index.ts | 5 +- packages/opencode/src/util/git.ts | 64 +++++++++++++++++++ .../opencode/test/project/project.test.ts | 59 ++++++++--------- 4 files changed, 112 insertions(+), 62 deletions(-) create mode 100644 packages/opencode/src/util/git.ts diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index f6902de4e1..c79a62c6c9 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -2,7 +2,6 @@ import z from "zod" import fs from "fs/promises" import { Filesystem } from "../util/filesystem" import path from "path" -import { $ } from "bun" import { Storage } from "../storage/storage" import { Log } from "../util/log" import { Flag } from "@/flag/flag" @@ -13,6 +12,7 @@ import { BusEvent } from "@/bus/bus-event" import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" import { existsSync } from "fs" +import { git } from "../util/git" export namespace Project { const log = Log.create({ service: "project" }) @@ -55,15 +55,15 @@ export namespace Project { const { id, sandbox, worktree, vcs } = await iife(async () => { const matches = Filesystem.up({ targets: [".git"], start: directory }) - const git = await matches.next().then((x) => x.value) + const dotgit = await matches.next().then((x) => x.value) await matches.return() - if (git) { - let sandbox = path.dirname(git) + if (dotgit) { + let sandbox = path.dirname(dotgit) const gitBinary = Bun.which("git") // cached id calculation - let id = await Bun.file(path.join(git, "opencode")) + let id = await Bun.file(path.join(dotgit, "opencode")) .text() .then((x) => x.trim()) .catch(() => undefined) @@ -79,13 +79,11 @@ export namespace Project { // generate id from root commit if (!id) { - const roots = await $`git rev-list --max-parents=0 --all` - .quiet() - .nothrow() - .cwd(sandbox) - .text() - .then((x) => - x + const roots = await git(["rev-list", "--max-parents=0", "--all"], { + cwd: sandbox, + }) + .then(async (result) => + (await result.text()) .split("\n") .filter(Boolean) .map((x) => x.trim()) @@ -104,7 +102,7 @@ export namespace Project { id = roots[0] if (id) { - void Bun.file(path.join(git, "opencode")) + void Bun.file(path.join(dotgit, "opencode")) .write(id) .catch(() => undefined) } @@ -119,12 +117,10 @@ export namespace Project { } } - const top = await $`git rev-parse --show-toplevel` - .quiet() - .nothrow() - .cwd(sandbox) - .text() - .then((x) => path.resolve(sandbox, x.trim())) + const top = await git(["rev-parse", "--show-toplevel"], { + cwd: sandbox, + }) + .then(async (result) => path.resolve(sandbox, (await result.text()).trim())) .catch(() => undefined) if (!top) { @@ -138,13 +134,11 @@ export namespace Project { sandbox = top - const worktree = await $`git rev-parse --git-common-dir` - .quiet() - .nothrow() - .cwd(sandbox) - .text() - .then((x) => { - const dirname = path.dirname(x.trim()) + const worktree = await git(["rev-parse", "--git-common-dir"], { + cwd: sandbox, + }) + .then(async (result) => { + const dirname = path.dirname((await result.text()).trim()) if (dirname === ".") return sandbox return dirname }) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index b3c8a905c2..a1c2b57812 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -2,6 +2,7 @@ import { $ } from "bun" import path from "path" import fs from "fs/promises" import { Log } from "../util/log" +import { Flag } from "../flag/flag" import { Global } from "../global" import z from "zod" import { Config } from "../config/config" @@ -23,7 +24,7 @@ export namespace Snapshot { } export async function cleanup() { - if (Instance.project.vcs !== "git") return + if (Instance.project.vcs !== "git" || Flag.OPENCODE_CLIENT === "acp") return const cfg = await Config.get() if (cfg.snapshot === false) return const git = gitdir() @@ -48,7 +49,7 @@ export namespace Snapshot { } export async function track() { - if (Instance.project.vcs !== "git") return + if (Instance.project.vcs !== "git" || Flag.OPENCODE_CLIENT === "acp") return const cfg = await Config.get() if (cfg.snapshot === false) return const git = gitdir() diff --git a/packages/opencode/src/util/git.ts b/packages/opencode/src/util/git.ts new file mode 100644 index 0000000000..201def36a8 --- /dev/null +++ b/packages/opencode/src/util/git.ts @@ -0,0 +1,64 @@ +import { $ } from "bun" +import { Flag } from "../flag/flag" + +export interface GitResult { + exitCode: number + text(): string | Promise + stdout: Buffer | ReadableStream + stderr: Buffer | ReadableStream +} + +/** + * Run a git command. + * + * Uses Bun's lightweight `$` shell by default. When the process is running + * as an ACP client, child processes inherit the parent's stdin pipe which + * carries protocol data – on Windows this causes git to deadlock. In that + * case we fall back to `Bun.spawn` with `stdin: "ignore"`. + */ +export async function git(args: string[], opts: { cwd: string; env?: Record }): Promise { + if (Flag.OPENCODE_CLIENT === "acp") { + try { + const proc = Bun.spawn(["git", ...args], { + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + cwd: opts.cwd, + env: opts.env ? { ...process.env, ...opts.env } : process.env, + }) + // Read output concurrently with exit to avoid pipe buffer deadlock + const [exitCode, stdout, stderr] = await Promise.all([ + proc.exited, + new Response(proc.stdout).arrayBuffer(), + new Response(proc.stderr).arrayBuffer(), + ]) + const stdoutBuf = Buffer.from(stdout) + const stderrBuf = Buffer.from(stderr) + return { + exitCode, + text: () => stdoutBuf.toString(), + stdout: stdoutBuf, + stderr: stderrBuf, + } + } catch (error) { + const stderr = Buffer.from(error instanceof Error ? error.message : String(error)) + return { + exitCode: 1, + text: () => "", + stdout: Buffer.alloc(0), + stderr, + } + } + } + + const env = opts.env ? { ...process.env, ...opts.env } : undefined + let cmd = $`git ${args}`.quiet().nothrow().cwd(opts.cwd) + if (env) cmd = cmd.env(env) + const result = await cmd + return { + exitCode: result.exitCode, + text: () => result.text(), + stdout: result.stdout, + stderr: result.stderr, + } +} diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 0e99c5648b..581c63b567 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -8,54 +8,45 @@ import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) -const bunModule = await import("bun") +const gitModule = await import("../../src/util/git") +const originalGit = gitModule.git + type Mode = "none" | "rev-list-fail" | "top-fail" | "common-dir-fail" let mode: Mode = "none" -function render(parts: TemplateStringsArray, vals: unknown[]) { - return parts.reduce((acc, part, i) => `${acc}${part}${i < vals.length ? String(vals[i]) : ""}`, "") -} - -function fakeShell(output: { exitCode: number; stdout: string; stderr: string }) { - const result = { - exitCode: output.exitCode, - stdout: Buffer.from(output.stdout), - stderr: Buffer.from(output.stderr), - text: async () => output.stdout, - } - const shell = { - quiet: () => shell, - nothrow: () => shell, - cwd: () => shell, - env: () => shell, - text: async () => output.stdout, - then: (onfulfilled: (value: typeof result) => unknown, onrejected?: (reason: unknown) => unknown) => - Promise.resolve(result).then(onfulfilled, onrejected), - catch: (onrejected: (reason: unknown) => unknown) => Promise.resolve(result).catch(onrejected), - finally: (onfinally: (() => void) | undefined | null) => Promise.resolve(result).finally(onfinally), - } - return shell -} - -mock.module("bun", () => ({ - ...bunModule, - $: (parts: TemplateStringsArray, ...vals: unknown[]) => { - const cmd = render(parts, vals).replaceAll(",", " ").replace(/\s+/g, " ").trim() +mock.module("../../src/util/git", () => ({ + git: (args: string[], opts: { cwd: string; env?: Record }) => { + const cmd = ["git", ...args].join(" ") if ( mode === "rev-list-fail" && cmd.includes("git rev-list") && cmd.includes("--max-parents=0") && cmd.includes("--all") ) { - return fakeShell({ exitCode: 128, stdout: "", stderr: "fatal" }) + return Promise.resolve({ + exitCode: 128, + text: () => Promise.resolve(""), + stdout: Buffer.from(""), + stderr: Buffer.from("fatal"), + }) } if (mode === "top-fail" && cmd.includes("git rev-parse") && cmd.includes("--show-toplevel")) { - return fakeShell({ exitCode: 128, stdout: "", stderr: "fatal" }) + return Promise.resolve({ + exitCode: 128, + text: () => Promise.resolve(""), + stdout: Buffer.from(""), + stderr: Buffer.from("fatal"), + }) } if (mode === "common-dir-fail" && cmd.includes("git rev-parse") && cmd.includes("--git-common-dir")) { - return fakeShell({ exitCode: 128, stdout: "", stderr: "fatal" }) + return Promise.resolve({ + exitCode: 128, + text: () => Promise.resolve(""), + stdout: Buffer.from(""), + stderr: Buffer.from("fatal"), + }) } - return (bunModule.$ as any)(parts, ...vals) + return originalGit(args, opts) }, })) From adb0c4d4f94f6260a67bb9a48ef3a7faa6042bf3 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 13 Feb 2026 08:49:52 +0800 Subject: [PATCH 004/756] desktop: only show loading window if sqlite migration is necessary --- packages/desktop/src-tauri/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index bec72c04fa..fe71ef029d 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -566,8 +566,8 @@ async fn initialize(app: AppHandle) { // come from any invocation of the sidecar CLI. The progress is captured by a stdout stream interceptor. // Then in the loading task, we wait for sqlite migration to complete before // starting our health check against the server, otherwise long migrations could result in a timeout. - let sqlite_enabled = option_env!("OPENCODE_SQLITE").is_some(); - let sqlite_done = (sqlite_enabled && !sqlite_file_exists()).then(|| { + let needs_sqlite_migration = option_env!("OPENCODE_SQLITE").is_some() && !sqlite_file_exists(); + let sqlite_done = needs_sqlite_migration.then(|| { tracing::info!( path = %opencode_db_path().expect("failed to get db path").display(), "Sqlite file not found, waiting for it to be generated" @@ -670,7 +670,7 @@ async fn initialize(app: AppHandle) { .map_err(|_| ()) .shared(); - let loading_window = if sqlite_enabled + let loading_window = if needs_sqlite_migration && timeout(Duration::from_secs(1), loading_task.clone()) .await .is_err() From 0303c29e3ff4f45aff4176e496ecb3f5fa5b611a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:27:16 -0600 Subject: [PATCH 005/756] fix(app): failed to create store --- .../context/global-sync/child-store.test.ts | 39 +++++++++++++++++++ .../src/context/global-sync/child-store.ts | 6 +-- 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 packages/app/src/context/global-sync/child-store.test.ts diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts new file mode 100644 index 0000000000..500f0fc70a --- /dev/null +++ b/packages/app/src/context/global-sync/child-store.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test" +import { createRoot, getOwner } from "solid-js" +import { createStore } from "solid-js/store" +import type { State } from "./types" +import { createChildStoreManager } from "./child-store" + +const child = () => createStore({} as State) + +describe("createChildStoreManager", () => { + test("does not evict the active directory during mark", () => { + const owner = createRoot((dispose) => { + const current = getOwner() + dispose() + return current + }) + if (!owner) throw new Error("owner required") + + const manager = createChildStoreManager({ + owner, + markStats() {}, + incrementEvictions() {}, + isBooting: () => false, + isLoadingSessions: () => false, + onBootstrap() {}, + onDispose() {}, + }) + + Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => { + manager.children[directory] = child() + manager.pin(directory) + }) + + const directory = "/active" + manager.children[directory] = child() + manager.mark(directory) + + expect(manager.children[directory]).toBeDefined() + }) +}) diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 2feb7fe088..af08c3bd43 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -36,7 +36,7 @@ export function createChildStoreManager(input: { const mark = (directory: string) => { if (!directory) return lifecycle.set(directory, { lastAccessAt: Date.now() }) - runEviction() + runEviction(directory) } const pin = (directory: string) => { @@ -106,7 +106,7 @@ export function createChildStoreManager(input: { return true } - function runEviction() { + function runEviction(skip?: string) { const stores = Object.keys(children) if (stores.length === 0) return const list = pickDirectoriesToEvict({ @@ -116,7 +116,7 @@ export function createChildStoreManager(input: { max: MAX_DIR_STORES, ttl: DIR_IDLE_TTL_MS, now: Date.now(), - }) + }).filter((directory) => directory !== skip) if (list.length === 0) return for (const directory of list) { if (!disposeDirectory(directory)) continue From 8da5fd0a66b2b31f4d77eb8c0949c148b9a7d760 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:35:01 -0600 Subject: [PATCH 006/756] fix(app): worktree delete --- packages/opencode/src/worktree/index.ts | 81 +++++++++++++------ .../test/project/worktree-remove.test.ts | 64 +++++++++++++++ 2 files changed, 119 insertions(+), 26 deletions(-) create mode 100644 packages/opencode/test/project/worktree-remove.test.ts diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 88c778cbb8..85d7f6d0e8 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -420,49 +420,78 @@ export namespace Worktree { } const directory = await canonical(input.directory) + const locate = async (stdout: Uint8Array | undefined) => { + const lines = outputText(stdout) + .split("\n") + .map((line) => line.trim()) + const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => { + if (!line) return acc + if (line.startsWith("worktree ")) { + acc.push({ path: line.slice("worktree ".length).trim() }) + return acc + } + const current = acc[acc.length - 1] + if (!current) return acc + if (line.startsWith("branch ")) { + current.branch = line.slice("branch ".length).trim() + } + return acc + }, []) + + return (async () => { + for (const item of entries) { + if (!item.path) continue + const key = await canonical(item.path) + if (key === directory) return item + } + })() + } + + const clean = (target: string) => + fs + .rm(target, { + recursive: true, + force: true, + maxRetries: 5, + retryDelay: 100, + }) + .catch((error) => { + const message = error instanceof Error ? error.message : String(error) + throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" }) + }) + 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" }) } - const lines = outputText(list.stdout) - .split("\n") - .map((line) => line.trim()) - const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => { - if (!line) return acc - if (line.startsWith("worktree ")) { - acc.push({ path: line.slice("worktree ".length).trim() }) - return acc - } - const current = acc[acc.length - 1] - if (!current) return acc - if (line.startsWith("branch ")) { - current.branch = line.slice("branch ".length).trim() - } - return acc - }, []) - - const entry = await (async () => { - for (const item of entries) { - if (!item.path) continue - const key = await canonical(item.path) - if (key === directory) return item - } - })() + const entry = await locate(list.stdout) if (!entry?.path) { const directoryExists = await exists(directory) if (directoryExists) { - await fs.rm(directory, { recursive: true, force: true }) + await clean(directory) } return true } const removed = await $`git worktree remove --force ${entry.path}`.quiet().nothrow().cwd(Instance.worktree) if (removed.exitCode !== 0) { - throw new RemoveFailedError({ message: errorText(removed) || "Failed to remove git worktree" }) + const next = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree) + if (next.exitCode !== 0) { + throw new RemoveFailedError({ + message: errorText(removed) || errorText(next) || "Failed to remove git worktree", + }) + } + + const stale = await locate(next.stdout) + if (stale?.path) { + throw new RemoveFailedError({ message: errorText(removed) || "Failed to remove git worktree" }) + } } + await clean(entry.path) + const branch = entry.branch?.replace(/^refs\/heads\//, "") if (branch) { const deleted = await $`git branch -D ${branch}`.quiet().nothrow().cwd(Instance.worktree) diff --git a/packages/opencode/test/project/worktree-remove.test.ts b/packages/opencode/test/project/worktree-remove.test.ts new file mode 100644 index 0000000000..32d38fe84d --- /dev/null +++ b/packages/opencode/test/project/worktree-remove.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from "bun:test" +import { $ } from "bun" +import fs from "fs/promises" +import path from "path" +import { Instance } from "../../src/project/instance" +import { Worktree } from "../../src/worktree" +import { tmpdir } from "../fixture/fixture" + +describe("Worktree.remove", () => { + test("continues when git remove exits non-zero after detaching", async () => { + await using tmp = await tmpdir({ git: true }) + const root = tmp.path + const name = `remove-regression-${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() + + const real = (await $`which git`.quiet().text()).trim() + expect(real).toBeTruthy() + + const bin = path.join(root, "bin") + const shim = path.join(bin, "git") + await fs.mkdir(bin, { recursive: true }) + await Bun.write( + shim, + [ + "#!/bin/bash", + `REAL_GIT=${JSON.stringify(real)}`, + 'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then', + ' "$REAL_GIT" "$@" >/dev/null 2>&1', + ' echo "fatal: failed to remove worktree: Directory not empty" >&2', + " exit 1", + "fi", + 'exec "$REAL_GIT" "$@"', + ].join("\n"), + ) + await fs.chmod(shim, 0o755) + + const prev = process.env.PATH ?? "" + process.env.PATH = `${bin}${path.delimiter}${prev}` + + const ok = await (async () => { + try { + return await Instance.provide({ + directory: root, + fn: () => Worktree.remove({ directory: dir }), + }) + } finally { + process.env.PATH = prev + } + })() + + expect(ok).toBe(true) + expect(await Bun.file(dir).exists()).toBe(false) + + const list = await $`git worktree list --porcelain`.cwd(root).quiet().text() + expect(list).not.toContain(`worktree ${dir}`) + + const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow() + expect(ref.exitCode).not.toBe(0) + }) +}) From b525c03d205e37ad7527e6bd1749b324395dd6b7 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:52:20 -0600 Subject: [PATCH 007/756] chore: cleanup --- packages/ui/src/components/toast.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/ui/src/components/toast.css b/packages/ui/src/components/toast.css index de547f9c78..4e6504d061 100644 --- a/packages/ui/src/components/toast.css +++ b/packages/ui/src/components/toast.css @@ -21,6 +21,11 @@ padding: 0; max-height: 100%; overflow-y: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } } } @@ -101,6 +106,11 @@ min-width: 0; overflow-x: hidden; overflow-y: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } } [data-slot="toast-title"] { From 7f95cc64c57b439f58833d0300a1da93b3b893df Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:58:43 -0600 Subject: [PATCH 008/756] fix(app): prompt input quirks --- packages/app/src/components/prompt-input.tsx | 33 +++++++++++++---- .../prompt-input/editor-dom.test.ts | 36 ++++++++++++++++--- .../src/components/prompt-input/editor-dom.ts | 2 -- .../components/prompt-input/history.test.ts | 24 ++++++++++++- .../src/components/prompt-input/history.ts | 7 ++++ 5 files changed, 87 insertions(+), 15 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index abc203aa10..8e8c3c895b 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -38,7 +38,12 @@ import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments" -import { navigatePromptHistory, prependHistoryEntry, promptLength } from "./prompt-input/history" +import { + canNavigateHistoryAtCursor, + navigatePromptHistory, + prependHistoryEntry, + promptLength, +} from "./prompt-input/history" import { createPromptSubmit } from "./prompt-input/submit" import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover" import { PromptContextItems } from "./prompt-input/context-items" @@ -473,10 +478,7 @@ export const PromptInput: Component = (props) => { const prev = node.previousSibling const next = node.nextSibling const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR" - const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR" - if (!prevIsBr && !nextIsBr) return false - if (nextIsBr && !prevIsBr && prev) return false - return true + return !!prevIsBr && !next } if (node.nodeType !== Node.ELEMENT_NODE) return false const el = node as HTMLElement @@ -496,6 +498,11 @@ export const PromptInput: Component = (props) => { editorRef.appendChild(createPill(part)) } } + + const last = editorRef.lastChild + if (last?.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR") { + editorRef.appendChild(document.createTextNode("\u200B")) + } } createEffect( @@ -729,7 +736,17 @@ export const PromptInput: Component = (props) => { } } if (last.nodeType !== Node.TEXT_NODE) { - range.setStartAfter(last) + const isBreak = last.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR" + const next = last.nextSibling + const emptyText = next?.nodeType === Node.TEXT_NODE && (next.textContent ?? "") === "" + if (isBreak && (!next || emptyText)) { + const placeholder = next && emptyText ? next : document.createTextNode("\u200B") + if (!next) last.parentNode?.insertBefore(placeholder, null) + placeholder.textContent = "\u200B" + range.setStart(placeholder, 0) + } else { + range.setStartAfter(last) + } } } range.collapse(true) @@ -899,6 +916,8 @@ export const PromptInput: Component = (props) => { .current() .map((part) => ("content" in part ? part.content : "")) .join("") + const direction = event.key === "ArrowUp" ? "up" : "down" + if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition)) return const isEmpty = textContent.trim() === "" || textLength <= 1 const hasNewlines = textContent.includes("\n") const inHistory = store.historyIndex >= 0 @@ -907,7 +926,7 @@ export const PromptInput: Component = (props) => { const allowUp = isEmpty || atStart || (!hasNewlines && !inHistory) || (inHistory && atEnd) const allowDown = isEmpty || atEnd || (!hasNewlines && !inHistory) || (inHistory && atStart) - if (event.key === "ArrowUp") { + if (direction === "up") { if (!allowUp) return if (navigateHistory("up")) { event.preventDefault() diff --git a/packages/app/src/components/prompt-input/editor-dom.test.ts b/packages/app/src/components/prompt-input/editor-dom.test.ts index fce8b4b953..15e759f44a 100644 --- a/packages/app/src/components/prompt-input/editor-dom.test.ts +++ b/packages/app/src/components/prompt-input/editor-dom.test.ts @@ -2,17 +2,26 @@ import { describe, expect, test } from "bun:test" import { createTextFragment, getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./editor-dom" describe("prompt-input editor dom", () => { - test("createTextFragment preserves newlines with br and zero-width placeholders", () => { + test("createTextFragment preserves newlines with consecutive br nodes", () => { const fragment = createTextFragment("foo\n\nbar") const container = document.createElement("div") container.appendChild(fragment) - expect(container.childNodes.length).toBe(5) + expect(container.childNodes.length).toBe(4) + expect(container.childNodes[0]?.textContent).toBe("foo") + expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") + expect((container.childNodes[2] as HTMLElement).tagName).toBe("BR") + expect(container.childNodes[3]?.textContent).toBe("bar") + }) + + test("createTextFragment keeps trailing newline as terminal break", () => { + const fragment = createTextFragment("foo\n") + const container = document.createElement("div") + container.appendChild(fragment) + + expect(container.childNodes.length).toBe(2) expect(container.childNodes[0]?.textContent).toBe("foo") expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") - expect(container.childNodes[2]?.textContent).toBe("\u200B") - expect((container.childNodes[3] as HTMLElement).tagName).toBe("BR") - expect(container.childNodes[4]?.textContent).toBe("bar") }) test("length helpers treat breaks as one char and ignore zero-width chars", () => { @@ -48,4 +57,21 @@ describe("prompt-input editor dom", () => { container.remove() }) + + test("setCursorPosition and getCursorPosition round-trip across blank lines", () => { + const container = document.createElement("div") + container.appendChild(document.createTextNode("a")) + container.appendChild(document.createElement("br")) + container.appendChild(document.createElement("br")) + container.appendChild(document.createTextNode("b")) + document.body.appendChild(container) + + setCursorPosition(container, 2) + expect(getCursorPosition(container)).toBe(2) + + setCursorPosition(container, 3) + expect(getCursorPosition(container)).toBe(3) + + container.remove() + }) }) diff --git a/packages/app/src/components/prompt-input/editor-dom.ts b/packages/app/src/components/prompt-input/editor-dom.ts index 3116ceb126..4850a26ece 100644 --- a/packages/app/src/components/prompt-input/editor-dom.ts +++ b/packages/app/src/components/prompt-input/editor-dom.ts @@ -4,8 +4,6 @@ export function createTextFragment(content: string): DocumentFragment { segments.forEach((segment, index) => { if (segment) { fragment.appendChild(document.createTextNode(segment)) - } else if (segments.length > 1) { - fragment.appendChild(document.createTextNode("\u200B")) } if (index < segments.length - 1) { fragment.appendChild(document.createElement("br")) diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts index 54be9cb75b..a37fdad677 100644 --- a/packages/app/src/components/prompt-input/history.test.ts +++ b/packages/app/src/components/prompt-input/history.test.ts @@ -1,6 +1,12 @@ import { describe, expect, test } from "bun:test" import type { Prompt } from "@/context/prompt" -import { clonePromptParts, navigatePromptHistory, prependHistoryEntry, promptLength } from "./history" +import { + canNavigateHistoryAtCursor, + clonePromptParts, + navigatePromptHistory, + prependHistoryEntry, + promptLength, +} from "./history" const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] @@ -66,4 +72,20 @@ describe("prompt-input history", () => { if (original[1]?.type !== "file") throw new Error("expected file") expect(original[1].selection?.startLine).toBe(1) }) + + test("canNavigateHistoryAtCursor only allows multiline boundaries", () => { + const value = "a\nb\nc" + + expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true) + expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false) + + expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false) + expect(canNavigateHistoryAtCursor("down", value, 2)).toBe(false) + + expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false) + expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true) + + expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(true) + expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(true) + }) }) diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts index 63164f0ba3..f26f808487 100644 --- a/packages/app/src/components/prompt-input/history.ts +++ b/packages/app/src/components/prompt-input/history.ts @@ -4,6 +4,13 @@ const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] export const MAX_HISTORY = 100 +export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number) { + if (!text.includes("\n")) return true + const position = Math.max(0, Math.min(cursor, text.length)) + if (direction === "up") return !text.slice(0, position).includes("\n") + return !text.slice(position).includes("\n") +} + export function clonePromptParts(prompt: Prompt): Prompt { return prompt.map((part) => { if (part.type === "text") return { ...part } From c9719dff7223aa1fc19540f3cd627c7f40e4bf36 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:04:19 -0600 Subject: [PATCH 009/756] fix(app): notification should navigate to session --- packages/app/src/entry.tsx | 7 ++--- packages/app/src/index.ts | 1 + .../app/src/utils/notification-click.test.ts | 26 +++++++++++++++++++ packages/app/src/utils/notification-click.ts | 12 +++++++++ packages/desktop/src/index.tsx | 14 ++++++---- 5 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 packages/app/src/utils/notification-click.test.ts create mode 100644 packages/app/src/utils/notification-click.ts diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index f041204dcc..3a85086b48 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -4,6 +4,7 @@ import { AppBaseProviders, AppInterface } from "@/app" import { Platform, PlatformProvider } from "@/context/platform" import { dict as en } from "@/i18n/en" import { dict as zh } from "@/i18n/zh" +import { handleNotificationClick } from "@/utils/notification-click" import pkg from "../package.json" const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" @@ -68,11 +69,7 @@ const notify: Platform["notify"] = async (title, description, href) => { }) notification.onclick = () => { - window.focus() - if (href) { - window.history.pushState(null, "", href) - window.dispatchEvent(new PopStateEvent("popstate")) - } + handleNotificationClick(href) notification.close() } } diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index 59e1431fa8..33c22f099e 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -1,3 +1,4 @@ export { PlatformProvider, type Platform, type DisplayBackend } from "./context/platform" export { AppBaseProviders, AppInterface } from "./app" export { useCommand } from "./context/command" +export { handleNotificationClick } from "./utils/notification-click" diff --git a/packages/app/src/utils/notification-click.test.ts b/packages/app/src/utils/notification-click.test.ts new file mode 100644 index 0000000000..76535f83a8 --- /dev/null +++ b/packages/app/src/utils/notification-click.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test" +import { handleNotificationClick } from "./notification-click" + +describe("notification click", () => { + test("focuses and navigates when href exists", () => { + const calls: string[] = [] + handleNotificationClick("/abc/session/123", { + focus: () => calls.push("focus"), + location: { + assign: (href) => calls.push(href), + }, + }) + expect(calls).toEqual(["focus", "/abc/session/123"]) + }) + + test("only focuses when href is missing", () => { + const calls: string[] = [] + handleNotificationClick(undefined, { + focus: () => calls.push("focus"), + location: { + assign: (href) => calls.push(href), + }, + }) + expect(calls).toEqual(["focus"]) + }) +}) diff --git a/packages/app/src/utils/notification-click.ts b/packages/app/src/utils/notification-click.ts new file mode 100644 index 0000000000..1234cd1d62 --- /dev/null +++ b/packages/app/src/utils/notification-click.ts @@ -0,0 +1,12 @@ +type WindowTarget = { + focus: () => void + location: { + assign: (href: string) => void + } +} + +export const handleNotificationClick = (href?: string, target: WindowTarget = window) => { + target.focus() + if (!href) return + target.location.assign(href) +} diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 620914dd7e..ff0a093766 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -1,7 +1,14 @@ // @refresh reload import { webviewZoom } from "./webview-zoom" import { render } from "solid-js/web" -import { AppBaseProviders, AppInterface, PlatformProvider, Platform, useCommand } from "@opencode-ai/app" +import { + AppBaseProviders, + AppInterface, + PlatformProvider, + Platform, + useCommand, + handleNotificationClick, +} from "@opencode-ai/app" import { open, save } from "@tauri-apps/plugin-dialog" import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link" import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener" @@ -329,10 +336,7 @@ const createPlatform = (password: Accessor): Platform => { void win.show().catch(() => undefined) void win.unminimize().catch(() => undefined) void win.setFocus().catch(() => undefined) - if (href) { - window.history.pushState(null, "", href) - window.dispatchEvent(new PopStateEvent("popstate")) - } + handleNotificationClick(href) notification.close() } }) From dec304a2737b7accb3bf8b199fb58e81d65026e9 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:05:45 -0600 Subject: [PATCH 010/756] fix(app): emoji as avatar --- packages/ui/src/components/avatar.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx index 76bde1e156..c1617b265c 100644 --- a/packages/ui/src/components/avatar.tsx +++ b/packages/ui/src/components/avatar.tsx @@ -1,5 +1,16 @@ import { type ComponentProps, splitProps, Show } from "solid-js" +const segmenter = + typeof Intl !== "undefined" && "Segmenter" in Intl + ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) + : undefined + +function first(value: string) { + if (!value) return "" + if (!segmenter) return Array.from(value)[0] ?? "" + return segmenter.segment(value)[Symbol.iterator]().next().value?.segment ?? Array.from(value)[0] ?? "" +} + export interface AvatarProps extends ComponentProps<"div"> { fallback: string src?: string @@ -36,7 +47,7 @@ export function Avatar(props: AvatarProps) { ...(!src && split.foreground ? { "--avatar-fg": split.foreground } : {}), }} > - + {(src) => } From e0f1c3c20efb60f19f36e2c8df87dfd30fd2523e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 13 Feb 2026 10:15:36 +0800 Subject: [PATCH 011/756] cleanup desktop loading page --- packages/desktop/src-tauri/src/lib.rs | 2 ++ packages/desktop/src/loading.tsx | 30 +++++++-------------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index fe71ef029d..85ea21d38c 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -665,6 +665,8 @@ async fn initialize(app: AppHandle) { } let _ = server_ready_rx.await; + + tracing::info!("Loading task finished"); } }) .map_err(|_| ()) diff --git a/packages/desktop/src/loading.tsx b/packages/desktop/src/loading.tsx index ee29827227..23a8055c9d 100644 --- a/packages/desktop/src/loading.tsx +++ b/packages/desktop/src/loading.tsx @@ -5,7 +5,7 @@ import { Font } from "@opencode-ai/ui/font" import { Splash } from "@opencode-ai/ui/logo" import { Progress } from "@opencode-ai/ui/progress" import "./styles.css" -import { createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js" import { commands, events, InitStep } from "./bindings" import { Channel } from "@tauri-apps/api/core" @@ -29,36 +29,20 @@ render(() => { channel.onmessage = (next) => setStep(next) commands.awaitInitialization(channel as any).catch(() => undefined) - createEffect(() => { - if (phase() !== "sqlite_waiting") return - + onMount(() => { setLine(0) setPercent(0) const timers = delays.map((ms, i) => setTimeout(() => setLine(i + 1), ms)) - let stop: (() => void) | undefined - let active = true - - void events.sqliteMigrationProgress - .listen((e) => { - if (e.payload.type === "InProgress") setPercent(Math.max(0, Math.min(100, e.payload.value))) - if (e.payload.type === "Done") setPercent(100) - }) - .then((unlisten) => { - if (active) { - stop = unlisten - return - } - - unlisten() - }) - .catch(() => undefined) + const listener = events.sqliteMigrationProgress.listen((e) => { + if (e.payload.type === "InProgress") setPercent(Math.max(0, Math.min(100, e.payload.value))) + if (e.payload.type === "Done") setPercent(100) + }) onCleanup(() => { - active = false + listener.then((cb) => cb()) timers.forEach(clearTimeout) - stop?.() }) }) From fb7b2f6b4d66d14177b5c0168049863842665925 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:19:14 -0600 Subject: [PATCH 012/756] feat(app): toggle all provider models --- .../src/components/dialog-manage-models.tsx | 32 ++++++++++++++++++- packages/app/src/i18n/en.ts | 1 + packages/ui/src/components/list.tsx | 7 ++-- packages/ui/src/components/switch.tsx | 2 +- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index d4d4af0f10..ace79e38a7 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/packages/app/src/components/dialog-manage-models.tsx @@ -1,6 +1,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { Button } from "@opencode-ai/ui/button" import type { Component } from "solid-js" import { useLocal } from "@/context/local" @@ -18,6 +19,14 @@ export const DialogManageModels: Component = () => { dialog.show(() => ) } const providerRank = (id: string) => popularProviders.indexOf(id) + const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID) + const providerVisible = (providerID: string) => + providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id })) + const setProviderVisibility = (providerID: string, checked: boolean) => { + providerList(providerID).forEach((x) => { + local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked) + }) + } return ( { items={local.model.list()} filterKeys={["provider.name", "name", "id"]} sortBy={(a, b) => a.name.localeCompare(b.name)} - groupBy={(x) => x.provider.name} + groupBy={(x) => x.provider.id} + groupHeader={(group) => { + const provider = group.items[0].provider + return ( + <> + {provider.name} + + setProviderVisibility(provider.id, checked)} + hideLabel + > + {provider.name} + + + + ) + }} sortGroupsBy={(a, b) => { const aRank = providerRank(a.items[0].provider.id) const bRank = providerRank(b.items[0].provider.id) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index c138c7b614..99513edaa1 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -109,6 +109,7 @@ export const dict = { "dialog.model.empty": "No model results", "dialog.model.manage": "Manage models", "dialog.model.manage.description": "Customize which models appear in the model selector.", + "dialog.model.manage.provider.toggle": "Toggle all {{provider}} models", "dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode", "dialog.model.unpaid.addMore.title": "Add more models from popular providers", diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index abd5572207..aa2347037e 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -45,6 +45,7 @@ export interface ListProps extends FilteredListProps { itemWrapper?: (item: T, node: JSX.Element) => JSX.Element divider?: boolean add?: ListAddProps + groupHeader?: (group: { category: string; items: T[] }) => JSX.Element } export interface ListRef { @@ -206,7 +207,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) ) } - function GroupHeader(groupProps: { category: string }): JSX.Element { + function GroupHeader(groupProps: { group: { category: string; items: T[] } }): JSX.Element { const [stuck, setStuck] = createSignal(false) const [header, setHeader] = createSignal(undefined) @@ -228,7 +229,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) return (
- {groupProps.category} + {props.groupHeader?.(groupProps.group) ?? groupProps.group.category}
) } @@ -323,7 +324,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) return (
- +
diff --git a/packages/ui/src/components/switch.tsx b/packages/ui/src/components/switch.tsx index a8600aef44..f4f95baf57 100644 --- a/packages/ui/src/components/switch.tsx +++ b/packages/ui/src/components/switch.tsx @@ -10,7 +10,7 @@ export interface SwitchProps extends ParentProps> export function Switch(props: SwitchProps) { const [local, others] = splitProps(props, ["children", "class", "hideLabel", "description"]) return ( - + From dd296f703391aa67ef8cf8340e2712574b380cb1 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:20:24 -0600 Subject: [PATCH 013/756] fix(app): reconnect event stream on disconnect --- packages/app/src/context/global-sdk.tsx | 83 ++++++++++++++----------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index 346657e2fb..3f93b76a72 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -46,6 +46,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo type Queued = { directory: string; payload: Event } const FLUSH_FRAME_MS = 16 const STREAM_YIELD_MS = 8 + const RECONNECT_DELAY_MS = 250 let queue: Queued[] = [] let buffer: Queued[] = [] @@ -91,50 +92,58 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo } let streamErrorLogged = false + const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) void (async () => { - const events = await eventSdk.global.event({ - onSseError: (error) => { - if (streamErrorLogged) return - streamErrorLogged = true - console.error("[global-sdk] event stream error", { - url: server.url, - fetch: eventFetch ? "platform" : "webview", - error, + while (!abort.signal.aborted) { + try { + const events = await eventSdk.global.event({ + onSseError: (error) => { + if (streamErrorLogged) return + streamErrorLogged = true + console.error("[global-sdk] event stream error", { + url: server.url, + fetch: eventFetch ? "platform" : "webview", + error, + }) + }, }) - }, - }) - let yielded = Date.now() - for await (const event of events.stream) { - const directory = event.directory ?? "global" - const payload = event.payload - const k = key(directory, payload) - if (k) { - const i = coalesced.get(k) - if (i !== undefined) { - queue[i] = { directory, payload } - continue + let yielded = Date.now() + for await (const event of events.stream) { + streamErrorLogged = false + const directory = event.directory ?? "global" + const payload = event.payload + const k = key(directory, payload) + if (k) { + const i = coalesced.get(k) + if (i !== undefined) { + queue[i] = { directory, payload } + continue + } + coalesced.set(k, queue.length) + } + queue.push({ directory, payload }) + schedule() + + if (Date.now() - yielded < STREAM_YIELD_MS) continue + yielded = Date.now() + await wait(0) + } + } catch (error) { + if (!streamErrorLogged) { + streamErrorLogged = true + console.error("[global-sdk] event stream failed", { + url: server.url, + fetch: eventFetch ? "platform" : "webview", + error, + }) } - coalesced.set(k, queue.length) } - queue.push({ directory, payload }) - schedule() - if (Date.now() - yielded < STREAM_YIELD_MS) continue - yielded = Date.now() - await new Promise((resolve) => setTimeout(resolve, 0)) + if (abort.signal.aborted) return + await wait(RECONNECT_DELAY_MS) } - })() - .finally(flush) - .catch((error) => { - if (streamErrorLogged) return - streamErrorLogged = true - console.error("[global-sdk] event stream failed", { - url: server.url, - fetch: eventFetch ? "platform" : "webview", - error, - }) - }) + })().finally(flush) onCleanup(() => { abort.abort() From b06afd657d59c2c88394513e3b633060ec6f454b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 13 Feb 2026 10:46:45 +0800 Subject: [PATCH 014/756] ci: remove signpath policy --- .signpath/policies/opencode/test-signing.yml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .signpath/policies/opencode/test-signing.yml diff --git a/.signpath/policies/opencode/test-signing.yml b/.signpath/policies/opencode/test-signing.yml deleted file mode 100644 index 4c9f654cd3..0000000000 --- a/.signpath/policies/opencode/test-signing.yml +++ /dev/null @@ -1,7 +0,0 @@ -github-policies: - runners: - allowed_groups: - - "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt" - build: - disallow_reruns: false - branch_rulesets: From 1608565c808c9136bdc6930a356649bd9824cc69 Mon Sep 17 00:00:00 2001 From: Spoon <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Fri, 13 Feb 2026 05:52:17 +0100 Subject: [PATCH 015/756] feat(hook): add tool.definition hook for plugins to modify tool description and parameters (#4956) --- packages/opencode/src/tool/registry.ts | 10 +++++++++- packages/plugin/src/index.ts | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 5ed5a879b4..9a06cb5993 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -149,9 +149,17 @@ export namespace ToolRegistry { }) .map(async (t) => { using _ = log.time(t.id) + const tool = await t.init({ agent }) + const output = { + description: tool.description, + parameters: tool.parameters, + } + await Plugin.trigger("tool.definition", { toolID: t.id }, output) return { id: t.id, - ...(await t.init({ agent })), + ...tool, + description: output.description, + parameters: output.parameters, } }), ) diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 664f2c9673..bd4ba53049 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -224,4 +224,8 @@ export interface Hooks { input: { sessionID: string; messageID: string; partID: string }, output: { text: string }, ) => Promise + /** + * Modify tool definitions (description and parameters) sent to LLM + */ + "tool.definition"?: (input: { toolID: string }, output: { description: string; parameters: any }) => Promise } From 98aeb60a7f0e00e251ff02c360829a3679d65717 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:20:33 -0600 Subject: [PATCH 016/756] fix: ensure @-ing a dir uses the read tool instead of dead list tool (#13428) --- packages/opencode/src/session/prompt.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 99d44cd850..be813e823f 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -26,7 +26,6 @@ import { ToolRegistry } from "../tool/registry" import { MCP } from "../mcp" import { LSP } from "../lsp" import { ReadTool } from "../tool/read" -import { ListTool } from "../tool/ls" import { FileTime } from "../file/time" import { Flag } from "../flag/flag" import { ulid } from "ulid" @@ -1198,7 +1197,7 @@ export namespace SessionPrompt { } if (part.mime === "application/x-directory") { - const args = { path: filepath } + const args = { filePath: filepath } const listCtx: Tool.Context = { sessionID: input.sessionID, abort: new AbortController().signal, @@ -1209,7 +1208,7 @@ export namespace SessionPrompt { metadata: async () => {}, ask: async () => {}, } - const result = await ListTool.init().then((t) => t.execute(args, listCtx)) + const result = await ReadTool.init().then((t) => t.execute(args, listCtx)) return [ { id: Identifier.ascending("part"), @@ -1217,7 +1216,7 @@ export namespace SessionPrompt { sessionID: input.sessionID, type: "text", synthetic: true, - text: `Called the list tool with the following input: ${JSON.stringify(args)}`, + text: `Called the Read tool with the following input: ${JSON.stringify(args)}`, }, { id: Identifier.ascending("part"), From 1fb6c0b5b356e3816398ba71ac1b01485697bc31 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:24:31 -0600 Subject: [PATCH 017/756] Revert "fix: token substitution in OPENCODE_CONFIG_CONTENT" (#13429) --- packages/opencode/src/config/config.ts | 8 +-- packages/opencode/src/flag/flag.ts | 13 +--- packages/opencode/test/config/config.test.ts | 65 -------------------- 3 files changed, 2 insertions(+), 84 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f4d7a840fe..8f0f583ea3 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -175,14 +175,8 @@ export namespace Config { } // Inline config content overrides all non-managed config sources. - // Route through load() to enable {env:} and {file:} token substitution. - // Use a path within Instance.directory so relative {file:} paths resolve correctly. - // The filename "OPENCODE_CONFIG_CONTENT" appears in error messages for clarity. if (Flag.OPENCODE_CONFIG_CONTENT) { - result = mergeConfigConcatArrays( - result, - await load(Flag.OPENCODE_CONFIG_CONTENT, path.join(Instance.directory, "OPENCODE_CONFIG_CONTENT")), - ) + result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT)) log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 641cb3325b..dfcb88bc51 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -8,7 +8,7 @@ export namespace Flag { export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"] export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"] export declare const OPENCODE_CONFIG_DIR: string | undefined - export declare const OPENCODE_CONFIG_CONTENT: string | undefined + export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"] export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE") export const OPENCODE_DISABLE_PRUNE = truthy("OPENCODE_DISABLE_PRUNE") export const OPENCODE_DISABLE_TERMINAL_TITLE = truthy("OPENCODE_DISABLE_TERMINAL_TITLE") @@ -94,14 +94,3 @@ Object.defineProperty(Flag, "OPENCODE_CLIENT", { enumerable: true, configurable: false, }) - -// Dynamic getter for OPENCODE_CONFIG_CONTENT -// This must be evaluated at access time, not module load time, -// because external tooling may set this env var at runtime -Object.defineProperty(Flag, "OPENCODE_CONFIG_CONTENT", { - get() { - return process.env["OPENCODE_CONFIG_CONTENT"] - }, - enumerable: true, - configurable: false, -}) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 331e05d5a7..91b87f6498 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1800,68 +1800,3 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { } }) }) - -// OPENCODE_CONFIG_CONTENT should support {env:} and {file:} token substitution -// just like file-based config sources do. -describe("OPENCODE_CONFIG_CONTENT token substitution", () => { - test("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", async () => { - const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] - const originalTestVar = process.env["TEST_CONFIG_VAR"] - process.env["TEST_CONFIG_VAR"] = "test_api_key_12345" - process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ - $schema: "https://opencode.ai/config.json", - theme: "{env:TEST_CONFIG_VAR}", - }) - - try { - await using tmp = await tmpdir() - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.theme).toBe("test_api_key_12345") - }, - }) - } finally { - if (originalEnv !== undefined) { - process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv - } else { - delete process.env["OPENCODE_CONFIG_CONTENT"] - } - if (originalTestVar !== undefined) { - process.env["TEST_CONFIG_VAR"] = originalTestVar - } else { - delete process.env["TEST_CONFIG_VAR"] - } - } - }) - - test("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", async () => { - const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] - - try { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "api_key.txt"), "secret_key_from_file") - process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ - $schema: "https://opencode.ai/config.json", - theme: "{file:./api_key.txt}", - }) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.theme).toBe("secret_key_from_file") - }, - }) - } finally { - if (originalEnv !== undefined) { - process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv - } else { - delete process.env["OPENCODE_CONFIG_CONTENT"] - } - } - }) -}) From 34ebe814ddd130a787455dda089facb23538ca20 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 13 Feb 2026 05:51:04 +0000 Subject: [PATCH 018/756] release: v1.1.65 --- bun.lock | 30 +++++++++++++------------- 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/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 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index 3fe8a4ca07..8b58d4e1cd 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -73,7 +73,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -107,7 +107,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -134,7 +134,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -158,7 +158,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -182,7 +182,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -215,7 +215,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -244,7 +244,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -260,7 +260,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.64", + "version": "1.1.65", "bin": { "opencode": "./bin/opencode", }, @@ -366,7 +366,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -386,7 +386,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.64", + "version": "1.1.65", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -397,7 +397,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -410,7 +410,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -452,7 +452,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "zod": "catalog:", }, @@ -463,7 +463,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.64", + "version": "1.1.65", "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 ebd1a4b35b..49ce671b60 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.64", + "version": "1.1.65", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index c5556a4431..3d7ef57851 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.64", + "version": "1.1.65", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 498270b952..0676595c70 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.1.64", + "version": "1.1.65", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index d2117dffb2..265546fc7f 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.64", + "version": "1.1.65", "$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 f632ab92fe..0f4bbb6eca 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.64", + "version": "1.1.65", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index da89d36a88..8e4862b30d 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.64", + "version": "1.1.65", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 31b62e12b1..bd2fac19f7 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.64", + "version": "1.1.65", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 22aca32bae..84ae20633e 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.1.64" +version = "1.1.65" 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.1.64/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.65/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.64/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.65/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.64/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.65/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.1.64/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.65/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.1.64/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.65/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index ae9a6d7b3c..4c10ab05f8 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.64", + "version": "1.1.65", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index f58a3d2fe9..ef4535ca96 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.64", + "version": "1.1.65", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d88d5a7ba3..c373083f58 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.1.64", + "version": "1.1.65", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 13d0b549ba..ff8108b7be 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.1.64", + "version": "1.1.65", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 7f0eaff83f..78a5702228 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.64", + "version": "1.1.65", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 6d20e3dfdc..5dbbb4605a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.64", + "version": "1.1.65", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 078adbe142..f37bb5c1d1 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.64", + "version": "1.1.65", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 6f5fe726f5..7c6698117a 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.1.64", + "version": "1.1.65", "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 30c07d3139..d1980decac 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.1.64", + "version": "1.1.65", "publisher": "sst-dev", "repository": { "type": "git", From 0d90a22f9057dd69dca65ab52450f17d47a8656e Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:56:11 -0600 Subject: [PATCH 019/756] feat: update some ai sdk packages and uuse adaptive reasoning for opus 4.6 on vertex/bedrock/anthropic (#13439) --- bun.lock | 26 ++++++++++++------ packages/opencode/package.json | 6 ++--- packages/opencode/src/provider/transform.ts | 30 +++++++++++++++++++++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index 8b58d4e1cd..4a054c6483 100644 --- a/bun.lock +++ b/bun.lock @@ -268,12 +268,12 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", - "@ai-sdk/amazon-bedrock": "3.0.74", - "@ai-sdk/anthropic": "2.0.58", + "@ai-sdk/amazon-bedrock": "3.0.79", + "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/azure": "2.0.91", "@ai-sdk/cerebras": "1.0.36", "@ai-sdk/cohere": "2.0.22", - "@ai-sdk/deepinfra": "1.0.33", + "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", "@ai-sdk/google": "2.0.52", "@ai-sdk/google-vertex": "3.0.98", @@ -565,7 +565,7 @@ "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q83HE3FBb/HPIvjXsehrHOgCuGHPorSMFt6BYnzIYZy8gNnSqV1OWX4oXVsCAuYPPMtYW/KMK35hmoIFV8QKoQ=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.79", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GfAQUb1GEmdTjLu5Ud1d5sieNHDpwoQdb4S14KmJlA5RsGREUZ1tfSKngFaiClxFtL0xPSZjePhTMV6Z65A7/g=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="], @@ -577,7 +577,7 @@ "@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-lqmINr+1Jy2yGXxnQB6IrC2xMtUY5uK96pyKfqTj1kLlXGatKnJfXF7WTkOGgQrFqIYqpjDz+sPVR3n0KUEUtA=="], - "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hn2y8Q+2iZgGNVJyzPsH8EECECryFMVmxBJrBvBWoi8xcJPRyt0fZP5dOSLyGg3q0oxmPS9M0Eq0NNlKot/bYQ=="], + "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.36", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.33", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LndvRktEgY2IFu4peDJMEXcjhHEEFtM0upLx/J64kCpFHCifalXpK4PPSX3PVndnn0bJzvamO5+fc0z2ooqBZw=="], "@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NiKjvqXI/96e/7SjZGgQH141PBqggsF7fNbjGTv4RgVWayMXp9mj0Ou2NjAUGwwxJwj/qseY0gXiDCYaHWFBkw=="], @@ -4151,7 +4151,9 @@ "@actions/http-client/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], + + "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "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-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw=="], @@ -4163,7 +4165,9 @@ "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], - "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], + "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], + + "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "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-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], @@ -4453,6 +4457,8 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q83HE3FBb/HPIvjXsehrHOgCuGHPorSMFt6BYnzIYZy8gNnSqV1OWX4oXVsCAuYPPMtYW/KMK35hmoIFV8QKoQ=="], + "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="], "ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="], @@ -4575,7 +4581,7 @@ "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="], + "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], "opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], @@ -4995,6 +5001,8 @@ "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw=="], + "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="], "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="], @@ -5099,6 +5107,8 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "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-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ef4535ca96..82d562bb09 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -51,12 +51,12 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", - "@ai-sdk/amazon-bedrock": "3.0.74", - "@ai-sdk/anthropic": "2.0.58", + "@ai-sdk/amazon-bedrock": "3.0.79", + "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/azure": "2.0.91", "@ai-sdk/cerebras": "1.0.36", "@ai-sdk/cohere": "2.0.22", - "@ai-sdk/deepinfra": "1.0.33", + "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", "@ai-sdk/google": "2.0.52", "@ai-sdk/google-vertex": "3.0.98", diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 876a26fce7..8091f731f0 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -458,6 +458,22 @@ export namespace ProviderTransform { // https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic case "@ai-sdk/google-vertex/anthropic": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider + + if (model.api.id.includes("opus-4-6") || model.api.id.includes("opus-4.6")) { + const efforts = ["low", "medium", "high", "max"] + return Object.fromEntries( + efforts.map((effort) => [ + effort, + { + thinking: { + type: "adaptive", + }, + effort, + }, + ]), + ) + } + return { high: { thinking: { @@ -475,6 +491,20 @@ export namespace ProviderTransform { case "@ai-sdk/amazon-bedrock": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock + if (model.api.id.includes("opus-4-6") || model.api.id.includes("opus-4.6")) { + const efforts = ["low", "medium", "high", "max"] + return Object.fromEntries( + efforts.map((effort) => [ + effort, + { + reasoningConfig: { + type: "adaptive", + maxReasoningEffort: effort, + }, + }, + ]), + ) + } // For Anthropic models on Bedrock, use reasoningConfig with budgetTokens if (model.api.id.includes("anthropic")) { return { From 693127d382abed14113f3b7a347851b7a44d74cd Mon Sep 17 00:00:00 2001 From: Rahul Mishra Date: Fri, 13 Feb 2026 12:29:37 +0530 Subject: [PATCH 020/756] feat(cli): add --dir option to run command (#12443) --- packages/opencode/src/cli/cmd/run.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 163a5820d9..0febec3a20 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -274,6 +274,10 @@ export const RunCommand = cmd({ type: "string", describe: "attach to a running opencode server (e.g., http://localhost:4096)", }) + .option("dir", { + type: "string", + describe: "directory to run in, path on remote server if attaching", + }) .option("port", { type: "number", describe: "port for the local server (defaults to random port if no value provided)", @@ -293,6 +297,18 @@ export const RunCommand = cmd({ .map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg)) .join(" ") + const directory = (() => { + if (!args.dir) return undefined + if (args.attach) return args.dir + try { + process.chdir(args.dir) + return process.cwd() + } catch { + UI.error("Failed to change directory to " + args.dir) + process.exit(1) + } + })() + const files: { type: "file"; url: string; filename: string; mime: string }[] = [] if (args.file) { const list = Array.isArray(args.file) ? args.file : [args.file] @@ -582,7 +598,7 @@ export const RunCommand = cmd({ } if (args.attach) { - const sdk = createOpencodeClient({ baseUrl: args.attach }) + const sdk = createOpencodeClient({ baseUrl: args.attach, directory }) return await execute(sdk) } From b8ee88212639ec63f4fe87555b5e87f74643e76b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 13 Feb 2026 07:06:28 +0000 Subject: [PATCH 021/756] 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 70d7378493..c493161ee6 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-XIf7b6yALzH1/MkGGrsmq2DeXIC9vgD9a7D/dxhi6iU=", - "aarch64-linux": "sha256-mKDCs6QhIelWc3E17zOufaSDTovtjO/Xyh3JtlWl01s=", - "aarch64-darwin": "sha256-wC7bbbIyZ62uMxTr9FElTbEBMrfz0S/ndqwZZ3V9EOA=", - "x86_64-darwin": "sha256-/7Nn65m5Zhvzz0TKsG9nWd2v5WDHQNi3UzCfuAR8SLo=" + "x86_64-linux": "sha256-FsFTitxnN2brebZDBRGJB0NWTOVYDa/QcNRH0ip/Gk4=", + "aarch64-linux": "sha256-knSEqEPyonBUfmGZKTq5Om4HikItWbfPdfT7p6iljzs=", + "aarch64-darwin": "sha256-uRgWfuOlLECRCOszm8XhySiWxu9IdDhpSbosPZPAZVI=", + "x86_64-darwin": "sha256-gHuA+Ud9L+XLvKm5Vp5jCXfZWOtunnmX/lB8vczHsG0=" } } From ebb907d646022d2e7bb8effc164e1f09943d64a9 Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:08:13 +0100 Subject: [PATCH 022/756] fix(desktop): performance optimization for showing large diff & files (#13460) --- packages/app/src/pages/session/file-tabs.tsx | 23 +- packages/ui/src/components/code.tsx | 118 +++++++-- packages/ui/src/components/diff.tsx | 38 ++- packages/ui/src/components/session-review.css | 26 ++ packages/ui/src/components/session-review.tsx | 228 +++++++++++------- packages/ui/src/i18n/ar.ts | 5 + packages/ui/src/i18n/br.ts | 5 + packages/ui/src/i18n/bs.ts | 5 + packages/ui/src/i18n/da.ts | 5 + packages/ui/src/i18n/de.ts | 5 + packages/ui/src/i18n/en.ts | 5 + packages/ui/src/i18n/es.ts | 5 + packages/ui/src/i18n/fr.ts | 5 + packages/ui/src/i18n/ja.ts | 5 + packages/ui/src/i18n/ko.ts | 5 + packages/ui/src/i18n/no.ts | 5 + packages/ui/src/i18n/pl.ts | 5 + packages/ui/src/i18n/ru.ts | 5 + packages/ui/src/i18n/th.ts | 5 + packages/ui/src/i18n/zh.ts | 5 + packages/ui/src/i18n/zht.ts | 5 + packages/util/src/encode.ts | 21 ++ 22 files changed, 407 insertions(+), 127 deletions(-) diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 5b3f57dbed..d22fa358b0 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -1,7 +1,7 @@ import { type ValidComponent, createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Dynamic } from "solid-js/web" -import { checksum } from "@opencode-ai/util/encode" +import { sampledChecksum } from "@opencode-ai/util/encode" import { decode64 } from "@/utils/base64" import { showToast } from "@opencode-ai/ui/toast" import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment" @@ -49,7 +49,7 @@ export function FileTabContent(props: { return props.file.get(p) }) const contents = createMemo(() => state()?.content?.content ?? "") - const cacheKey = createMemo(() => checksum(contents())) + const cacheKey = createMemo(() => sampledChecksum(contents())) const isImage = createMemo(() => { const c = state()?.content return c?.encoding === "base64" && c?.mimeType?.startsWith("image/") && c?.mimeType !== "image/svg+xml" @@ -163,11 +163,20 @@ export function FileTabContent(props: { return } + const estimateTop = (range: SelectedLineRange) => { + const line = Math.max(range.start, range.end) + const height = 24 + const offset = 2 + return Math.max(0, (line - 1) * height + offset) + } + + const large = contents().length > 500_000 + const next: Record = {} for (const comment of fileComments()) { const marker = findMarker(root, comment.selection) - if (!marker) continue - next[comment.id] = markerTop(el, marker) + if (marker) next[comment.id] = markerTop(el, marker) + else if (large) next[comment.id] = estimateTop(comment.selection) } const removed = Object.keys(note.positions).filter((id) => next[id] === undefined) @@ -194,12 +203,12 @@ export function FileTabContent(props: { } const marker = findMarker(root, range) - if (!marker) { - setNote("draftTop", undefined) + if (marker) { + setNote("draftTop", markerTop(el, marker)) return } - setNote("draftTop", markerTop(el, marker)) + setNote("draftTop", large ? estimateTop(range) : undefined) } const scheduleComments = () => { diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx index abe0d7ca9e..837cc53376 100644 --- a/packages/ui/src/components/code.tsx +++ b/packages/ui/src/components/code.tsx @@ -1,10 +1,27 @@ -import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs" +import { + DEFAULT_VIRTUAL_FILE_METRICS, + type FileContents, + File, + FileOptions, + LineAnnotation, + type SelectedLineRange, + type VirtualFileMetrics, + VirtualizedFile, + Virtualizer, +} from "@pierre/diffs" import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js" import { Portal } from "solid-js/web" import { createDefaultOptions, styleVariables } from "../pierre" import { getWorkerPool } from "../pierre/worker" import { Icon } from "./icon" +const VIRTUALIZE_BYTES = 500_000 +const codeMetrics = { + ...DEFAULT_VIRTUAL_FILE_METRICS, + lineHeight: 24, + fileGap: 0, +} satisfies Partial + type SelectionSide = "additions" | "deletions" export type CodeProps = FileOptions & { @@ -160,16 +177,28 @@ export function Code(props: CodeProps) { const [findPos, setFindPos] = createSignal<{ top: number; right: number }>({ top: 8, right: 8 }) - const file = createMemo( - () => - new File( - { - ...createDefaultOptions("unified"), - ...others, - }, - getWorkerPool("unified"), - ), - ) + let instance: File | VirtualizedFile | undefined + let virtualizer: Virtualizer | undefined + let virtualRoot: Document | HTMLElement | undefined + + const bytes = createMemo(() => { + const value = local.file.contents as unknown + if (typeof value === "string") return value.length + if (Array.isArray(value)) { + return value.reduce( + (acc, part) => acc + (typeof part === "string" ? part.length + 1 : String(part).length + 1), + 0, + ) + } + if (value == null) return 0 + return String(value).length + }) + const virtual = createMemo(() => bytes() > VIRTUALIZE_BYTES) + + const options = createMemo(() => ({ + ...createDefaultOptions("unified"), + ...others, + })) const getRoot = () => { const host = container.querySelector("diffs-container") @@ -577,6 +606,14 @@ export function Code(props: CodeProps) { } const applySelection = (range: SelectedLineRange | null) => { + const current = instance + if (!current) return false + + if (virtual()) { + current.setSelectedLines(range) + return true + } + const root = getRoot() if (!root) return false @@ -584,7 +621,7 @@ export function Code(props: CodeProps) { if (root.querySelectorAll("[data-line]").length < lines) return false if (!range) { - file().setSelectedLines(null) + current.setSelectedLines(null) return true } @@ -592,12 +629,12 @@ export function Code(props: CodeProps) { const end = Math.max(range.start, range.end) if (start < 1 || end > lines) { - file().setSelectedLines(null) + current.setSelectedLines(null) return true } if (!root.querySelector(`[data-line="${start}"]`) || !root.querySelector(`[data-line="${end}"]`)) { - file().setSelectedLines(null) + current.setSelectedLines(null) return true } @@ -608,7 +645,7 @@ export function Code(props: CodeProps) { return { start: range.start, end: range.end } })() - file().setSelectedLines(normalized) + current.setSelectedLines(normalized) return true } @@ -619,9 +656,12 @@ export function Code(props: CodeProps) { const token = renderToken - const lines = lineCount() + const lines = virtual() ? undefined : lineCount() - const isReady = (root: ShadowRoot) => root.querySelectorAll("[data-line]").length >= lines + const isReady = (root: ShadowRoot) => + virtual() + ? root.querySelector("[data-line]") != null + : root.querySelectorAll("[data-line]").length >= (lines ?? 0) const notify = () => { if (token !== renderToken) return @@ -844,20 +884,41 @@ export function Code(props: CodeProps) { } createEffect(() => { - const current = file() + const opts = options() + const workerPool = getWorkerPool("unified") + const isVirtual = virtual() - onCleanup(() => { - current.cleanUp() - }) - }) - - createEffect(() => { observer?.disconnect() observer = undefined + instance?.cleanUp() + instance = undefined + + if (!isVirtual && virtualizer) { + virtualizer.cleanUp() + virtualizer = undefined + virtualRoot = undefined + } + + const v = (() => { + if (!isVirtual) return + if (typeof document === "undefined") return + + const root = getScrollParent(wrapper) ?? document + if (virtualizer && virtualRoot === root) return virtualizer + + virtualizer?.cleanUp() + virtualizer = new Virtualizer() + virtualRoot = root + virtualizer.setup(root, root instanceof Document ? undefined : wrapper) + return virtualizer + })() + + instance = isVirtual && v ? new VirtualizedFile(opts, v, codeMetrics, workerPool) : new File(opts, workerPool) + container.innerHTML = "" const value = text() - file().render({ + instance.render({ file: typeof local.file.contents === "string" ? local.file : { ...local.file, contents: value }, lineAnnotations: local.annotations, containerWrapper: container, @@ -910,6 +971,13 @@ export function Code(props: CodeProps) { onCleanup(() => { observer?.disconnect() + instance?.cleanUp() + instance = undefined + + virtualizer?.cleanUp() + virtualizer = undefined + virtualRoot = undefined + clearOverlayScroll() clearOverlay() if (findCurrent === host) { diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx index 0966db75e0..0002232b01 100644 --- a/packages/ui/src/components/diff.tsx +++ b/packages/ui/src/components/diff.tsx @@ -1,5 +1,5 @@ -import { checksum } from "@opencode-ai/util/encode" -import { FileDiff, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs" +import { sampledChecksum } from "@opencode-ai/util/encode" +import { FileDiff, type FileDiffOptions, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs" import { createMediaQuery } from "@solid-primitives/media" import { createEffect, createMemo, createSignal, onCleanup, splitProps } from "solid-js" import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre" @@ -78,14 +78,29 @@ export function Diff(props: DiffProps) { const mobile = createMediaQuery("(max-width: 640px)") - const options = createMemo(() => { - const opts = { + const large = createMemo(() => { + const before = typeof local.before?.contents === "string" ? local.before.contents : "" + const after = typeof local.after?.contents === "string" ? local.after.contents : "" + return Math.max(before.length, after.length) > 500_000 + }) + + const largeOptions = { + lineDiffType: "none", + maxLineDiffLength: 0, + tokenizeMaxLineLength: 1, + } satisfies Pick, "lineDiffType" | "maxLineDiffLength" | "tokenizeMaxLineLength"> + + const options = createMemo>(() => { + const base = { ...createDefaultOptions(props.diffStyle), ...others, } - if (!mobile()) return opts + + const perf = large() ? { ...base, ...largeOptions } : base + if (!mobile()) return perf + return { - ...opts, + ...perf, disableLineNumbers: true, } }) @@ -528,12 +543,17 @@ export function Diff(props: DiffProps) { createEffect(() => { const opts = options() - const workerPool = getWorkerPool(props.diffStyle) + const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle) const virtualizer = getVirtualizer() const annotations = local.annotations const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : "" const afterContents = typeof local.after?.contents === "string" ? local.after.contents : "" + const cacheKey = (contents: string) => { + if (!large()) return sampledChecksum(contents, contents.length) + return sampledChecksum(contents) + } + instance?.cleanUp() instance = virtualizer ? new VirtualizedFileDiff(opts, virtualizer, virtualMetrics, workerPool) @@ -545,12 +565,12 @@ export function Diff(props: DiffProps) { oldFile: { ...local.before, contents: beforeContents, - cacheKey: checksum(beforeContents), + cacheKey: cacheKey(beforeContents), }, newFile: { ...local.after, contents: afterContents, - cacheKey: checksum(afterContents), + cacheKey: cacheKey(afterContents), }, lineAnnotations: annotations, containerWrapper: container, diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index 30bfe3b712..46473b75e5 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -222,4 +222,30 @@ --line-comment-popover-z: 30; --line-comment-open-z: 6; } + + [data-slot="session-review-large-diff"] { + padding: 12px; + background: var(--background-stronger); + } + + [data-slot="session-review-large-diff-title"] { + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-medium); + color: var(--text-strong); + margin-bottom: 4px; + } + + [data-slot="session-review-large-diff-meta"] { + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + color: var(--text-weak); + word-break: break-word; + } + + [data-slot="session-review-large-diff-actions"] { + display: flex; + gap: 8px; + margin-top: 10px; + } } diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index fe2475548e..5f1e6b1aba 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -17,6 +17,26 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { type SelectedLineRange } from "@pierre/diffs" import { Dynamic } from "solid-js/web" +const MAX_DIFF_LINES = 20_000 +const MAX_DIFF_BYTES = 2_000_000 + +function linesOver(text: string, max: number) { + let lines = 1 + for (let i = 0; i < text.length; i++) { + if (text.charCodeAt(i) !== 10) continue + lines++ + if (lines > max) return true + } + return lines > max +} + +function formatBytes(bytes: number) { + if (!Number.isFinite(bytes) || bytes <= 0) return "0 B" + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${Math.round((bytes / 1024) * 10) / 10} KB` + return `${Math.round((bytes / (1024 * 1024)) * 10) / 10} MB` +} + export type SessionReviewDiffStyle = "unified" | "split" export type SessionReviewComment = { @@ -326,12 +346,28 @@ export const SessionReview = (props: SessionReviewProps) => { {(diff) => { let wrapper: HTMLDivElement | undefined + const expanded = createMemo(() => open().includes(diff.file)) + const [force, setForce] = createSignal(false) + const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) const commentedLines = createMemo(() => comments().map((c) => c.selection)) const beforeText = () => (typeof diff.before === "string" ? diff.before : "") const afterText = () => (typeof diff.after === "string" ? diff.after : "") + const tooLarge = createMemo(() => { + if (!expanded()) return false + if (force()) return false + if (isImageFile(diff.file)) return false + + const before = beforeText() + const after = afterText() + + if (before.length > MAX_DIFF_BYTES || after.length > MAX_DIFF_BYTES) return true + if (linesOver(before, MAX_DIFF_LINES) || linesOver(after, MAX_DIFF_LINES)) return true + return false + }) + const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) const isDeleted = () => diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) @@ -571,94 +607,114 @@ export const SessionReview = (props: SessionReviewProps) => { scheduleAnchors() }} > - - -
- {diff.file} -
-
- -
- - {i18n.t("ui.sessionReview.change.removed")} - -
-
- -
- - {imageStatus() === "loading" ? "Loading..." : "Image"} - -
-
- - { - props.onDiffRendered?.() - scheduleAnchors() - }} - enableLineSelection={props.onLineComment != null} - onLineSelected={handleLineSelected} - onLineSelectionEnd={handleLineSelectionEnd} - selectedLines={selectedLines()} - commentedLines={commentedLines()} - before={{ - name: diff.file!, - contents: typeof diff.before === "string" ? diff.before : "", - }} - after={{ - name: diff.file!, - contents: typeof diff.after === "string" ? diff.after : "", - }} - /> - -
- - - {(comment) => ( - setSelection({ file: comment.file, range: comment.selection })} - onClick={() => { - if (isCommentOpen(comment)) { - setOpened(null) - return - } - - openComment(comment) - }} - open={isCommentOpen(comment)} - comment={comment.comment} - selection={selectionLabel(comment.selection)} - /> - )} - - - - {(range) => ( - - setCommenting(null)} - onSubmit={(comment) => { - props.onLineComment?.({ - file: diff.file, - selection: range(), - comment, - preview: selectionPreview(diff, range()), - }) - setCommenting(null) + + + +
+ {diff.file} +
+
+ +
+ + {i18n.t("ui.sessionReview.change.removed")} + +
+
+ +
+ + {imageStatus() === "loading" + ? i18n.t("ui.sessionReview.image.loading") + : i18n.t("ui.sessionReview.image.placeholder")} + +
+
+ +
+
+ {i18n.t("ui.sessionReview.largeDiff.title")} +
+
+ Limit: {MAX_DIFF_LINES.toLocaleString()} lines / {formatBytes(MAX_DIFF_BYTES)}. + Current: {formatBytes(Math.max(beforeText().length, afterText().length))}. +
+
+ +
+
+
+ + { + props.onDiffRendered?.() + scheduleAnchors() + }} + enableLineSelection={props.onLineComment != null} + onLineSelected={handleLineSelected} + onLineSelectionEnd={handleLineSelectionEnd} + selectedLines={selectedLines()} + commentedLines={commentedLines()} + before={{ + name: diff.file!, + contents: typeof diff.before === "string" ? diff.before : "", }} + after={{ + name: diff.file!, + contents: typeof diff.after === "string" ? diff.after : "", + }} + /> + +
+ + + {(comment) => ( + setSelection({ file: comment.file, range: comment.selection })} + onClick={() => { + if (isCommentOpen(comment)) { + setOpened(null) + return + } + + openComment(comment) + }} + open={isCommentOpen(comment)} + comment={comment.comment} + selection={selectionLabel(comment.selection)} /> -
- )} + )} +
+ + + {(range) => ( + + setCommenting(null)} + onSubmit={(comment) => { + props.onLineComment?.({ + file: diff.file, + selection: range(), + comment, + preview: selectionPreview(diff, range()), + }) + setCommenting(null) + }} + /> + + )} +
diff --git a/packages/ui/src/i18n/ar.ts b/packages/ui/src/i18n/ar.ts index 7ee17e2e01..9a6c8dcbd0 100644 --- a/packages/ui/src/i18n/ar.ts +++ b/packages/ui/src/i18n/ar.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "مضاف", "ui.sessionReview.change.removed": "محذوف", "ui.sessionReview.change.modified": "معدل", + "ui.sessionReview.image.loading": "جار التحميل...", + "ui.sessionReview.image.placeholder": "صورة", + "ui.sessionReview.largeDiff.title": "Diff كبير جدا لعرضه", + "ui.sessionReview.largeDiff.meta": "الحد: {{lines}} سطر / {{limit}}. الحالي: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "اعرض على أي حال", "ui.lineComment.label.prefix": "تعليق على ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/br.ts b/packages/ui/src/i18n/br.ts index 6d7449d845..148b0ae174 100644 --- a/packages/ui/src/i18n/br.ts +++ b/packages/ui/src/i18n/br.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "Adicionado", "ui.sessionReview.change.removed": "Removido", "ui.sessionReview.change.modified": "Modificado", + "ui.sessionReview.image.loading": "Carregando...", + "ui.sessionReview.image.placeholder": "Imagem", + "ui.sessionReview.largeDiff.title": "Diff grande demais para renderizar", + "ui.sessionReview.largeDiff.meta": "Limite: {{lines}} linhas / {{limit}}. Atual: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Renderizar mesmo assim", "ui.lineComment.label.prefix": "Comentar em ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/bs.ts b/packages/ui/src/i18n/bs.ts index 24e4c12068..7614af087f 100644 --- a/packages/ui/src/i18n/bs.ts +++ b/packages/ui/src/i18n/bs.ts @@ -12,6 +12,11 @@ export const dict = { "ui.sessionReview.change.added": "Dodano", "ui.sessionReview.change.removed": "Uklonjeno", "ui.sessionReview.change.modified": "Izmijenjeno", + "ui.sessionReview.image.loading": "Učitavanje...", + "ui.sessionReview.image.placeholder": "Slika", + "ui.sessionReview.largeDiff.title": "Diff je prevelik za prikaz", + "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} linija / {{limit}}. Trenutno: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Prikaži svejedno", "ui.lineComment.label.prefix": "Komentar na ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/da.ts b/packages/ui/src/i18n/da.ts index 218f3b26a4..2f49a94344 100644 --- a/packages/ui/src/i18n/da.ts +++ b/packages/ui/src/i18n/da.ts @@ -9,6 +9,11 @@ export const dict = { "ui.sessionReview.change.added": "Tilføjet", "ui.sessionReview.change.removed": "Fjernet", "ui.sessionReview.change.modified": "Ændret", + "ui.sessionReview.image.loading": "Indlæser...", + "ui.sessionReview.image.placeholder": "Billede", + "ui.sessionReview.largeDiff.title": "Diff er for stor til at blive vist", + "ui.sessionReview.largeDiff.meta": "Grænse: {{lines}} linjer / {{limit}}. Nuværende: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Vis alligevel", "ui.lineComment.label.prefix": "Kommenter på ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Kommenterer på ", diff --git a/packages/ui/src/i18n/de.ts b/packages/ui/src/i18n/de.ts index 921a12c996..44090b7bdb 100644 --- a/packages/ui/src/i18n/de.ts +++ b/packages/ui/src/i18n/de.ts @@ -13,6 +13,11 @@ export const dict = { "ui.sessionReview.change.added": "Hinzugefügt", "ui.sessionReview.change.removed": "Entfernt", "ui.sessionReview.change.modified": "Geändert", + "ui.sessionReview.image.loading": "Wird geladen...", + "ui.sessionReview.image.placeholder": "Bild", + "ui.sessionReview.largeDiff.title": "Diff zu groß zum Rendern", + "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} Zeilen / {{limit}}. Aktuell: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Trotzdem rendern", "ui.lineComment.label.prefix": "Kommentar zu ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Kommentiere ", diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 631bc660a6..9b6ab0bd6d 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "Added", "ui.sessionReview.change.removed": "Removed", "ui.sessionReview.change.modified": "Modified", + "ui.sessionReview.image.loading": "Loading...", + "ui.sessionReview.image.placeholder": "Image", + "ui.sessionReview.largeDiff.title": "Diff too large to render", + "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} lines / {{limit}}. Current: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Render anyway", "ui.lineComment.label.prefix": "Comment on ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/es.ts b/packages/ui/src/i18n/es.ts index 4fd921b606..c2f8ac3b9d 100644 --- a/packages/ui/src/i18n/es.ts +++ b/packages/ui/src/i18n/es.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "Añadido", "ui.sessionReview.change.removed": "Eliminado", "ui.sessionReview.change.modified": "Modificado", + "ui.sessionReview.image.loading": "Cargando...", + "ui.sessionReview.image.placeholder": "Imagen", + "ui.sessionReview.largeDiff.title": "Diff demasiado grande para renderizar", + "ui.sessionReview.largeDiff.meta": "Límite: {{lines}} líneas / {{limit}}. Actual: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Renderizar de todos modos", "ui.lineComment.label.prefix": "Comentar en ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts index 537d01bba9..679d56fa76 100644 --- a/packages/ui/src/i18n/fr.ts +++ b/packages/ui/src/i18n/fr.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "Ajouté", "ui.sessionReview.change.removed": "Supprimé", "ui.sessionReview.change.modified": "Modifié", + "ui.sessionReview.image.loading": "Chargement...", + "ui.sessionReview.image.placeholder": "Image", + "ui.sessionReview.largeDiff.title": "Diff trop volumineux pour être affiché", + "ui.sessionReview.largeDiff.meta": "Limite : {{lines}} lignes / {{limit}}. Actuel : {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Afficher quand même", "ui.lineComment.label.prefix": "Commenter sur ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts index 6086070bdb..bf85807d00 100644 --- a/packages/ui/src/i18n/ja.ts +++ b/packages/ui/src/i18n/ja.ts @@ -9,6 +9,11 @@ export const dict = { "ui.sessionReview.change.added": "追加", "ui.sessionReview.change.removed": "削除", "ui.sessionReview.change.modified": "変更", + "ui.sessionReview.image.loading": "読み込み中...", + "ui.sessionReview.image.placeholder": "画像", + "ui.sessionReview.largeDiff.title": "差分が大きすぎて表示できません", + "ui.sessionReview.largeDiff.meta": "上限: {{lines}} 行 / {{limit}}。現在: {{current}}。", + "ui.sessionReview.largeDiff.renderAnyway": "それでも表示する", "ui.lineComment.label.prefix": "", "ui.lineComment.label.suffix": "へのコメント", "ui.lineComment.editorLabel.prefix": "", diff --git a/packages/ui/src/i18n/ko.ts b/packages/ui/src/i18n/ko.ts index fd394dbb7b..aba793a11b 100644 --- a/packages/ui/src/i18n/ko.ts +++ b/packages/ui/src/i18n/ko.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "추가됨", "ui.sessionReview.change.removed": "삭제됨", "ui.sessionReview.change.modified": "수정됨", + "ui.sessionReview.image.loading": "로딩 중...", + "ui.sessionReview.image.placeholder": "이미지", + "ui.sessionReview.largeDiff.title": "차이가 너무 커서 렌더링할 수 없습니다", + "ui.sessionReview.largeDiff.meta": "제한: {{lines}}줄 / {{limit}}. 현재: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "그래도 렌더링", "ui.lineComment.label.prefix": "", "ui.lineComment.label.suffix": "에 댓글 달기", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index dcb353614d..7982b3ac75 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -11,6 +11,11 @@ export const dict: Record = { "ui.sessionReview.change.added": "Lagt til", "ui.sessionReview.change.removed": "Fjernet", "ui.sessionReview.change.modified": "Endret", + "ui.sessionReview.image.loading": "Laster...", + "ui.sessionReview.image.placeholder": "Bilde", + "ui.sessionReview.largeDiff.title": "Diff er for stor til å gjengi", + "ui.sessionReview.largeDiff.meta": "Grense: {{lines}} linjer / {{limit}}. Nåværende: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Gjengi likevel", "ui.lineComment.label.prefix": "Kommenter på ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts index fb10debbb9..2489ac7f2e 100644 --- a/packages/ui/src/i18n/pl.ts +++ b/packages/ui/src/i18n/pl.ts @@ -9,6 +9,11 @@ export const dict = { "ui.sessionReview.change.added": "Dodano", "ui.sessionReview.change.removed": "Usunięto", "ui.sessionReview.change.modified": "Zmodyfikowano", + "ui.sessionReview.image.loading": "Ładowanie...", + "ui.sessionReview.image.placeholder": "Obraz", + "ui.sessionReview.largeDiff.title": "Diff jest zbyt duży, aby go wyrenderować", + "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} linii / {{limit}}. Obecnie: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Renderuj mimo to", "ui.lineComment.label.prefix": "Komentarz do ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Komentowanie: ", diff --git a/packages/ui/src/i18n/ru.ts b/packages/ui/src/i18n/ru.ts index 417fe0ce8b..8e6bb678f2 100644 --- a/packages/ui/src/i18n/ru.ts +++ b/packages/ui/src/i18n/ru.ts @@ -9,6 +9,11 @@ export const dict = { "ui.sessionReview.change.added": "Добавлено", "ui.sessionReview.change.removed": "Удалено", "ui.sessionReview.change.modified": "Изменено", + "ui.sessionReview.image.loading": "Загрузка...", + "ui.sessionReview.image.placeholder": "Изображение", + "ui.sessionReview.largeDiff.title": "Diff слишком большой для отображения", + "ui.sessionReview.largeDiff.meta": "Лимит: {{lines}} строк / {{limit}}. Текущий: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "Отобразить всё равно", "ui.lineComment.label.prefix": "Комментарий к ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Комментирование: ", diff --git a/packages/ui/src/i18n/th.ts b/packages/ui/src/i18n/th.ts index 68bb0d733d..b036eca2e8 100644 --- a/packages/ui/src/i18n/th.ts +++ b/packages/ui/src/i18n/th.ts @@ -8,6 +8,11 @@ export const dict = { "ui.sessionReview.change.added": "เพิ่ม", "ui.sessionReview.change.removed": "ลบ", "ui.sessionReview.change.modified": "แก้ไข", + "ui.sessionReview.image.loading": "กำลังโหลด...", + "ui.sessionReview.image.placeholder": "รูปภาพ", + "ui.sessionReview.largeDiff.title": "Diff มีขนาดใหญ่เกินไปจนไม่สามารถแสดงผลได้", + "ui.sessionReview.largeDiff.meta": "ขีดจำกัด: {{lines}} บรรทัด / {{limit}}. ปัจจุบัน: {{current}}.", + "ui.sessionReview.largeDiff.renderAnyway": "แสดงผลต่อไป", "ui.lineComment.label.prefix": "แสดงความคิดเห็นบน ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 53beeb1e4f..dcb8062a33 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -12,6 +12,11 @@ export const dict = { "ui.sessionReview.change.added": "已添加", "ui.sessionReview.change.removed": "已移除", "ui.sessionReview.change.modified": "已修改", + "ui.sessionReview.image.loading": "加载中...", + "ui.sessionReview.image.placeholder": "图片", + "ui.sessionReview.largeDiff.title": "差异过大,无法渲染", + "ui.sessionReview.largeDiff.meta": "限制:{{lines}} 行 / {{limit}}。当前:{{current}}。", + "ui.sessionReview.largeDiff.renderAnyway": "仍然渲染", "ui.lineComment.label.prefix": "评论 ", "ui.lineComment.label.suffix": "", diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts index 1449b0530a..271a6ded32 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -12,6 +12,11 @@ export const dict = { "ui.sessionReview.change.added": "已新增", "ui.sessionReview.change.removed": "已移除", "ui.sessionReview.change.modified": "已修改", + "ui.sessionReview.image.loading": "載入中...", + "ui.sessionReview.image.placeholder": "圖片", + "ui.sessionReview.largeDiff.title": "差異過大,無法渲染", + "ui.sessionReview.largeDiff.meta": "限制:{{lines}} 行 / {{limit}}。目前:{{current}}。", + "ui.sessionReview.largeDiff.renderAnyway": "仍然渲染", "ui.lineComment.label.prefix": "評論 ", "ui.lineComment.label.suffix": "", diff --git a/packages/util/src/encode.ts b/packages/util/src/encode.ts index 138cf16086..e4c6e70acb 100644 --- a/packages/util/src/encode.ts +++ b/packages/util/src/encode.ts @@ -28,3 +28,24 @@ export function checksum(content: string): string | undefined { } return (hash >>> 0).toString(36) } + +export function sampledChecksum(content: string, limit = 500_000): string | undefined { + if (!content) return undefined + if (content.length <= limit) return checksum(content) + + const size = 4096 + const points = [ + 0, + Math.floor(content.length * 0.25), + Math.floor(content.length * 0.5), + Math.floor(content.length * 0.75), + content.length - size, + ] + const hashes = points + .map((point) => { + const start = Math.max(0, Math.min(content.length - size, point - Math.floor(size / 2))) + return checksum(content.slice(start, start + size)) ?? "" + }) + .join(":") + return `${content.length}:${hashes}` +} From 9f20e0d14b1d7db2167b2a81523a2521fe1c3b73 Mon Sep 17 00:00:00 2001 From: Jun <87404676+Seungjun0906@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:12:28 +0900 Subject: [PATCH 023/756] fix(web): sync docs locale cookie on alias redirects (#13109) --- packages/app/src/context/language.tsx | 5 +++++ packages/web/src/middleware.ts | 31 +++++++++++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx index a5d894e62e..b21ec6d3cc 100644 --- a/packages/app/src/context/language.tsx +++ b/packages/app/src/context/language.tsx @@ -57,6 +57,10 @@ export type Locale = type RawDictionary = typeof en & typeof uiEn type Dictionary = i18n.Flatten +function cookie(locale: Locale) { + return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` +} + const LOCALES: readonly Locale[] = [ "en", "zh", @@ -199,6 +203,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont createEffect(() => { if (typeof document !== "object") return document.documentElement.lang = locale() + document.cookie = cookie(locale()) }) return { diff --git a/packages/web/src/middleware.ts b/packages/web/src/middleware.ts index 97d085dfbf..cf9f97b0b1 100644 --- a/packages/web/src/middleware.ts +++ b/packages/web/src/middleware.ts @@ -12,7 +12,28 @@ function docsAlias(pathname: string) { const next = locale === "root" ? `/docs${tail}` : `/docs/${locale}${tail}` if (next === pathname) return null - return next + return { + path: next, + locale, + } +} + +function cookie(locale: string) { + const value = locale === "root" ? "en" : locale + return `oc_locale=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax` +} + +function redirect(url: URL, path: string, locale?: string) { + const next = new URL(url.toString()) + next.pathname = path + const headers = new Headers({ + Location: next.toString(), + }) + if (locale) headers.set("Set-Cookie", cookie(locale)) + return new Response(null, { + status: 302, + headers, + }) } function localeFromCookie(header: string | null) { @@ -59,9 +80,7 @@ function localeFromAcceptLanguage(header: string | null) { export const onRequest = defineMiddleware((ctx, next) => { const alias = docsAlias(ctx.url.pathname) if (alias) { - const url = new URL(ctx.request.url) - url.pathname = alias - return ctx.redirect(url.toString(), 302) + return redirect(ctx.url, alias.path, alias.locale) } if (ctx.url.pathname !== "/docs" && ctx.url.pathname !== "/docs/") return next() @@ -71,7 +90,5 @@ export const onRequest = defineMiddleware((ctx, next) => { localeFromAcceptLanguage(ctx.request.headers.get("accept-language")) if (!locale || locale === "root") return next() - const url = new URL(ctx.request.url) - url.pathname = `/docs/${locale}/` - return ctx.redirect(url.toString(), 302) + return redirect(ctx.url, `/docs/${locale}/`) }) From ebe5a2b74a564dd92677f2cdaa8d21280aedf7fa Mon Sep 17 00:00:00 2001 From: Chris Yang <18487241+ysm-dev@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:16:14 +0900 Subject: [PATCH 024/756] fix(app): remount SDK/sync tree when server URL changes (#13437) --- packages/app/src/app.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 3032a795f8..1121c2e955 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,5 +1,5 @@ import "@/index.css" -import { ErrorBoundary, Suspense, lazy, type JSX, type ParentProps } from "solid-js" +import { ErrorBoundary, Show, Suspense, lazy, type JSX, type ParentProps } from "solid-js" import { Router, Route, Navigate } from "@solidjs/router" import { MetaProvider } from "@solidjs/meta" import { Font } from "@opencode-ai/ui/font" @@ -156,8 +156,11 @@ export function AppBaseProviders(props: ParentProps) { function ServerKey(props: ParentProps) { const server = useServer() - if (!server.url) return null - return props.children + return ( + + {props.children} + + ) } export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) { From b1764b2ffdba86c70c6f2777d1342ad87ac6ec41 Mon Sep 17 00:00:00 2001 From: Annopick Date: Fri, 13 Feb 2026 19:18:47 +0800 Subject: [PATCH 025/756] docs: Fix zh-cn translation mistake in tools.mdx (#13407) --- packages/web/src/content/docs/zh-cn/tools.mdx | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/packages/web/src/content/docs/zh-cn/tools.mdx b/packages/web/src/content/docs/zh-cn/tools.mdx index 1be9d66901..a1a97a3ed7 100644 --- a/packages/web/src/content/docs/zh-cn/tools.mdx +++ b/packages/web/src/content/docs/zh-cn/tools.mdx @@ -24,7 +24,7 @@ Tools allow the LLM to perform actions in your codebase. opencode comes with a s } ``` -您还可以使用万用字元同时控制多个工具。例如,要求 MCP 服务器批准所有工具: +您还可以使用通配符同时控制多个工具。例如,要求 MCP 服务器批准所有工具: ```json title="opencode.json" { @@ -39,15 +39,15 @@ Tools allow the LLM to perform actions in your codebase. opencode comes with a s --- -## 內建 +## 內建工具 以下是 opencode 中可用的所有内置工具。 --- -### 巴什 +### Bash -在专案环境中执行shell命令。 +在专项任务环境中执行shell命令。 ```json title="opencode.json" {4} { @@ -58,13 +58,13 @@ Tools allow the LLM to perform actions in your codebase. opencode comes with a s } ``` -This tool allows the LLM to run terminal commands like `npm install`, `git status`, or any other shell command. +这个工具允许 LLM 运行终端命令,例如:`npm install`, `git status`,或者其他任何终端命令。 --- -### 編輯 +### 编辑 -使用精確的字符串替換修改現有文件。 +使用精确的字符串替换来修改现有文件。 ```json title="opencode.json" {4} { @@ -75,13 +75,13 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -该工具取消替换精确的文字来匹配对文件执行精确编辑。这是 LLM 修改代码的主要方式。 +该工具通过替换完全匹配的文本来对文件进行精确编辑。这是 LLM 修改代码的主要方式。 --- -### 寫 +### 写入 -建立新文件或覆盖現有文件。 +创建新文件或覆盖现有文件。 ```json title="opencode.json" {4} { @@ -92,17 +92,17 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -使用它允许 LLM 创建新文件。如果现有文件已经存在,将会覆盖它们。 +使用此功能可允许 LLM 创建新文件。如果文件已存在,则会覆盖现有文件。 :::note -`write`工具由`edit`许可权控制,该许可权主题所有文件修改(`edit`、`write`、`patch`、`multiedit`)。 +`写入`工具由`编辑`权限控制,涵盖所有文件修改(`编辑`、`写入`、`修补`、`多重编辑`)。 ::: --- -### 讀 +### 读取 -從程式碼庫中讀取文件內容。 +读取代码库中的文件内容。 ```json title="opencode.json" {4} { @@ -113,13 +113,13 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -该工具讀取文件并返回其內容。它支持讀取大文件的特定行范围。 +该工具读取文件并返回其内容。它支持读取大型文件中的特定行范围。 --- ### grep -使用正規表示式搜索文件內容。 +使用正则表达式搜索文件内容。 ```json title="opencode.json" {4} { @@ -130,13 +130,13 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -在您的程式碼庫中快速進行內容搜索。支持完整的正規表示式語法和文件模式过濾。 +快速搜索代码库中的内容。支持完整的正则表达式语法和文件模式过滤。 --- -### 全域性 +### 通配符 -通过模式匹配查询文件。 +通过模式匹配查找文件。 ```json title="opencode.json" {4} { @@ -147,13 +147,13 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -使用 `**/*.js` 或 `src/**/*.ts` 等全域性模式搜索档案。返回按时间排序的匹配档案路径修改。 +使用类似 **/*.js 或 src/**/*.ts 的通配符模式搜索文件。返回按修改时间排序的匹配文件路径。 --- -### 列表 +### 罗列 -列出給定路徑中的文件和目录。 +列出给定路径下的文件和目录。 ```json title="opencode.json" {4} { @@ -164,16 +164,16 @@ This tool allows the LLM to run terminal commands like `npm install`, `git statu } ``` -该工具列出目录內容。它接受全域性模式來过濾結果。 +此工具用于列出目录内容。它接受通配符模式来筛选结果。 --- ### lsp(实验性) -与您配置的LSP服务器交互,通知计划码智慧功能,例如定义、引用、悬停资讯和呼叫层次结构。 +与已配置的 LSP 服务器交互,以获取代码智能功能,例如定义、引用、悬停信息和调用层次结构。 :::note -This tool is only available when `OPENCODE_EXPERIMENTAL_LSP_TOOL=true` (or `OPENCODE_EXPERIMENTAL=true`). +只有当 OPENCODE_EXPERIMENTAL_LSP_TOOL=true(或 OPENCODE_EXPERIMENTAL=true)时,此工具才可用。 ::: ```json title="opencode.json" {4} @@ -187,13 +187,13 @@ This tool is only available when `OPENCODE_EXPERIMENTAL_LSP_TOOL=true` (or `OPEN 支持的操作包括 `goToDefinition`、`findReferences`、`hover`、`documentSymbol`、`workspaceSymbol`、`goToImplementation`、`prepareCallHierarchy`、`incomingCalls` 和 `outgoingCalls`。 -To configure which LSP servers are available for your project, see [LSP Servers](/docs/lsp). +要配置哪些 LSP 服务器可用于您的项目,请参阅 [LSP Servers](/docs/lsp). --- -### 修補 +### 修补 -对文件应用補丁。 +对文件应用补丁。 ```json title="opencode.json" {4} { @@ -204,17 +204,17 @@ To configure which LSP servers are available for your project, see [LSP Servers] } ``` -该工具将補丁文件应用到您的程式碼庫。对于应用來自各種來源的差異和補丁很有帮助。 +此工具可将补丁文件应用到您的代码库。它可用于应用来自各种来源的差异和补丁。 :::note -`patch`工具由`edit`许可权控制,该许可权主题所有文件修改(`edit`、`write`、`patch`、`multiedit`)。 +`修补`工具由`编辑`权限控制,涵盖所有文件修改(`编辑`、`写入`、`修补`、`多重编辑`)。 ::: --- ### 技能 -加载[skill](/docs/skills)(`SKILL.md` 档案)并在对话中返回其内容。 +加载[技能](/docs/skills)(`SKILL.md` 文件)并在对话中返回其内容。 ```json title="opencode.json" {4} { @@ -227,9 +227,9 @@ To configure which LSP servers are available for your project, see [LSP Servers] --- -### 待辦寫入 +### 写入待办 -在編碼会话期間管理待辦事項列表。 +在编码会话过程中管理待办事项列表。 ```json title="opencode.json" {4} { @@ -240,17 +240,17 @@ To configure which LSP servers are available for your project, see [LSP Servers] } ``` -建立和更新任务列表以跟踪复杂操作期间的详细信息。LLM 使用它来组织多步骤任务。 +创建和更新任务列表,以跟踪复杂操作的进度。LLM 利用此功能来组织多步骤任务。 :::note -默认情况下,子代理取消此工具,但您可以手动启用它。 [了解更多](/docs/agents/#permissions) +此工具默认情况下对子代理禁用,但您可以手动启用它。 [了解更多](/docs/agents/#permissions) ::: --- -### 託多雷德 +### 读取待办 -閱讀現有的待辦事項列表。 +阅读现有的待办事项清单。 ```json title="opencode.json" {4} { @@ -261,17 +261,17 @@ To configure which LSP servers are available for your project, see [LSP Servers] } ``` -读取当前完成待办事项列表状态。由 LLM 用于跟踪哪些任务待处理或已已。 +读取当前待办事项列表状态。LLM 使用此信息来跟踪哪些任务处于待处理状态或已完成状态。 :::note -默认情况下,子代理取消此工具,但您可以手动启用它。 [了解更多](/docs/agents/#permissions) +此工具默认情况下对子代理禁用,但您可以手动启用它。 [了解更多](/docs/agents/#permissions) ::: --- -### 網頁抓取 +### 网页获取 -获取網頁內容。 +获取网页内容。 ```json title="opencode.json" {4} { @@ -282,18 +282,18 @@ To configure which LSP servers are available for your project, see [LSP Servers] } ``` -允许 LLM 获取和读取网页。对于查询文件或研究线上资源很有帮助。 +允许LLM获取并读取网页。可用于查找文档或研究在线资源。 --- -### 網路搜索 +### 网页搜索 -在網路上搜索資訊。 +在网上搜索信息。 :::note -仅当使用 opencode 提供或 `OPENCODE_ENABLE_EXA` 程序环境变量设置为任何真值(例如 `true` 或 `1`)时,此工具才可用。 +只有在使用 OpenCode 提供程序时,或者当 OPENCODE_ENABLE_EXA 环境变量被设置为任何真值(例如 true 或 1)时,此工具才可用。 -要在启动 opencode 时启用: +在启动 OpenCode 时启用: ```bash OPENCODE_ENABLE_EXA=1 opencode @@ -310,19 +310,19 @@ OPENCODE_ENABLE_EXA=1 opencode } ``` -使用 Exa AI 执行网路搜索以线上查询相关资讯。对于研究主题、查询时事或收集训练超出数据范围的资讯很有帮助。 +利用 Exa AI 进行网络搜索,查找相关信息。可用于研究特定主题、了解时事新闻或收集超出训练数据范围的信息。 -不需要 API 密钥 — 该工具消耗身份验证即可直接连线到 Exa AI 的托管 MCP 服务。 +无需 API 密钥——该工具无需身份验证即可直接连接到 Exa AI 托管的 MCP 服务。 :::tip -当您需要查询资讯(发现)时,请使用 `websearch`;当您需要从特定 URL 检索内容(搜索)时,请使用 `webfetch`。 +当您需要查找信息时,请使用`网页搜索`;当您需要从特定 URL 检索内容时,请使用`网页获取`。 ::: --- -### 問題 +### 提问 -在执行过程中詢問用户問題。 +在执行过程中向用户提问。 ```json title="opencode.json" {4} { @@ -333,20 +333,20 @@ OPENCODE_ENABLE_EXA=1 opencode } ``` -该工具允许 LLM 在任务期间询问用户问题。它适用于: +该工具允许 LLM 在执行任务期间向用户提问。它在以下方面很有用: -- 收集用户偏好或要求 -- 澄清不明確的指令 -- 就實施选择做出決策 -- 提供选择方向 +- 收集用户偏好或需求 +- 澄清含糊不清的指示 +- 就实施方案做出决定 +- 提供关于选择下一步方向的选项 -每个問題都包含標題、問題文字和選項列表。用户可以從提供的選項中進行选择或輸入自定義答案。当存在多个問題時,用户可以在提交所有答案之前在这些問題之间导航。 +每个问题都包含标题、问题正文和选项列表。用户可以从提供的选项中选择答案,也可以输入自定义答案。如果有多个问题,用户可以在提交所有答案之前在不同问题之间切换。 --- -## 定製工具 +## 自定义工具 -自定义工具可以让您定义LLM可以调用自己的函式。这些是在您的配置文件中定义的并且可以执行任何代码。 +自定义工具允许您定义LLM可以调用的自定义函数。这些函数在您的配置文件中定义,并且可以执行任意代码。 [了解更多](/docs/custom-tools)关于创建自定义工具。 @@ -360,15 +360,15 @@ MCP(模型上下文协议)服务器允许您集成外部工具和服务。 --- -## 内部結構 +## 内部规则 -Internally, tools like `grep`, `glob`, and `list` use [ripgrep](https://github.com/BurntSushi/ripgrep) under the hood. By default, ripgrep respects `.gitignore` patterns, which means files and directories listed in your `.gitignore` will be excluded from searches and listings. +在内部,`grep`、 `通配符` 和 `罗列` 等工具底层都使用了 ripgrep。默认情况下,ripgrep 会遵循 .gitignore 文件中的规则,这意味着 .gitignore 文件中列出的文件和目录将被排除在搜索和列表之外。 --- ### 忽略模式 -要包含通常会被忽略的文件,请在专案根目录中建立 `.ignore` 文件。该文件可以明确允许某些路径。 +为了使工具不跳过那些通常会被忽略的文件,请在项目根目录下创建一个 `.ignore` 文件。该文件内定义的目录可以不会被跳过。 ```text title=".ignore" !node_modules/ @@ -376,4 +376,4 @@ Internally, tools like `grep`, `glob`, and `list` use [ripgrep](https://github.c !build/ ``` -例如,此 `.ignore` 档案允许 ripgrep 在 `node_modules/`、`dist/` 和 `build/` 目录中搜索,即使它们列在 `.gitignore` 中。 +例如,这个 `.ignore` 文件允许 ripgrep 在 `node_modules/`、`dist/` 和 `build/` 目录中搜索,即使它们已在 `.gitignore` 中列出。 From f991a6c0b6bba97be27f3c132c14c5fa78d05536 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 13 Feb 2026 11:19:37 +0000 Subject: [PATCH 026/756] chore: generate --- packages/web/src/content/docs/zh-cn/tools.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/zh-cn/tools.mdx b/packages/web/src/content/docs/zh-cn/tools.mdx index a1a97a3ed7..86190a4e06 100644 --- a/packages/web/src/content/docs/zh-cn/tools.mdx +++ b/packages/web/src/content/docs/zh-cn/tools.mdx @@ -147,7 +147,7 @@ Tools allow the LLM to perform actions in your codebase. opencode comes with a s } ``` -使用类似 **/*.js 或 src/**/*.ts 的通配符模式搜索文件。返回按修改时间排序的匹配文件路径。 +使用类似 **/\*.js 或 src/**/\*.ts 的通配符模式搜索文件。返回按修改时间排序的匹配文件路径。 --- From e242fe19e48f6aa70e5c3f7d54f34d688181edb2 Mon Sep 17 00:00:00 2001 From: eytans Date: Fri, 13 Feb 2026 13:25:47 +0200 Subject: [PATCH 027/756] fix(web): use prompt_async endpoint to avoid timeout over VPN/tunnel (#12749) --- packages/app/e2e/prompt/prompt-async.spec.ts | 43 +++++++++++++++++++ .../app/src/components/prompt-input/submit.ts | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 packages/app/e2e/prompt/prompt-async.spec.ts diff --git a/packages/app/e2e/prompt/prompt-async.spec.ts b/packages/app/e2e/prompt/prompt-async.spec.ts new file mode 100644 index 0000000000..ce9b1a7a3b --- /dev/null +++ b/packages/app/e2e/prompt/prompt-async.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { sessionIDFromUrl } from "../actions" + +// Regression test for Issue #12453: the synchronous POST /message endpoint holds +// the connection open while the agent works, causing "Failed to fetch" over +// VPN/Tailscale. The fix switches to POST /prompt_async which returns immediately. +test("prompt succeeds when sync message endpoint is unreachable", async ({ page, sdk, gotoSession }) => { + test.setTimeout(120_000) + + // Simulate Tailscale/VPN killing the long-lived sync connection + await page.route("**/session/*/message", (route) => route.abort("connectionfailed")) + + await gotoSession() + + const token = `E2E_ASYNC_${Date.now()}` + await page.locator(promptSelector).click() + await page.keyboard.type(`Reply with exactly: ${token}`) + await page.keyboard.press("Enter") + + await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 }) + const sessionID = sessionIDFromUrl(page.url())! + + try { + // Agent response arrives via SSE despite sync endpoint being dead + await expect + .poll( + async () => { + const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? []) + return messages + .filter((m) => m.info.role === "assistant") + .flatMap((m) => m.parts) + .filter((p) => p.type === "text") + .map((p) => p.text) + .join("\n") + }, + { timeout: 90_000 }, + ) + .toContain(token) + } finally { + await sdk.session.delete({ sessionID }).catch(() => undefined) + } +}) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 49d75a95ec..9a1fba5d5c 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -385,7 +385,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { const send = async () => { const ok = await waitForWorktree() if (!ok) return - await client.session.prompt({ + await client.session.promptAsync({ sessionID: session.id, agent, model, From 1c71604e0a2a34786daa99b7002c2f567671051a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 13 Feb 2026 05:50:12 -0600 Subject: [PATCH 028/756] fix(app): terminal resize --- packages/app/src/components/terminal.tsx | 138 ++++++++++++++++------- 1 file changed, 98 insertions(+), 40 deletions(-) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index ccf7012d20..14413dfda6 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -156,6 +156,10 @@ export const Terminal = (props: TerminalProps) => { let serializeAddon: SerializeAddon let fitAddon: FitAddon let handleResize: () => void + let fitFrame: number | undefined + let sizeTimer: ReturnType | undefined + let pendingSize: { cols: number; rows: number } | undefined + let lastSize: { cols: number; rows: number } | undefined let disposed = false const cleanups: VoidFunction[] = [] const start = @@ -209,6 +213,43 @@ export const Terminal = (props: TerminalProps) => { const [terminalColors, setTerminalColors] = createSignal(getTerminalColors()) + const scheduleFit = () => { + if (disposed) return + if (!fitAddon) return + if (fitFrame !== undefined) return + + fitFrame = requestAnimationFrame(() => { + fitFrame = undefined + if (disposed) return + fitAddon.fit() + }) + } + + const scheduleSize = (cols: number, rows: number) => { + if (disposed) return + if (lastSize?.cols === cols && lastSize?.rows === rows) return + + pendingSize = { cols, rows } + + if (!lastSize) { + lastSize = pendingSize + void pushSize(cols, rows) + return + } + + if (sizeTimer !== undefined) return + sizeTimer = setTimeout(() => { + sizeTimer = undefined + const next = pendingSize + if (!next) return + pendingSize = undefined + if (disposed) return + if (lastSize?.cols === next.cols && lastSize?.rows === next.rows) return + lastSize = next + void pushSize(next.cols, next.rows) + }, 100) + } + createEffect(() => { const colors = getTerminalColors() setTerminalColors(colors) @@ -220,6 +261,16 @@ export const Terminal = (props: TerminalProps) => { const font = monoFontFamily(settings.appearance.font()) if (!term) return setOptionIfSupported(term, "fontFamily", font) + scheduleFit() + }) + + let zoom = platform.webviewZoom?.() + createEffect(() => { + const next = platform.webviewZoom?.() + if (next === undefined) return + if (next === zoom) return + zoom = next + scheduleFit() }) const focusTerminal = () => { @@ -263,25 +314,6 @@ export const Terminal = (props: TerminalProps) => { const once = { value: false } - const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`) - url.searchParams.set("directory", sdk.directory) - url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0)) - url.protocol = url.protocol === "https:" ? "wss:" : "ws:" - if (window.__OPENCODE__?.serverPassword) { - url.username = "opencode" - url.password = window.__OPENCODE__?.serverPassword - } - const socket = new WebSocket(url) - socket.binaryType = "arraybuffer" - cleanups.push(() => { - if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close() - }) - if (disposed) { - cleanup() - return - } - ws = socket - const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" const restoreSize = restore && @@ -344,9 +376,28 @@ export const Terminal = (props: TerminalProps) => { focusTerminal() + if (typeof document !== "undefined" && document.fonts) { + document.fonts.ready.then(scheduleFit) + } + + const onResize = t.onResize((size) => { + scheduleSize(size.cols, size.rows) + }) + cleanups.push(() => disposeIfDisposable(onResize)) + const onData = t.onData((data) => { + if (ws?.readyState === WebSocket.OPEN) ws.send(data) + }) + cleanups.push(() => disposeIfDisposable(onData)) + const onKey = t.onKey((key) => { + if (key.key == "Enter") { + props.onSubmit?.() + } + }) + cleanups.push(() => disposeIfDisposable(onKey)) + const startResize = () => { fit.observeResize() - handleResize = () => fit.fit() + handleResize = scheduleFit window.addEventListener("resize", handleResize) cleanups.push(() => window.removeEventListener("resize", handleResize)) } @@ -354,11 +405,13 @@ export const Terminal = (props: TerminalProps) => { if (restore && restoreSize) { t.write(restore, () => { fit.fit() + scheduleSize(t.cols, t.rows) if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) startResize() }) } else { fit.fit() + scheduleSize(t.cols, t.rows) if (restore) { t.write(restore, () => { if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) @@ -367,35 +420,38 @@ export const Terminal = (props: TerminalProps) => { startResize() } - const onResize = t.onResize(async (size) => { - if (socket.readyState === WebSocket.OPEN) { - await pushSize(size.cols, size.rows) - } - }) - cleanups.push(() => disposeIfDisposable(onResize)) - const onData = t.onData((data) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(data) - } - }) - cleanups.push(() => disposeIfDisposable(onData)) - const onKey = t.onKey((key) => { - if (key.key == "Enter") { - props.onSubmit?.() - } - }) - cleanups.push(() => disposeIfDisposable(onKey)) // t.onScroll((ydisp) => { // console.log("Scroll position:", ydisp) // }) + const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`) + url.searchParams.set("directory", sdk.directory) + url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0)) + url.protocol = url.protocol === "https:" ? "wss:" : "ws:" + if (window.__OPENCODE__?.serverPassword) { + url.username = "opencode" + url.password = window.__OPENCODE__?.serverPassword + } + const socket = new WebSocket(url) + socket.binaryType = "arraybuffer" + ws = socket + cleanups.push(() => { + if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close() + }) + if (disposed) { + cleanup() + return + } + const handleOpen = () => { local.onConnect?.() - void pushSize(t.cols, t.rows) + scheduleSize(t.cols, t.rows) } socket.addEventListener("open", handleOpen) cleanups.push(() => socket.removeEventListener("open", handleOpen)) + if (socket.readyState === WebSocket.OPEN) handleOpen() + const decoder = new TextDecoder() const handleMessage = (event: MessageEvent) => { @@ -462,6 +518,8 @@ export const Terminal = (props: TerminalProps) => { onCleanup(() => { disposed = true + if (fitFrame !== undefined) cancelAnimationFrame(fitFrame) + if (sizeTimer !== undefined) clearTimeout(sizeTimer) output?.flush() persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup }) cleanup() @@ -477,7 +535,7 @@ export const Terminal = (props: TerminalProps) => { classList={{ ...(local.classList ?? {}), "select-text": true, - "size-full px-6 py-3 font-mono": true, + "size-full px-6 py-3 font-mono relative overflow-hidden": true, [local.class ?? ""]: !!local.class, }} {...others} From 4f51c0912d76698325862e8fcd7d484b7b9a61fe Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 13 Feb 2026 05:52:33 -0600 Subject: [PATCH 029/756] chore: cleanup --- packages/app/src/components/session/session-header.tsx | 2 +- packages/app/src/pages/session.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index b85b9a536a..f81a2ec440 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -552,7 +552,7 @@ export function SessionHeader() {
-