From 89f0a447f60ed8f98d018faa9726fd3e35c21881 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 6 Feb 2026 13:33:05 -0500 Subject: [PATCH 01/27] wip: zen --- .../console/core/script/disable-reload.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 packages/console/core/script/disable-reload.ts diff --git a/packages/console/core/script/disable-reload.ts b/packages/console/core/script/disable-reload.ts new file mode 100644 index 0000000000..860739eb00 --- /dev/null +++ b/packages/console/core/script/disable-reload.ts @@ -0,0 +1,34 @@ +import { Database, eq } from "../src/drizzle/index.js" +import { BillingTable } from "../src/schema/billing.sql.js" +import { WorkspaceTable } from "../src/schema/workspace.sql.js" + +const workspaceID = process.argv[2] + +if (!workspaceID) { + console.error("Usage: bun disable-reload.ts ") + process.exit(1) +} + +const billing = await Database.use((tx) => + tx + .select({ reload: BillingTable.reload }) + .from(BillingTable) + .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, BillingTable.workspaceID)) + .where(eq(BillingTable.workspaceID, workspaceID)) + .then((rows) => rows[0]), +) +if (!billing) { + console.error("Error: Workspace or billing record not found") + process.exit(1) +} + +if (!billing.reload) { + console.log(`Reload is already disabled for workspace ${workspaceID}`) + process.exit(0) +} + +await Database.use((tx) => + tx.update(BillingTable).set({ reload: false }).where(eq(BillingTable.workspaceID, workspaceID)), +) + +console.log(`Disabled reload for workspace ${workspaceID}`) From e5b355e4583f00fb108ac7dc23c9266ae189d5f1 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 6 Feb 2026 13:57:23 -0500 Subject: [PATCH 02/27] zen: handle 1m context --- .../console/app/src/routes/zen/util/provider/anthropic.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts index a5f92a29ac..7081e980d9 100644 --- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts +++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts @@ -20,7 +20,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => const isBedrockModelArn = providerModel.startsWith("arn:aws:bedrock:") const isBedrockModelID = providerModel.startsWith("global.anthropic.") const isBedrock = isBedrockModelArn || isBedrockModelID - const isSonnet = reqModel.includes("sonnet") + const supports1m = reqModel.includes("sonnet") || reqModel.includes("opus-4-6") return { format: "anthropic", modifyUrl: (providerApi: string, isStream?: boolean) => @@ -33,7 +33,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => } else { headers.set("x-api-key", apiKey) headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01") - if (body.model.startsWith("claude-sonnet-")) { + if (supports1m) { headers.set("anthropic-beta", "context-1m-2025-08-07") } } @@ -43,7 +43,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => ...(isBedrock ? { anthropic_version: "bedrock-2023-05-31", - anthropic_beta: isSonnet ? "context-1m-2025-08-07" : undefined, + anthropic_beta: supports1m ? "context-1m-2025-08-07" : undefined, model: undefined, stream: undefined, } From 84c5df19c75bb366a3e718cae3129f5b72835c5c Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:15:47 -0500 Subject: [PATCH 03/27] feat(tui): add Claude Code-style --fork flag to duplicate sessions before continuing (resolves #11137) (#11340) --- packages/opencode/src/cli/cmd/run.ts | 23 +++++++++++--- packages/opencode/src/cli/cmd/tui/app.tsx | 31 +++++++++++++++++-- .../opencode/src/cli/cmd/tui/context/args.tsx | 1 + packages/opencode/src/cli/cmd/tui/thread.ts | 10 ++++++ 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 6960ffd553..bf71fbe30e 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -236,6 +236,10 @@ export const RunCommand = cmd({ describe: "session id to continue", type: "string", }) + .option("fork", { + describe: "fork the session before continuing (requires --continue or --session)", + type: "boolean", + }) .option("share", { type: "boolean", describe: "share the session", @@ -324,6 +328,11 @@ export const RunCommand = cmd({ process.exit(1) } + if (args.fork && !args.continue && !args.session) { + UI.error("--fork requires --continue or --session") + process.exit(1) + } + const rules: PermissionNext.Ruleset = [ { permission: "question", @@ -349,11 +358,17 @@ export const RunCommand = cmd({ } async function session(sdk: OpencodeClient) { - if (args.continue) { - const result = await sdk.session.list() - return result.data?.find((s) => !s.parentID)?.id + const baseID = args.continue + ? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id + : args.session + + if (baseID && args.fork) { + const forked = await sdk.session.fork({ sessionID: baseID }) + return forked.data?.id } - if (args.session) return args.session + + if (baseID) return baseID + const name = title() const result = await sdk.session.create({ title: name, permission: rules }) return result.data?.id diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 7442037604..0d5aefe7bc 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -250,7 +250,8 @@ function App() { }) local.model.set({ providerID, modelID }, { recent: true }) } - if (args.sessionID) { + // Handle --session without --fork immediately (fork is handled in createEffect below) + if (args.sessionID && !args.fork) { route.navigate({ type: "session", sessionID: args.sessionID, @@ -268,10 +269,36 @@ function App() { .find((x) => x.parentID === undefined)?.id if (match) { continued = true - route.navigate({ type: "session", sessionID: match }) + if (args.fork) { + sdk.client.session.fork({ sessionID: match }).then((result) => { + if (result.data?.id) { + route.navigate({ type: "session", sessionID: result.data.id }) + } else { + toast.show({ message: "Failed to fork session", variant: "error" }) + } + }) + } else { + route.navigate({ type: "session", sessionID: match }) + } } }) + // Handle --session with --fork: wait for sync to be fully complete before forking + // (session list loads in non-blocking phase for --session, so we must wait for "complete" + // to avoid a race where reconcile overwrites the newly forked session) + let forked = false + createEffect(() => { + if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return + forked = true + sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => { + if (result.data?.id) { + route.navigate({ type: "session", sessionID: result.data.id }) + } else { + toast.show({ message: "Failed to fork session", variant: "error" }) + } + }) + }) + createEffect( on( () => sync.status === "complete" && sync.data.provider.length === 0, diff --git a/packages/opencode/src/cli/cmd/tui/context/args.tsx b/packages/opencode/src/cli/cmd/tui/context/args.tsx index ffd43009a4..8a229ffaba 100644 --- a/packages/opencode/src/cli/cmd/tui/context/args.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/args.tsx @@ -6,6 +6,7 @@ export interface Args { prompt?: string continue?: boolean sessionID?: string + fork?: boolean } export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({ diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 0571426854..2ea49ff6b2 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -64,6 +64,10 @@ export const TuiThreadCommand = cmd({ type: "string", describe: "session id to continue", }) + .option("fork", { + type: "boolean", + describe: "fork the session when continuing (use with --continue or --session)", + }) .option("prompt", { type: "string", describe: "prompt to use", @@ -73,6 +77,11 @@ export const TuiThreadCommand = cmd({ describe: "agent to use", }), handler: async (args) => { + if (args.fork && !args.continue && !args.session) { + UI.error("--fork requires --continue or --session") + process.exit(1) + } + // Resolve relative paths against PWD to preserve behavior when using --cwd flag const baseCwd = process.env.PWD ?? process.cwd() const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd() @@ -150,6 +159,7 @@ export const TuiThreadCommand = cmd({ agent: args.agent, model: args.model, prompt, + fork: args.fork, }, onExit: async () => { await client.call("shutdown", undefined) From 918795d868630ba247f187b4a3944389ec79ea0e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 19:16:36 +0000 Subject: [PATCH 04/27] chore: generate --- packages/opencode/src/cli/cmd/run.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index bf71fbe30e..0eb09dd622 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -358,9 +358,7 @@ export const RunCommand = cmd({ } async function session(sdk: OpencodeClient) { - const baseID = args.continue - ? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id - : args.session + const baseID = args.continue ? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id : args.session if (baseID && args.fork) { const forked = await sdk.session.fork({ sessionID: baseID }) From a25cd2da727f0171cea7a23611abb56b8003b7d2 Mon Sep 17 00:00:00 2001 From: Ivan Gonzalez Date: Fri, 6 Feb 2026 13:19:14 -0600 Subject: [PATCH 05/27] feat(opencode): use reasoning summary auto for gpt-5 models that are not chat (#12502) --- packages/opencode/src/provider/transform.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index e564c54a1e..8aab0d4151 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -646,6 +646,7 @@ export namespace ProviderTransform { if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) { if (!input.model.api.id.includes("gpt-5-pro")) { result["reasoningEffort"] = "medium" + result["reasoningSummary"] = "auto" } // Only set textVerbosity for non-chat gpt-5.x models From 24ed2d3a1d1c846f30e265f34838a48aa97fb32d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:40:23 -0600 Subject: [PATCH 06/27] fix(ui): markdown table rendering --- packages/ui/src/components/markdown.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ui/src/components/markdown.css b/packages/ui/src/components/markdown.css index ef43187336..68ae93bda4 100644 --- a/packages/ui/src/components/markdown.css +++ b/packages/ui/src/components/markdown.css @@ -181,6 +181,8 @@ border-collapse: collapse; margin: 1.5rem 0; font-size: var(--font-size-base); + display: block; + overflow-x: auto; } th, From 288a49165138cbe8071d7f65bdc37a1d627d4d2e Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:46:32 +0100 Subject: [PATCH 07/27] fix(docs-windows-wsl): update caution note for server security (#12467) --- packages/web/src/content/docs/windows-wsl.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/web/src/content/docs/windows-wsl.mdx b/packages/web/src/content/docs/windows-wsl.mdx index ebc35d0d9e..ca3b6a4e32 100644 --- a/packages/web/src/content/docs/windows-wsl.mdx +++ b/packages/web/src/content/docs/windows-wsl.mdx @@ -60,13 +60,12 @@ If `localhost` does not work in your setup, connect using the WSL IP address ins :::caution When using `--hostname 0.0.0.0`, set `OPENCODE_SERVER_PASSWORD` to secure the server. +::: ```bash OPENCODE_SERVER_PASSWORD=your-password opencode serve --hostname 0.0.0.0 ``` -::: - --- ## Web Client + WSL From 71930621fdfeb8f3d2b3baf956cd690cd699101a Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Fri, 6 Feb 2026 18:01:40 -0300 Subject: [PATCH 08/27] feat(desktop): Session Review Images (#12360) --- packages/ui/src/components/session-review.css | 4 + packages/ui/src/components/session-review.tsx | 78 +++++++++++++------ packages/ui/src/i18n/ar.ts | 1 + packages/ui/src/i18n/br.ts | 1 + packages/ui/src/i18n/bs.ts | 1 + packages/ui/src/i18n/da.ts | 1 + packages/ui/src/i18n/de.ts | 1 + packages/ui/src/i18n/en.ts | 1 + packages/ui/src/i18n/es.ts | 1 + packages/ui/src/i18n/fr.ts | 1 + packages/ui/src/i18n/ja.ts | 1 + packages/ui/src/i18n/ko.ts | 1 + packages/ui/src/i18n/no.ts | 1 + packages/ui/src/i18n/pl.ts | 1 + packages/ui/src/i18n/ru.ts | 1 + packages/ui/src/i18n/th.ts | 1 + packages/ui/src/i18n/zh.ts | 1 + packages/ui/src/i18n/zht.ts | 1 + 18 files changed, 74 insertions(+), 24 deletions(-) diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index df6df46499..30bfe3b712 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -166,6 +166,10 @@ color: var(--icon-diff-delete-base); } + [data-slot="session-review-change"][data-type="modified"] { + color: var(--icon-diff-modified-base); + } + [data-slot="session-review-file-container"] { padding: 0; } diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 70d9fe8025..fe2475548e 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -332,8 +332,9 @@ export const SessionReview = (props: SessionReviewProps) => { const beforeText = () => (typeof diff.before === "string" ? diff.before : "") const afterText = () => (typeof diff.after === "string" ? diff.after : "") - const isAdded = () => beforeText().length === 0 && afterText().length > 0 - const isDeleted = () => afterText().length === 0 && beforeText().length > 0 + const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) + const isDeleted = () => + diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) const isImage = () => isImageFile(diff.file) const isAudio = () => isAudioFile(diff.file) @@ -422,6 +423,7 @@ export const SessionReview = (props: SessionReviewProps) => { if (!isImage()) return if (imageSrc()) return if (imageStatus() !== "idle") return + if (isDeleted()) return const reader = props.readFile if (!reader) return @@ -546,6 +548,11 @@ export const SessionReview = (props: SessionReviewProps) => { {i18n.t("ui.sessionReview.change.removed")} + + + {i18n.t("ui.sessionReview.change.modified")} + + @@ -564,28 +571,51 @@ export const SessionReview = (props: SessionReviewProps) => { scheduleAnchors() }} > - { - 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 : "", - }} - /> + + +
+ {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) => ( diff --git a/packages/ui/src/i18n/ar.ts b/packages/ui/src/i18n/ar.ts index d1abf5de32..f041eb4858 100644 --- a/packages/ui/src/i18n/ar.ts +++ b/packages/ui/src/i18n/ar.ts @@ -8,6 +8,7 @@ export const dict = { "ui.sessionReview.change.added": "مضاف", "ui.sessionReview.change.removed": "محذوف", + "ui.sessionReview.change.modified": "معدل", "ui.lineComment.label.prefix": "تعليق على ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "جارٍ التعليق على ", diff --git a/packages/ui/src/i18n/br.ts b/packages/ui/src/i18n/br.ts index 36bef26507..b8765ff948 100644 --- a/packages/ui/src/i18n/br.ts +++ b/packages/ui/src/i18n/br.ts @@ -8,6 +8,7 @@ export const dict = { "ui.sessionReview.change.added": "Adicionado", "ui.sessionReview.change.removed": "Removido", + "ui.sessionReview.change.modified": "Modificado", "ui.lineComment.label.prefix": "Comentar em ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Comentando em ", diff --git a/packages/ui/src/i18n/bs.ts b/packages/ui/src/i18n/bs.ts index 75f0783bc4..24e4c12068 100644 --- a/packages/ui/src/i18n/bs.ts +++ b/packages/ui/src/i18n/bs.ts @@ -11,6 +11,7 @@ export const dict = { "ui.sessionReview.collapseAll": "Sažmi sve", "ui.sessionReview.change.added": "Dodano", "ui.sessionReview.change.removed": "Uklonjeno", + "ui.sessionReview.change.modified": "Izmijenjeno", "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 0142d161f4..a87cf795e1 100644 --- a/packages/ui/src/i18n/da.ts +++ b/packages/ui/src/i18n/da.ts @@ -8,6 +8,7 @@ export const dict = { "ui.sessionReview.change.added": "Tilføjet", "ui.sessionReview.change.removed": "Fjernet", + "ui.sessionReview.change.modified": "Ændret", "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 e004233f60..921a12c996 100644 --- a/packages/ui/src/i18n/de.ts +++ b/packages/ui/src/i18n/de.ts @@ -12,6 +12,7 @@ export const dict = { "ui.sessionReview.change.added": "Hinzugefügt", "ui.sessionReview.change.removed": "Entfernt", + "ui.sessionReview.change.modified": "Geändert", "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 a92a498c98..631bc660a6 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -7,6 +7,7 @@ export const dict = { "ui.sessionReview.collapseAll": "Collapse all", "ui.sessionReview.change.added": "Added", "ui.sessionReview.change.removed": "Removed", + "ui.sessionReview.change.modified": "Modified", "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 283548bb34..7308fccad7 100644 --- a/packages/ui/src/i18n/es.ts +++ b/packages/ui/src/i18n/es.ts @@ -8,6 +8,7 @@ export const dict = { "ui.sessionReview.change.added": "Añadido", "ui.sessionReview.change.removed": "Eliminado", + "ui.sessionReview.change.modified": "Modificado", "ui.lineComment.label.prefix": "Comentar en ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Comentando en ", diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts index e9ab8bf8ad..4b6f28f0d8 100644 --- a/packages/ui/src/i18n/fr.ts +++ b/packages/ui/src/i18n/fr.ts @@ -8,6 +8,7 @@ export const dict = { "ui.sessionReview.change.added": "Ajouté", "ui.sessionReview.change.removed": "Supprimé", + "ui.sessionReview.change.modified": "Modifié", "ui.lineComment.label.prefix": "Commenter sur ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Commentaire sur ", diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts index 4da2578d3a..6086070bdb 100644 --- a/packages/ui/src/i18n/ja.ts +++ b/packages/ui/src/i18n/ja.ts @@ -8,6 +8,7 @@ export const dict = { "ui.sessionReview.change.added": "追加", "ui.sessionReview.change.removed": "削除", + "ui.sessionReview.change.modified": "変更", "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 9fb120b5ea..d2203aad18 100644 --- a/packages/ui/src/i18n/ko.ts +++ b/packages/ui/src/i18n/ko.ts @@ -8,6 +8,7 @@ export const dict = { "ui.sessionReview.change.added": "추가됨", "ui.sessionReview.change.removed": "삭제됨", + "ui.sessionReview.change.modified": "수정됨", "ui.lineComment.label.prefix": "", "ui.lineComment.label.suffix": "에 댓글 달기", "ui.lineComment.editorLabel.prefix": "", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index e578e3cdfa..ca1504da2e 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -11,6 +11,7 @@ export const dict: Record = { "ui.sessionReview.change.added": "Lagt til", "ui.sessionReview.change.removed": "Fjernet", + "ui.sessionReview.change.modified": "Endret", "ui.lineComment.label.prefix": "Kommenter på ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "Kommenterer på ", diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts index 0690a75818..fb10debbb9 100644 --- a/packages/ui/src/i18n/pl.ts +++ b/packages/ui/src/i18n/pl.ts @@ -8,6 +8,7 @@ export const dict = { "ui.sessionReview.change.added": "Dodano", "ui.sessionReview.change.removed": "Usunięto", + "ui.sessionReview.change.modified": "Zmodyfikowano", "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 d5a5b59fa8..417fe0ce8b 100644 --- a/packages/ui/src/i18n/ru.ts +++ b/packages/ui/src/i18n/ru.ts @@ -8,6 +8,7 @@ export const dict = { "ui.sessionReview.change.added": "Добавлено", "ui.sessionReview.change.removed": "Удалено", + "ui.sessionReview.change.modified": "Изменено", "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 097a2dab0a..4a1fbb0a4a 100644 --- a/packages/ui/src/i18n/th.ts +++ b/packages/ui/src/i18n/th.ts @@ -7,6 +7,7 @@ export const dict = { "ui.sessionReview.collapseAll": "ย่อทั้งหมด", "ui.sessionReview.change.added": "เพิ่ม", "ui.sessionReview.change.removed": "ลบ", + "ui.sessionReview.change.modified": "แก้ไข", "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 25f36b3cd2..aead06a03d 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -12,6 +12,7 @@ export const dict = { "ui.sessionReview.change.added": "已添加", "ui.sessionReview.change.removed": "已移除", + "ui.sessionReview.change.modified": "已修改", "ui.lineComment.label.prefix": "评论 ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "正在评论 ", diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts index ebc096cbf7..0b9fb6eacd 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -12,6 +12,7 @@ export const dict = { "ui.sessionReview.change.added": "已新增", "ui.sessionReview.change.removed": "已移除", + "ui.sessionReview.change.modified": "已修改", "ui.lineComment.label.prefix": "評論 ", "ui.lineComment.label.suffix": "", "ui.lineComment.editorLabel.prefix": "正在評論 ", From def907ae4bf228b43e0b4606f65b9cf2e03c65fc Mon Sep 17 00:00:00 2001 From: Goni Zahavy Date: Sat, 7 Feb 2026 00:13:11 +0200 Subject: [PATCH 09/27] fix(opencode): SessionPrompt.shell() now triggers loop if messages are queued (#10987) --- .../opencode/src/server/routes/session.ts | 2 +- packages/opencode/src/session/prompt.ts | 33 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 3850376bdb..82e6f3121b 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -539,7 +539,7 @@ export const SessionRoutes = lazy(() => }, auto: body.auto, }) - await SessionPrompt.loop(sessionID) + await SessionPrompt.loop({ sessionID }) return c.json(true) }, ) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index bcfccfb3e6..6643466b2e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -174,7 +174,7 @@ export namespace SessionPrompt { return message } - return loop(input.sessionID) + return loop({sessionID: input.sessionID}) }) export async function resolvePromptParts(template: string): Promise { @@ -239,6 +239,13 @@ export namespace SessionPrompt { return controller.signal } + function resume(sessionID: string) { + const s = state() + if (!s[sessionID]) return + + return s[sessionID].abort.signal + } + export function cancel(sessionID: string) { log.info("cancel", { sessionID }) const s = state() @@ -253,8 +260,14 @@ export namespace SessionPrompt { return } - export const loop = fn(Identifier.schema("session"), async (sessionID) => { - const abort = start(sessionID) + export const LoopInput = z.object({ + sessionID: Identifier.schema("session"), + resume_existing: z.boolean().optional(), + }) + export const loop = fn(LoopInput, async (input) => { + const { sessionID, resume_existing } = input + + const abort = resume_existing ? resume(sessionID) : start(sessionID) if (!abort) { return new Promise((resolve, reject) => { const callbacks = state()[sessionID].callbacks @@ -1366,7 +1379,19 @@ NOTE: At any point in time through this workflow you should feel free to ask the if (!abort) { throw new Session.BusyError(input.sessionID) } - using _ = defer(() => cancel(input.sessionID)) + + using _ = defer(() => { + // If no queued callbacks, cancel (the default) + const callbacks = state()[input.sessionID]?.callbacks ?? [] + if (callbacks.length === 0) { + cancel(input.sessionID) + } else { + // Otherwise, trigger the session loop to process queued items + loop({sessionID: input.sessionID, resume_existing: true}).catch((error) => { + log.error("session loop failed to resume after shell command", { sessionID: input.sessionID, error }) + }) + } + }) const session = await Session.get(input.sessionID) if (session.revert) { From 13381580afacd5de41260a1f3a2f651a81f37149 Mon Sep 17 00:00:00 2001 From: Ganesh <179367536+itskritix@users.noreply.github.com> Date: Sat, 7 Feb 2026 03:43:37 +0530 Subject: [PATCH 10/27] fix(app): keep startup script field scrollable in edit project dialog (#12431) --- packages/app/src/components/dialog-edit-project.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 622daee7a3..dbad81798f 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -223,7 +223,7 @@ export function DialogEditProject(props: { project: LocalProject }) { value={store.startup} onChange={(v) => setStore("startup", v)} spellcheck={false} - class="max-h-40 w-full font-mono text-xs no-scrollbar" + class="max-h-14 w-full overflow-y-auto font-mono text-xs" /> From 898778daa933a97a0e21270ab11b466bca634bc9 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:13:48 -0600 Subject: [PATCH 11/27] chore: upgrade bun to 1.3.8 (#11892) --- bun.lock | 6 +++--- package.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 35de988870..11b6938e72 100644 --- a/bun.lock +++ b/bun.lock @@ -516,7 +516,7 @@ "@tailwindcss/vite": "4.1.11", "@tsconfig/bun": "1.0.9", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.5", + "@types/bun": "1.3.8", "@types/luxon": "3.7.1", "@types/node": "22.13.9", "@types/semver": "7.7.1", @@ -1825,7 +1825,7 @@ "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -2135,7 +2135,7 @@ "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], diff --git a/package.json b/package.json index 65cd0dea80..148d2ad841 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.5", + "packageManager": "bun@1.3.8", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev:desktop": "bun --cwd packages/desktop tauri dev", @@ -23,7 +23,7 @@ "packages/slack" ], "catalog": { - "@types/bun": "1.3.5", + "@types/bun": "1.3.8", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", "ulid": "3.0.1", From e767801db2a88ac9b7f3e9420641575105c71341 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 22:14:34 +0000 Subject: [PATCH 12/27] chore: generate --- packages/opencode/src/session/prompt.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6643466b2e..92ddf8c5b1 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -174,7 +174,7 @@ export namespace SessionPrompt { return message } - return loop({sessionID: input.sessionID}) + return loop({ sessionID: input.sessionID }) }) export async function resolvePromptParts(template: string): Promise { @@ -1387,8 +1387,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the cancel(input.sessionID) } else { // Otherwise, trigger the session loop to process queued items - loop({sessionID: input.sessionID, resume_existing: true}).catch((error) => { - log.error("session loop failed to resume after shell command", { sessionID: input.sessionID, error }) + loop({ sessionID: input.sessionID, resume_existing: true }).catch((error) => { + log.error("session loop failed to resume after shell command", { sessionID: input.sessionID, error }) }) } }) From e9a3cfc083bf480ba2c8aaa585a4e914549e3e56 Mon Sep 17 00:00:00 2001 From: Abdi Ibrahim <136372934+abdiths@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:15:04 -0500 Subject: [PATCH 13/27] fix(desktop): allow agent select to use full width on windows (#12428) --- packages/app/src/components/prompt-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 46d7f93eb3..2bccddc291 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1023,7 +1023,7 @@ export const PromptInput: Component = (props) => { options={local.agent.list().map((agent) => agent.name)} current={local.agent.current()?.name ?? ""} onSelect={local.agent.set} - class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-[80px]" : "max-w-[120px]"}`} + class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-full" : "max-w-[120px]"}`} valueClass="truncate" variant="ghost" /> From fde0b39b7c97dacb78cb55f3d963aa54f61650ea Mon Sep 17 00:00:00 2001 From: "Khang Ha (Kelvin)" Date: Sat, 7 Feb 2026 05:16:56 +0700 Subject: [PATCH 14/27] fix: properly encode file URLs with special characters (#12424) --- packages/app/src/components/file-tree.tsx | 10 +++- .../prompt-input/build-request-parts.ts | 10 +++- packages/app/src/context/file/path.ts | 19 ++++++- packages/opencode/src/acp/agent.ts | 10 ++-- packages/opencode/src/cli/cmd/run.ts | 3 +- .../cli/cmd/tui/component/dialog-status.tsx | 3 +- .../cmd/tui/component/prompt/autocomplete.tsx | 7 ++- packages/opencode/src/lsp/index.ts | 4 +- packages/opencode/src/session/prompt.ts | 6 +- .../test/session/prompt-special-chars.test.ts | 56 +++++++++++++++++++ packages/sdk/js/example/example.ts | 5 +- script/duplicate-pr.ts | 3 +- 12 files changed, 114 insertions(+), 22 deletions(-) create mode 100644 packages/opencode/test/session/prompt-special-chars.test.ts diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 183c1555bd..4a3e276724 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -19,6 +19,14 @@ import { import { Dynamic } from "solid-js/web" import type { FileNode } from "@opencode-ai/sdk/v2" +function pathToFileUrl(filepath: string): string { + const encodedPath = filepath + .split("/") + .map((segment) => encodeURIComponent(segment)) + .join("/") + return `file://${encodedPath}` +} + type Kind = "add" | "del" | "mix" type Filter = { @@ -247,7 +255,7 @@ export default function FileTree(props: { onDragStart={(e: DragEvent) => { if (!draggable()) return e.dataTransfer?.setData("text/plain", `file:${local.node.path}`) - e.dataTransfer?.setData("text/uri-list", `file://${local.node.path}`) + e.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path)) if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy" const dragImage = document.createElement("div") diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts index 4cf2f29acf..7010a1fd84 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -30,6 +30,12 @@ type BuildRequestPartsInput = { const absolute = (directory: string, path: string) => path.startsWith("/") ? path : (directory + "/" + path).replace("//", "/") +const encodeFilePath = (filepath: string): string => + filepath + .split("/") + .map((segment) => encodeURIComponent(segment)) + .join("/") + const fileQuery = (selection: FileSelection | undefined) => selection ? `?start=${selection.startLine}&end=${selection.endLine}` : "" @@ -99,7 +105,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) { id: Identifier.ascending("part"), type: "file", mime: "text/plain", - url: `file://${path}${fileQuery(attachment.selection)}`, + url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`, filename: getFilename(attachment.path), source: { type: "file", @@ -129,7 +135,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) { const used = new Set(files.map((part) => part.url)) const context = input.context.flatMap((item) => { const path = absolute(input.sessionDirectory, item.path) - const url = `file://${path}${fileQuery(item.selection)}` + const url = `file://${encodeFilePath(path)}${fileQuery(item.selection)}` const comment = item.comment?.trim() if (!comment && used.has(url)) return [] used.add(url) diff --git a/packages/app/src/context/file/path.ts b/packages/app/src/context/file/path.ts index ced30d0fdd..155f05aafa 100644 --- a/packages/app/src/context/file/path.ts +++ b/packages/app/src/context/file/path.ts @@ -72,12 +72,27 @@ export function unquoteGitPath(input: string) { return new TextDecoder().decode(new Uint8Array(bytes)) } +export function decodeFilePath(input: string) { + try { + return decodeURIComponent(input) + } catch { + return input + } +} + +export function encodeFilePath(filepath: string): string { + return filepath + .split("/") + .map((segment) => encodeURIComponent(segment)) + .join("/") +} + export function createPathHelpers(scope: () => string) { const normalize = (input: string) => { const root = scope() const prefix = root.endsWith("/") ? root : root + "/" - let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input))) + let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input)))) if (path.startsWith(prefix)) { path = path.slice(prefix.length) @@ -100,7 +115,7 @@ export function createPathHelpers(scope: () => string) { const tab = (input: string) => { const path = normalize(input) - return `file://${path}` + return `file://${encodeFilePath(path)}` } const pathFromTab = (tabValue: string) => { diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 775acc52a5..f38731676c 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -29,6 +29,7 @@ import { } from "@agentclientprotocol/sdk" import { Log } from "../util/log" +import { pathToFileURL } from "bun" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { Provider } from "../provider/provider" @@ -986,7 +987,7 @@ export namespace ACP { type: "image", mimeType: effectiveMime, data: base64Data, - uri: `file://${filename}`, + uri: pathToFileURL(filename).href, }, }, }) @@ -996,13 +997,14 @@ export namespace ACP { } else { // Non-image: text types get decoded, binary types stay as blob const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json" + const fileUri = pathToFileURL(filename).href const resource = isText ? { - uri: `file://${filename}`, + uri: fileUri, mimeType: effectiveMime, text: Buffer.from(base64Data, "base64").toString("utf-8"), } - : { uri: `file://${filename}`, mimeType: effectiveMime, blob: base64Data } + : { uri: fileUri, mimeType: effectiveMime, blob: base64Data } await this.connection .sessionUpdate({ @@ -1544,7 +1546,7 @@ export namespace ACP { const name = path.split("/").pop() || path return { type: "file", - url: `file://${path}`, + url: pathToFileURL(path).href, filename: name, mime: "text/plain", } diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 0eb09dd622..163a5820d9 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -1,5 +1,6 @@ import type { Argv } from "yargs" import path from "path" +import { pathToFileURL } from "bun" import { UI } from "../ui" import { cmd } from "./cmd" import { Flag } from "../../flag/flag" @@ -314,7 +315,7 @@ export const RunCommand = cmd({ files.push({ type: "file", - url: `file://${resolvedPath}`, + url: pathToFileURL(resolvedPath).href, filename: path.basename(resolvedPath), mime, }) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index e2ab579a97..f3cd54db6e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -1,4 +1,5 @@ import { TextAttributes } from "@opentui/core" +import { fileURLToPath } from "bun" import { useTheme } from "../context/theme" import { useDialog } from "@tui/ui/dialog" import { useSync } from "@tui/context/sync" @@ -19,7 +20,7 @@ export function DialogStatus() { const list = sync.data.config.plugin ?? [] const result = list.map((value) => { if (value.startsWith("file://")) { - const path = value.substring("file://".length) + const path = fileURLToPath(value) const parts = path.split("/") const filename = parts.pop() || path if (!filename.includes(".")) return { name: filename } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 455fccb8c5..42cf82b421 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -1,4 +1,5 @@ import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core" +import { pathToFileURL } from "bun" import fuzzysort from "fuzzysort" import { firstBy } from "remeda" import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js" @@ -246,17 +247,17 @@ export function Autocomplete(props: { const width = props.anchor().width - 4 options.push( ...sortedFiles.map((item): AutocompleteOption => { - let url = `file://${process.cwd()}/${item}` + const fullPath = `${process.cwd()}/${item}` + const urlObj = pathToFileURL(fullPath) let filename = item if (lineRange && !item.endsWith("/")) { filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}` - const urlObj = new URL(url) urlObj.searchParams.set("start", String(lineRange.startLine)) if (lineRange.endLine !== undefined) { urlObj.searchParams.set("end", String(lineRange.endLine)) } - url = urlObj.toString() } + const url = urlObj.href const isDir = item.endsWith("/") return { diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 0fd3b69dfc..9d7d30632a 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -3,7 +3,7 @@ import { Bus } from "@/bus" import { Log } from "../util/log" import { LSPClient } from "./client" import path from "path" -import { pathToFileURL } from "url" +import { pathToFileURL, fileURLToPath } from "url" import { LSPServer } from "./server" import z from "zod" import { Config } from "../config/config" @@ -369,7 +369,7 @@ export namespace LSP { } export async function documentSymbol(uri: string) { - const file = new URL(uri).pathname + const file = fileURLToPath(uri) return run(file, (client) => client.connection .sendRequest("textDocument/documentSymbol", { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 92ddf8c5b1..6113856cc6 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -32,7 +32,7 @@ import { Flag } from "../flag/flag" import { ulid } from "ulid" import { spawn } from "child_process" import { Command } from "../command" -import { $, fileURLToPath } from "bun" +import { $, fileURLToPath, pathToFileURL } from "bun" import { ConfigMarkdown } from "../config/markdown" import { SessionSummary } from "./summary" import { NamedError } from "@opencode-ai/util/error" @@ -210,7 +210,7 @@ export namespace SessionPrompt { if (stats.isDirectory()) { parts.push({ type: "file", - url: `file://${filepath}`, + url: pathToFileURL(filepath).href, filename: name, mime: "application/x-directory", }) @@ -219,7 +219,7 @@ export namespace SessionPrompt { parts.push({ type: "file", - url: `file://${filepath}`, + url: pathToFileURL(filepath).href, filename: name, mime: "text/plain", }) diff --git a/packages/opencode/test/session/prompt-special-chars.test.ts b/packages/opencode/test/session/prompt-special-chars.test.ts new file mode 100644 index 0000000000..dce0b00495 --- /dev/null +++ b/packages/opencode/test/session/prompt-special-chars.test.ts @@ -0,0 +1,56 @@ +import path from "path" +import { describe, expect, test } from "bun:test" +import { fileURLToPath } from "url" +import { Instance } from "../../src/project/instance" +import { Log } from "../../src/util/log" +import { Session } from "../../src/session" +import { SessionPrompt } from "../../src/session/prompt" +import { MessageV2 } from "../../src/session/message-v2" +import { tmpdir } from "../fixture/fixture" + +Log.init({ print: false }) + +describe("session.prompt special characters", () => { + test("handles filenames with # character", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + await Bun.write(path.join(dir, "file#name.txt"), "special content\n") + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + const template = "Read @file#name.txt" + const parts = await SessionPrompt.resolvePromptParts(template) + const fileParts = parts.filter((part) => part.type === "file") + + expect(fileParts.length).toBe(1) + expect(fileParts[0].filename).toBe("file#name.txt") + + // Verify the URL is properly encoded (# should be %23) + expect(fileParts[0].url).toContain("%23") + + // Verify the URL can be correctly converted back to a file path + const decodedPath = fileURLToPath(fileParts[0].url) + expect(decodedPath).toBe(path.join(tmp.path, "file#name.txt")) + + const message = await SessionPrompt.prompt({ + sessionID: session.id, + parts, + noReply: true, + }) + const stored = await MessageV2.get({ sessionID: session.id, messageID: message.info.id }) + + // Verify the file content was read correctly + const textParts = stored.parts.filter((part) => part.type === "text") + const hasContent = textParts.some((part) => part.text.includes("special content")) + expect(hasContent).toBe(true) + + await Session.remove(session.id) + }, + }) + }) +}) diff --git a/packages/sdk/js/example/example.ts b/packages/sdk/js/example/example.ts index 481fc42402..42838a82a7 100644 --- a/packages/sdk/js/example/example.ts +++ b/packages/sdk/js/example/example.ts @@ -1,4 +1,5 @@ import { createOpencodeClient, createOpencodeServer } from "@opencode-ai/sdk" +import { pathToFileURL } from "bun" const server = await createOpencodeServer() const client = createOpencodeClient({ baseUrl: server.url }) @@ -17,7 +18,7 @@ for await (const file of input) { { type: "file", mime: "text/plain", - url: `file://${file}`, + url: pathToFileURL(file).href, }, { type: "text", @@ -41,7 +42,7 @@ await Promise.all( { type: "file", mime: "text/plain", - url: `file://${file}`, + url: pathToFileURL(file).href, }, { type: "text", diff --git a/script/duplicate-pr.ts b/script/duplicate-pr.ts index aba078cecf..b77737c1d4 100755 --- a/script/duplicate-pr.ts +++ b/script/duplicate-pr.ts @@ -1,6 +1,7 @@ #!/usr/bin/env bun import path from "path" +import { pathToFileURL } from "bun" import { createOpencode } from "@opencode-ai/sdk" import { parseArgs } from "util" @@ -49,7 +50,7 @@ Examples: } parts.push({ type: "file", - url: `file://${resolved}`, + url: pathToFileURL(resolved).href, filename: path.basename(resolved), mime: "text/plain", }) From 89064c34c502a82d44b78030cc9b522f7e5966be Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 6 Feb 2026 17:18:03 -0500 Subject: [PATCH 15/27] fix(opencode): cleanup orphaned worktree directories (#12399) --- packages/opencode/src/worktree/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index b0dfd57dd2..2e095136eb 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -411,8 +411,13 @@ export namespace Worktree { if (key === directory) return item } })() + if (!entry?.path) { - throw new RemoveFailedError({ message: "Worktree not found" }) + const directoryExists = await exists(directory) + if (directoryExists) { + await fs.rm(directory, { recursive: true, force: true }) + } + return true } const removed = await $`git worktree remove --force ${entry.path}`.quiet().nothrow().cwd(Instance.worktree) From c42d2602b418838155513b3a98a2fb8c811a17a3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 22:27:14 +0000 Subject: [PATCH 16/27] 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 0bb59650f6..059a27ee4f 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-FMrW0aXYOgRe3ginr4l1LwCszsD/r5CQkvRU6HHA7iw=", - "aarch64-linux": "sha256-NZTtIsFZshWOp5mVFvrcVeHUlx62QcsSJKPYjwPhmYk=", - "aarch64-darwin": "sha256-6cWt8KaqojTJ/b3WSYb3dDPTNuKBDt9Fxx6p/WGBnik=", - "x86_64-darwin": "sha256-F6zuxV34RQ9RTjH0c22rGZaPrhemhRUPi+OkF+Y0ytM=" + "x86_64-linux": "sha256-hfU9NxzKIqJJeMbPqhIl/ljxeu0HnnWIjzpFccwB9As=", + "aarch64-linux": "sha256-LNdGstIWKeVwzSYKHkcQELLRlVlpusLR0BJXJKzj9Sw=", + "aarch64-darwin": "sha256-M+qU71LfomD7rU0UClY84B4cdyhqJcHbfcnvn40ReYI=", + "x86_64-darwin": "sha256-7h5dx4PQehYyl6lZ5txEUbBQq3gkTmLk9GyjyIKwpRE=" } } From 7249b87bf682ed2b21c316aeddad409d5c558f5b Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 6 Feb 2026 17:31:40 -0500 Subject: [PATCH 17/27] feat(skill): add skill discovery from URLs via well-known RFC (#12423) Co-authored-by: Frank --- .../opencode/test/skill/discovery.test.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packages/opencode/test/skill/discovery.test.ts diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts new file mode 100644 index 0000000000..90759fa3c8 --- /dev/null +++ b/packages/opencode/test/skill/discovery.test.ts @@ -0,0 +1,60 @@ +import { describe, test, expect } from "bun:test" +import { Discovery } from "../../src/skill/discovery" +import path from "path" + +const CLOUDFLARE_SKILLS_URL = "https://developers.cloudflare.com/.well-known/skills/" + +describe("Discovery.pull", () => { + test("downloads skills from cloudflare url", async () => { + const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL) + expect(dirs.length).toBeGreaterThan(0) + for (const dir of dirs) { + expect(dir).toStartWith(Discovery.dir()) + const md = path.join(dir, "SKILL.md") + expect(await Bun.file(md).exists()).toBe(true) + } + }, 30_000) + + test("url without trailing slash works", async () => { + const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL.replace(/\/$/, "")) + expect(dirs.length).toBeGreaterThan(0) + for (const dir of dirs) { + const md = path.join(dir, "SKILL.md") + expect(await Bun.file(md).exists()).toBe(true) + } + }, 30_000) + + test("returns empty array for invalid url", async () => { + const dirs = await Discovery.pull("https://example.invalid/.well-known/skills/") + expect(dirs).toEqual([]) + }) + + test("returns empty array for non-json response", async () => { + const dirs = await Discovery.pull("https://example.com/") + expect(dirs).toEqual([]) + }) + + test("downloads reference files alongside SKILL.md", async () => { + const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL) + // find a skill dir that should have reference files (e.g. agents-sdk) + const agentsSdk = dirs.find((d) => d.endsWith("/agents-sdk")) + if (agentsSdk) { + const refs = path.join(agentsSdk, "references") + expect(await Bun.file(path.join(agentsSdk, "SKILL.md")).exists()).toBe(true) + // agents-sdk has reference files per the index + const refDir = await Array.fromAsync(new Bun.Glob("**/*.md").scan({ cwd: refs, onlyFiles: true })) + expect(refDir.length).toBeGreaterThan(0) + } + }, 30_000) + + test("caches downloaded files on second pull", async () => { + // first pull to populate cache + const first = await Discovery.pull(CLOUDFLARE_SKILLS_URL) + expect(first.length).toBeGreaterThan(0) + + // second pull should return same results from cache + const second = await Discovery.pull(CLOUDFLARE_SKILLS_URL) + expect(second.length).toBe(first.length) + expect(second.sort()).toEqual(first.sort()) + }, 60_000) +}) From a486b74b14a862c3c4efc313695d7c54c7f63e5f Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Fri, 6 Feb 2026 22:33:47 +0000 Subject: [PATCH 18/27] feat(core): Set variant in assistant messages too (#12531) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline --- packages/opencode/src/session/compaction.ts | 1 + packages/opencode/src/session/message-v2.ts | 1 + packages/opencode/src/session/prompt.ts | 2 ++ packages/sdk/js/src/v2/gen/types.gen.ts | 1 + 4 files changed, 5 insertions(+) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index fb38253029..73a70af9d4 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -108,6 +108,7 @@ export namespace SessionCompaction { sessionID: input.sessionID, mode: "compaction", agent: "compaction", + variant: userMessage.variant, summary: true, path: { cwd: Instance.directory, diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index b6043b0325..65ac72e050 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -387,6 +387,7 @@ export namespace MessageV2 { write: z.number(), }), }), + variant: z.string().optional(), finish: z.string().optional(), }).meta({ ref: "AssistantMessage", diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6113856cc6..ad7b6f1a91 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -336,6 +336,7 @@ export namespace SessionPrompt { sessionID, mode: task.agent, agent: task.agent, + variant: lastUser.variant, path: { cwd: Instance.directory, root: Instance.worktree, @@ -539,6 +540,7 @@ export namespace SessionPrompt { role: "assistant", mode: agent.name, agent: agent.name, + variant: lastUser.variant, path: { cwd: Instance.directory, root: Instance.worktree, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 81df478441..d72c37a28b 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -197,6 +197,7 @@ export type AssistantMessage = { write: number } } + variant?: string finish?: string } From 95d2d4d3a77adceacdee28529d554dac9d2564b0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 22:34:23 +0000 Subject: [PATCH 19/27] chore: generate --- packages/sdk/openapi.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index feb4a29a5c..f50cc06c10 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -6392,6 +6392,9 @@ }, "required": ["input", "output", "reasoning", "cache"] }, + "variant": { + "type": "string" + }, "finish": { "type": "string" } From 576a681a4facafc42159dd1044b4abb40546bfdf Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Sat, 7 Feb 2026 00:46:31 +0100 Subject: [PATCH 20/27] feat: add models.dev schema ref for model autocomplete in opencode.json (#12528) --- bun.lock | 3 +++ package.json | 4 +++- packages/opencode/src/config/config.ts | 13 ++++++------- ...dard-community%2Fstandard-openapi@0.2.9.patch | 16 ++++++++++++++++ 4 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 patches/@standard-community%2Fstandard-openapi@0.2.9.patch diff --git a/bun.lock b/bun.lock index 11b6938e72..fcb2f8f0cf 100644 --- a/bun.lock +++ b/bun.lock @@ -497,6 +497,9 @@ "web-tree-sitter", "tree-sitter-bash", ], + "patchedDependencies": { + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + }, "overrides": { "@types/bun": "catalog:", "@types/node": "catalog:", diff --git a/package.json b/package.json index 148d2ad841..2c69f46d29 100644 --- a/package.json +++ b/package.json @@ -100,5 +100,7 @@ "@types/bun": "catalog:", "@types/node": "catalog:" }, - "patchedDependencies": {} + "patchedDependencies": { + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch" + } } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 6dd0592d51..e01fee28a7 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -33,6 +33,8 @@ import { proxied } from "@/util/proxied" import { iife } from "@/util/iife" export namespace Config { + const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }) + const log = Log.create({ service: "config" }) // Managed settings directory for enterprise deployments (highest priority, admin-controlled) @@ -653,7 +655,7 @@ export namespace Config { template: z.string(), description: z.string().optional(), agent: z.string().optional(), - model: z.string().optional(), + model: ModelId.optional(), subtask: z.boolean().optional(), }) export type Command = z.infer @@ -669,7 +671,7 @@ export namespace Config { export const Agent = z .object({ - model: z.string().optional(), + model: ModelId.optional(), variant: z .string() .optional() @@ -1040,11 +1042,8 @@ export namespace Config { .array(z.string()) .optional() .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"), - model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), - small_model: z - .string() - .describe("Small model to use for tasks like title generation in the format of provider/model") - .optional(), + model: ModelId.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), + small_model: ModelId.describe("Small model to use for tasks like title generation in the format of provider/model").optional(), default_agent: z .string() .optional() diff --git a/patches/@standard-community%2Fstandard-openapi@0.2.9.patch b/patches/@standard-community%2Fstandard-openapi@0.2.9.patch new file mode 100644 index 0000000000..2ac5af09ab --- /dev/null +++ b/patches/@standard-community%2Fstandard-openapi@0.2.9.patch @@ -0,0 +1,16 @@ +diff --git a/dist/vendors/convert.js b/dist/vendors/convert.js +index 0d615eebfd7cd646937ec1b29f8332db73586ec1..7b146f903c07a9377d676753691cba67781879be 100644 +--- a/dist/vendors/convert.js ++++ b/dist/vendors/convert.js +@@ -74,7 +74,10 @@ function convertToOpenAPISchema(jsonSchema, context) { + $ref: `#/components/schemas/${id}` + }; + } else if (_jsonSchema.$ref) { +- const { $ref, $defs } = _jsonSchema; ++ const { $ref, $defs, ...rest } = _jsonSchema; ++ if ($ref.includes("://")) { ++ return Object.keys(rest).length > 0 ? rest : { type: "string" }; ++ } + const ref = $ref.split("/").pop(); + context.components.schemas = { + ...context.components.schemas, From fbc08709d10fd99094d5dff6497a16739b020a47 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 23:47:25 +0000 Subject: [PATCH 21/27] chore: generate --- packages/opencode/src/config/config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index e01fee28a7..a231a53007 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1043,7 +1043,9 @@ export namespace Config { .optional() .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"), model: ModelId.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), - small_model: ModelId.describe("Small model to use for tasks like title generation in the format of provider/model").optional(), + small_model: ModelId.describe( + "Small model to use for tasks like title generation in the format of provider/model", + ).optional(), default_agent: z .string() .optional() From 4abf8049c99b6adc0f130e28d0126f59b3869e49 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 23:54:41 +0000 Subject: [PATCH 22/27] 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 059a27ee4f..eb1578dcde 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-hfU9NxzKIqJJeMbPqhIl/ljxeu0HnnWIjzpFccwB9As=", - "aarch64-linux": "sha256-LNdGstIWKeVwzSYKHkcQELLRlVlpusLR0BJXJKzj9Sw=", - "aarch64-darwin": "sha256-M+qU71LfomD7rU0UClY84B4cdyhqJcHbfcnvn40ReYI=", - "x86_64-darwin": "sha256-7h5dx4PQehYyl6lZ5txEUbBQq3gkTmLk9GyjyIKwpRE=" + "x86_64-linux": "sha256-UBz5qXhO+Xy6XptVdbo9V0wKsvZgItmHkWDm6I5VRCk=", + "aarch64-linux": "sha256-G2ezu/ThZR3kYfHnbD0EOcLoAa6hwtICpmo9r+bqibE=", + "aarch64-darwin": "sha256-PhSE23OzNlyfNFP5LffA3AtyN+hsyCeGInmDBBRjr0g=", + "x86_64-darwin": "sha256-vWusYJD+7ClDLUFy1wEqRLf9hY8V43iqdqnZ6YWkh1Q=" } } From b5b93aea425d44a0f49d37eb22d29b98616b7392 Mon Sep 17 00:00:00 2001 From: Rahul A Mistry <149420892+ProdigyRahul@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:32:40 +0530 Subject: [PATCH 23/27] fix(app): toggle file tree and review panel better ux (#12481) --- .../src/components/session/session-header.tsx | 6 +- .../app/src/components/settings-keybinds.tsx | 2 +- packages/app/src/pages/session.tsx | 31 +- .../src/pages/session/session-side-panel.tsx | 295 +++++++++--------- .../pages/session/use-session-commands.tsx | 7 +- 5 files changed, 180 insertions(+), 161 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 805e699312..7eaafc8542 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -544,11 +544,7 @@ export function SessionHeader() {