From def907ae4bf228b43e0b4606f65b9cf2e03c65fc Mon Sep 17 00:00:00 2001 From: Goni Zahavy Date: Sat, 7 Feb 2026 00:13:11 +0200 Subject: [PATCH 01/19] 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 02/19] 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 03/19] 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 04/19] 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 05/19] 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 06/19] 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 07/19] 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 08/19] 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 09/19] 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 10/19] 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 11/19] 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 12/19] 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 13/19] 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 14/19] 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 15/19] 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() {