From 4edb4fa4fa8735a205bd0750513e7c50adf35390 Mon Sep 17 00:00:00 2001 From: andrew-kramer-inno Date: Thu, 15 Jan 2026 09:40:37 -0800 Subject: [PATCH 01/34] fix: handle broken symlinks gracefully in grep tool (#8612) Co-authored-by: Alex Johnson --- packages/opencode/src/tool/grep.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index ad62621e07..097dedf4aa 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -37,7 +37,15 @@ export const GrepTool = Tool.define("grep", { await assertExternalDirectory(ctx, searchPath, { kind: "directory" }) const rgPath = await Ripgrep.filepath() - const args = ["-nH", "--hidden", "--follow", "--field-match-separator=|", "--regexp", params.pattern] + const args = [ + "-nH", + "--hidden", + "--follow", + "--no-messages", + "--field-match-separator=|", + "--regexp", + params.pattern, + ] if (params.include) { args.push("--glob", params.include) } @@ -52,7 +60,10 @@ export const GrepTool = Tool.define("grep", { const errorOutput = await new Response(proc.stderr).text() const exitCode = await proc.exited - if (exitCode === 1) { + // Exit codes: 0 = matches found, 1 = no matches, 2 = errors (but may still have matches) + // With --no-messages, we suppress error output but still get exit code 2 for broken symlinks etc. + // Only fail if exit code is 2 AND no output was produced + if (exitCode === 1 || (exitCode === 2 && !output.trim())) { return { title: params.pattern, metadata: { matches: 0, truncated: false }, @@ -60,10 +71,12 @@ export const GrepTool = Tool.define("grep", { } } - if (exitCode !== 0) { + if (exitCode !== 0 && exitCode !== 2) { throw new Error(`ripgrep failed: ${errorOutput}`) } + const hasErrors = exitCode === 2 + // Handle both Unix (\n) and Windows (\r\n) line endings const lines = output.trim().split(/\r?\n/) const matches = [] @@ -124,6 +137,11 @@ export const GrepTool = Tool.define("grep", { outputLines.push("(Results are truncated. Consider using a more specific path or pattern.)") } + if (hasErrors) { + outputLines.push("") + outputLines.push("(Some paths were inaccessible and skipped)") + } + return { title: params.pattern, metadata: { From ebc194ca9a8725721da13db5e2024a411a99aa8c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:50:18 -0600 Subject: [PATCH 02/34] Prettify retry duration display in TUI (#8608) Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- .../cli/cmd/tui/component/prompt/index.tsx | 4 +- packages/opencode/src/util/format.ts | 20 +++++++ packages/opencode/test/util/format.test.ts | 59 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/src/util/format.ts create mode 100644 packages/opencode/test/util/format.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 9ad85d08f0..96b9e8ffd5 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -23,6 +23,7 @@ import type { FilePart } from "@opencode-ai/sdk/v2" import { TuiEvent } from "../../event" import { iife } from "@/util/iife" import { Locale } from "@/util/locale" +import { formatDuration } from "@/util/format" import { createColors, createFrames } from "../../ui/spinner.ts" import { useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderConnect } from "../dialog-provider" @@ -1037,7 +1038,8 @@ export function Prompt(props: PromptProps) { if (!r) return "" const baseMessage = message() const truncatedHint = isTruncated() ? " (click to expand)" : "" - const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]` + const duration = formatDuration(seconds()) + const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]` return baseMessage + truncatedHint + retryInfo } diff --git a/packages/opencode/src/util/format.ts b/packages/opencode/src/util/format.ts new file mode 100644 index 0000000000..4ae62eac45 --- /dev/null +++ b/packages/opencode/src/util/format.ts @@ -0,0 +1,20 @@ +export function formatDuration(secs: number) { + if (secs <= 0) return "" + if (secs < 60) return `${secs}s` + if (secs < 3600) { + const mins = Math.floor(secs / 60) + const remaining = secs % 60 + return remaining > 0 ? `${mins}m ${remaining}s` : `${mins}m` + } + if (secs < 86400) { + const hours = Math.floor(secs / 3600) + const remaining = Math.floor((secs % 3600) / 60) + return remaining > 0 ? `${hours}h ${remaining}m` : `${hours}h` + } + if (secs < 604800) { + const days = Math.floor(secs / 86400) + return days === 1 ? "~1 day" : `~${days} days` + } + const weeks = Math.floor(secs / 604800) + return weeks === 1 ? "~1 week" : `~${weeks} weeks` +} diff --git a/packages/opencode/test/util/format.test.ts b/packages/opencode/test/util/format.test.ts new file mode 100644 index 0000000000..5b346e7f6b --- /dev/null +++ b/packages/opencode/test/util/format.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test" +import { formatDuration } from "../../src/util/format" + +describe("util.format", () => { + describe("formatDuration", () => { + test("returns empty string for zero or negative values", () => { + expect(formatDuration(0)).toBe("") + expect(formatDuration(-1)).toBe("") + expect(formatDuration(-100)).toBe("") + }) + + test("formats seconds under a minute", () => { + expect(formatDuration(1)).toBe("1s") + expect(formatDuration(30)).toBe("30s") + expect(formatDuration(59)).toBe("59s") + }) + + test("formats minutes under an hour", () => { + expect(formatDuration(60)).toBe("1m") + expect(formatDuration(61)).toBe("1m 1s") + expect(formatDuration(90)).toBe("1m 30s") + expect(formatDuration(120)).toBe("2m") + expect(formatDuration(330)).toBe("5m 30s") + expect(formatDuration(3599)).toBe("59m 59s") + }) + + test("formats hours under a day", () => { + expect(formatDuration(3600)).toBe("1h") + expect(formatDuration(3660)).toBe("1h 1m") + expect(formatDuration(7200)).toBe("2h") + expect(formatDuration(8100)).toBe("2h 15m") + expect(formatDuration(86399)).toBe("23h 59m") + }) + + test("formats days under a week", () => { + expect(formatDuration(86400)).toBe("~1 day") + expect(formatDuration(172800)).toBe("~2 days") + expect(formatDuration(259200)).toBe("~3 days") + expect(formatDuration(604799)).toBe("~6 days") + }) + + test("formats weeks", () => { + expect(formatDuration(604800)).toBe("~1 week") + expect(formatDuration(1209600)).toBe("~2 weeks") + expect(formatDuration(1609200)).toBe("~2 weeks") + }) + + test("handles boundary values correctly", () => { + expect(formatDuration(59)).toBe("59s") + expect(formatDuration(60)).toBe("1m") + expect(formatDuration(3599)).toBe("59m 59s") + expect(formatDuration(3600)).toBe("1h") + expect(formatDuration(86399)).toBe("23h 59m") + expect(formatDuration(86400)).toBe("~1 day") + expect(formatDuration(604799)).toBe("~6 days") + expect(formatDuration(604800)).toBe("~1 week") + }) + }) +}) From 37f30993fa933fc5c4b7f9de40d486e39b70070d Mon Sep 17 00:00:00 2001 From: Maciek Szczesniak Date: Thu, 15 Jan 2026 18:53:06 +0100 Subject: [PATCH 03/34] fix: show toast error message on ConfigMarkdown parse error (#8049) Co-authored-by: Aiden Cline --- packages/opencode/src/config/config.ts | 35 +++++++++++++++++++++----- packages/opencode/src/skill/skill.ts | 18 ++++++++++--- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 06803879f3..134358ec3b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -19,6 +19,8 @@ import { BunProc } from "@/bun" import { Installation } from "@/installation" import { ConfigMarkdown } from "./markdown" import { existsSync } from "fs" +import { Bus } from "@/bus" +import { Session } from "@/session" export namespace Config { const log = Log.create({ service: "config" }) @@ -231,8 +233,15 @@ export namespace Config { dot: true, cwd: dir, })) { - const md = await ConfigMarkdown.parse(item) - if (!md.data) continue + const md = await ConfigMarkdown.parse(item).catch((err) => { + const message = ConfigMarkdown.FrontmatterError.isInstance(err) + ? `${err.data.path}: ${err.data.message}` + : `Failed to parse command ${item}` + Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + log.error("failed to load command", { command: item, err }) + return undefined + }) + if (!md) continue const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"] const file = rel(item, patterns) ?? path.basename(item) @@ -263,8 +272,15 @@ export namespace Config { dot: true, cwd: dir, })) { - const md = await ConfigMarkdown.parse(item) - if (!md.data) continue + const md = await ConfigMarkdown.parse(item).catch((err) => { + const message = ConfigMarkdown.FrontmatterError.isInstance(err) + ? `${err.data.path}: ${err.data.message}` + : `Failed to parse agent ${item}` + Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + log.error("failed to load agent", { agent: item, err }) + return undefined + }) + if (!md) continue const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"] const file = rel(item, patterns) ?? path.basename(item) @@ -294,8 +310,15 @@ export namespace Config { dot: true, cwd: dir, })) { - const md = await ConfigMarkdown.parse(item) - if (!md.data) continue + const md = await ConfigMarkdown.parse(item).catch((err) => { + const message = ConfigMarkdown.FrontmatterError.isInstance(err) + ? `${err.data.path}: ${err.data.message}` + : `Failed to parse mode ${item}` + Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + log.error("failed to load mode", { mode: item, err }) + return undefined + }) + if (!md) continue const config = { name: path.basename(item, ".md"), diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 1cc3afee92..95a599a547 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -1,4 +1,5 @@ import z from "zod" +import path from "path" import { Config } from "../config/config" import { Instance } from "../project/instance" import { NamedError } from "@opencode-ai/util/error" @@ -7,6 +8,9 @@ import { Log } from "../util/log" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" import { Flag } from "@/flag/flag" +import { Bus } from "@/bus" +import { TuiEvent } from "@/cli/cmd/tui/event" +import { Session } from "@/session" export namespace Skill { const log = Log.create({ service: "skill" }) @@ -42,10 +46,16 @@ export namespace Skill { const skills: Record = {} const addSkill = async (match: string) => { - const md = await ConfigMarkdown.parse(match) - if (!md) { - return - } + const md = await ConfigMarkdown.parse(match).catch((err) => { + const message = ConfigMarkdown.FrontmatterError.isInstance(err) + ? `${err.data.path}: ${err.data.message}` + : `Failed to parse skill ${match}` + Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + log.error("failed to load skill", { skill: match, err }) + return undefined + }) + + if (!md) return const parsed = Info.pick({ name: true, description: true }).safeParse(md.data) if (!parsed.success) return From 5a8a0f6a5661ad5f02f951473595230b03b17840 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 15 Jan 2026 12:16:17 -0600 Subject: [PATCH 04/34] fix: downgrade bun to fix avx issue --- bun.lock | 6 +++--- package.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 020c80b02b..cf123051db 100644 --- a/bun.lock +++ b/bun.lock @@ -505,7 +505,7 @@ "@tailwindcss/vite": "4.1.11", "@tsconfig/bun": "1.0.9", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.6", + "@types/bun": "1.3.5", "@types/luxon": "3.7.1", "@types/node": "22.13.9", "@typescript/native-preview": "7.0.0-dev.20251207.1", @@ -1773,7 +1773,7 @@ "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], - "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -2075,7 +2075,7 @@ "bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="], - "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], "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 9aa069d52c..f1d6c4fead 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.6", + "packageManager": "bun@1.3.5", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "typecheck": "bun turbo typecheck", @@ -21,7 +21,7 @@ "packages/slack" ], "catalog": { - "@types/bun": "1.3.6", + "@types/bun": "1.3.5", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", "ulid": "3.0.1", From 161e3db795382b820cad2061fba79df08f263203 Mon Sep 17 00:00:00 2001 From: Github Action Date: Thu, 15 Jan 2026 18:17:44 +0000 Subject: [PATCH 05/34] Update Nix flake.lock and x86_64-linux hash --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index df85d88394..d1271d9fee 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { - "x86_64-linux": "sha256-XP1DXs1Fcfog99rjMryki9mMqn1g1H4ykHx7WDsnrnw=", + "x86_64-linux": "sha256-4ndHIlS9t1ynRdFszJ1nvcu3YhunhuOc7jcuHI1FbnM=", "aarch64-darwin": "sha256-fupiqvXkW3Cl44K+n1cDz81vOboMXIHPHTey6TewX70=" } } From 3f3550a16e788d7467197da99cb282ba8e0358ad Mon Sep 17 00:00:00 2001 From: Github Action Date: Thu, 15 Jan 2026 18:29:11 +0000 Subject: [PATCH 06/34] Update aarch64-darwin hash --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index d1271d9fee..c89b60ef97 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { "x86_64-linux": "sha256-4ndHIlS9t1ynRdFszJ1nvcu3YhunhuOc7jcuHI1FbnM=", - "aarch64-darwin": "sha256-fupiqvXkW3Cl44K+n1cDz81vOboMXIHPHTey6TewX70=" + "aarch64-darwin": "sha256-C0E9KAEj3GI83HwirIL2zlXYIe92T+7Iv6F51BB6slY=" } } From dc1c25cff5f49ba892a54b31c4c3fc16de1d7018 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:06:14 -0800 Subject: [PATCH 07/34] fix: ensure frontmatter can process same content as other agents (#8719) --- packages/opencode/src/cli/error.ts | 2 +- packages/opencode/src/config/config.ts | 6 +- packages/opencode/src/config/markdown.ts | 56 +++- packages/opencode/src/skill/skill.ts | 2 +- .../test/config/fixtures/empty-frontmatter.md | 4 + .../test/config/fixtures/frontmatter.md | 28 ++ .../test/config/fixtures/no-frontmatter.md | 1 + .../opencode/test/config/markdown.test.ts | 245 +++++++++++++----- 8 files changed, 266 insertions(+), 78 deletions(-) create mode 100644 packages/opencode/test/config/fixtures/empty-frontmatter.md create mode 100644 packages/opencode/test/config/fixtures/frontmatter.md create mode 100644 packages/opencode/test/config/fixtures/no-frontmatter.md diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 569b186d55..d7120aa5e9 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -28,7 +28,7 @@ export function FormatError(input: unknown) { return `Directory "${input.data.dir}" in ${input.data.path} is not valid. Rename the directory to "${input.data.suggestion}" or remove it. This is a common typo.` } if (ConfigMarkdown.FrontmatterError.isInstance(input)) { - return `Failed to parse frontmatter in ${input.data.path}:\n${input.data.message}` + return input.data.message } if (Config.InvalidError.isInstance(input)) return [ diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 134358ec3b..322ce273ab 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -235,7 +235,7 @@ export namespace Config { })) { const md = await ConfigMarkdown.parse(item).catch((err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) - ? `${err.data.path}: ${err.data.message}` + ? err.data.message : `Failed to parse command ${item}` Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load command", { command: item, err }) @@ -274,7 +274,7 @@ export namespace Config { })) { const md = await ConfigMarkdown.parse(item).catch((err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) - ? `${err.data.path}: ${err.data.message}` + ? err.data.message : `Failed to parse agent ${item}` Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load agent", { agent: item, err }) @@ -312,7 +312,7 @@ export namespace Config { })) { const md = await ConfigMarkdown.parse(item).catch((err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) - ? `${err.data.path}: ${err.data.message}` + ? err.data.message : `Failed to parse mode ${item}` Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load mode", { mode: item, err }) diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index f20842c41a..d1eeeac382 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -14,8 +14,60 @@ export namespace ConfigMarkdown { return Array.from(template.matchAll(SHELL_REGEX)) } + export function preprocessFrontmatter(content: string): string { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/) + if (!match) return content + + const frontmatter = match[1] + const lines = frontmatter.split("\n") + const result: string[] = [] + + for (const line of lines) { + // skip comments and empty lines + if (line.trim().startsWith("#") || line.trim() === "") { + result.push(line) + continue + } + + // skip lines that are continuations (indented) + if (line.match(/^\s+/)) { + result.push(line) + continue + } + + // match key: value pattern + const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/) + if (!kvMatch) { + result.push(line) + continue + } + + const key = kvMatch[1] + const value = kvMatch[2].trim() + + // skip if value is empty, already quoted, or uses block scalar + if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) { + result.push(line) + continue + } + + // if value contains a colon, convert to block scalar + if (value.includes(":")) { + result.push(`${key}: |`) + result.push(` ${value}`) + continue + } + + result.push(line) + } + + const processed = result.join("\n") + return content.replace(frontmatter, () => processed) + } + export async function parse(filePath: string) { - const template = await Bun.file(filePath).text() + const raw = await Bun.file(filePath).text() + const template = preprocessFrontmatter(raw) try { const md = matter(template) @@ -24,7 +76,7 @@ export namespace ConfigMarkdown { throw new FrontmatterError( { path: filePath, - message: `Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`, + message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`, }, { cause: err }, ) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 95a599a547..6ae0e9fe88 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -48,7 +48,7 @@ export namespace Skill { const addSkill = async (match: string) => { const md = await ConfigMarkdown.parse(match).catch((err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) - ? `${err.data.path}: ${err.data.message}` + ? err.data.message : `Failed to parse skill ${match}` Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load skill", { skill: match, err }) diff --git a/packages/opencode/test/config/fixtures/empty-frontmatter.md b/packages/opencode/test/config/fixtures/empty-frontmatter.md new file mode 100644 index 0000000000..95d5a80ed1 --- /dev/null +++ b/packages/opencode/test/config/fixtures/empty-frontmatter.md @@ -0,0 +1,4 @@ +--- +--- + +Content diff --git a/packages/opencode/test/config/fixtures/frontmatter.md b/packages/opencode/test/config/fixtures/frontmatter.md new file mode 100644 index 0000000000..27822d6218 --- /dev/null +++ b/packages/opencode/test/config/fixtures/frontmatter.md @@ -0,0 +1,28 @@ +--- +description: "This is a description wrapped in quotes" +# field: this is a commented out field that should be ignored +occupation: This man has the following occupation: Software Engineer +title: 'Hello World' +name: John "Doe" + +family: He has no 'family' +summary: > + This is a summary +url: https://example.com:8080/path?query=value +time: The time is 12:30:00 PM +nested: First: Second: Third: Fourth +quoted_colon: "Already quoted: no change needed" +single_quoted_colon: 'Single quoted: also fine' +mixed: He said "hello: world" and then left +empty: +dollar: Use $' and $& for special patterns +--- + +Content that should not be parsed: + +fake_field: this is not yaml +another: neither is this +time: 10:30:00 AM +url: https://should-not-be-parsed.com:3000 + +The above lines look like YAML but are just content. diff --git a/packages/opencode/test/config/fixtures/no-frontmatter.md b/packages/opencode/test/config/fixtures/no-frontmatter.md new file mode 100644 index 0000000000..39c9f3681a --- /dev/null +++ b/packages/opencode/test/config/fixtures/no-frontmatter.md @@ -0,0 +1 @@ +Content diff --git a/packages/opencode/test/config/markdown.test.ts b/packages/opencode/test/config/markdown.test.ts index 90a997d1f9..b4263ee6b5 100644 --- a/packages/opencode/test/config/markdown.test.ts +++ b/packages/opencode/test/config/markdown.test.ts @@ -1,89 +1,192 @@ -import { expect, test } from "bun:test" +import { expect, test, describe } from "bun:test" import { ConfigMarkdown } from "../../src/config/markdown" -const template = `This is a @valid/path/to/a/file and it should also match at -the beginning of a line: +describe("ConfigMarkdown: normal template", () => { + const template = `This is a @valid/path/to/a/file and it should also match at + the beginning of a line: -@another-valid/path/to/a/file + @another-valid/path/to/a/file -but this is not: + but this is not: - - Adds a "Co-authored-by:" footer which clarifies which AI agent - helped create this commit, using an appropriate \`noreply@...\` - or \`noreply@anthropic.com\` email address. + - Adds a "Co-authored-by:" footer which clarifies which AI agent + helped create this commit, using an appropriate \`noreply@...\` + or \`noreply@anthropic.com\` email address. -We also need to deal with files followed by @commas, ones -with @file-extensions.md, even @multiple.extensions.bak, -hidden directories like @.config/ or files like @.bashrc -and ones at the end of a sentence like @foo.md. + We also need to deal with files followed by @commas, ones + with @file-extensions.md, even @multiple.extensions.bak, + hidden directories like @.config/ or files like @.bashrc + and ones at the end of a sentence like @foo.md. -Also shouldn't forget @/absolute/paths.txt with and @/without/extensions, -as well as @~/home-files and @~/paths/under/home.txt. + Also shouldn't forget @/absolute/paths.txt with and @/without/extensions, + as well as @~/home-files and @~/paths/under/home.txt. -If the reference is \`@quoted/in/backticks\` then it shouldn't match at all.` + If the reference is \`@quoted/in/backticks\` then it shouldn't match at all.` -const matches = ConfigMarkdown.files(template) + const matches = ConfigMarkdown.files(template) -test("should extract exactly 12 file references", () => { - expect(matches.length).toBe(12) + test("should extract exactly 12 file references", () => { + expect(matches.length).toBe(12) + }) + + test("should extract valid/path/to/a/file", () => { + expect(matches[0][1]).toBe("valid/path/to/a/file") + }) + + test("should extract another-valid/path/to/a/file", () => { + expect(matches[1][1]).toBe("another-valid/path/to/a/file") + }) + + test("should extract paths ignoring comma after", () => { + expect(matches[2][1]).toBe("commas") + }) + + test("should extract a path with a file extension and comma after", () => { + expect(matches[3][1]).toBe("file-extensions.md") + }) + + test("should extract a path with multiple dots and comma after", () => { + expect(matches[4][1]).toBe("multiple.extensions.bak") + }) + + test("should extract hidden directory", () => { + expect(matches[5][1]).toBe(".config/") + }) + + test("should extract hidden file", () => { + expect(matches[6][1]).toBe(".bashrc") + }) + + test("should extract a file ignoring period at end of sentence", () => { + expect(matches[7][1]).toBe("foo.md") + }) + + test("should extract an absolute path with an extension", () => { + expect(matches[8][1]).toBe("/absolute/paths.txt") + }) + + test("should extract an absolute path without an extension", () => { + expect(matches[9][1]).toBe("/without/extensions") + }) + + test("should extract an absolute path in home directory", () => { + expect(matches[10][1]).toBe("~/home-files") + }) + + test("should extract an absolute path under home directory", () => { + expect(matches[11][1]).toBe("~/paths/under/home.txt") + }) + + test("should not match when preceded by backtick", () => { + const backtickTest = "This `@should/not/match` should be ignored" + const backtickMatches = ConfigMarkdown.files(backtickTest) + expect(backtickMatches.length).toBe(0) + }) + + test("should not match email addresses", () => { + const emailTest = "Contact user@example.com for help" + const emailMatches = ConfigMarkdown.files(emailTest) + expect(emailMatches.length).toBe(0) + }) }) -test("should extract valid/path/to/a/file", () => { - expect(matches[0][1]).toBe("valid/path/to/a/file") +describe("ConfigMarkdown: frontmatter parsing", async () => { + const parsed = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/frontmatter.md") + + test("should parse without throwing", () => { + expect(parsed).toBeDefined() + expect(parsed.data).toBeDefined() + expect(parsed.content).toBeDefined() + }) + + test("should extract description field", () => { + expect(parsed.data.description).toBe("This is a description wrapped in quotes") + }) + + test("should extract occupation field with colon in value", () => { + expect(parsed.data.occupation).toBe("This man has the following occupation: Software Engineer\n") + }) + + test("should extract title field with single quotes", () => { + expect(parsed.data.title).toBe("Hello World") + }) + + test("should extract name field with embedded quotes", () => { + expect(parsed.data.name).toBe('John "Doe"') + }) + + test("should extract family field with embedded single quotes", () => { + expect(parsed.data.family).toBe("He has no 'family'") + }) + + test("should extract multiline summary field", () => { + expect(parsed.data.summary).toBe("This is a summary\n") + }) + + test("should not include commented fields in data", () => { + expect(parsed.data.field).toBeUndefined() + }) + + test("should extract URL with port", () => { + expect(parsed.data.url).toBe("https://example.com:8080/path?query=value\n") + }) + + test("should extract time with colons", () => { + expect(parsed.data.time).toBe("The time is 12:30:00 PM\n") + }) + + test("should extract value with multiple colons", () => { + expect(parsed.data.nested).toBe("First: Second: Third: Fourth\n") + }) + + test("should preserve already double-quoted values with colons", () => { + expect(parsed.data.quoted_colon).toBe("Already quoted: no change needed") + }) + + test("should preserve already single-quoted values with colons", () => { + expect(parsed.data.single_quoted_colon).toBe("Single quoted: also fine") + }) + + test("should extract value with quotes and colons mixed", () => { + expect(parsed.data.mixed).toBe('He said "hello: world" and then left\n') + }) + + test("should handle empty values", () => { + expect(parsed.data.empty).toBeNull() + }) + + test("should handle dollar sign replacement patterns literally", () => { + expect(parsed.data.dollar).toBe("Use $' and $& for special patterns") + }) + + test("should not parse fake yaml from content", () => { + expect(parsed.data.fake_field).toBeUndefined() + expect(parsed.data.another).toBeUndefined() + }) + + test("should extract content after frontmatter without modification", () => { + expect(parsed.content).toContain("Content that should not be parsed:") + expect(parsed.content).toContain("fake_field: this is not yaml") + expect(parsed.content).toContain("url: https://should-not-be-parsed.com:3000") + }) }) -test("should extract another-valid/path/to/a/file", () => { - expect(matches[1][1]).toBe("another-valid/path/to/a/file") +describe("ConfigMarkdown: frontmatter parsing w/ empty frontmatter", async () => { + const result = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/empty-frontmatter.md") + + test("should parse without throwing", () => { + expect(result).toBeDefined() + expect(result.data).toEqual({}) + expect(result.content.trim()).toBe("Content") + }) }) -test("should extract paths ignoring comma after", () => { - expect(matches[2][1]).toBe("commas") -}) +describe("ConfigMarkdown: frontmatter parsing w/ no frontmatter", async () => { + const result = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/no-frontmatter.md") -test("should extract a path with a file extension and comma after", () => { - expect(matches[3][1]).toBe("file-extensions.md") -}) - -test("should extract a path with multiple dots and comma after", () => { - expect(matches[4][1]).toBe("multiple.extensions.bak") -}) - -test("should extract hidden directory", () => { - expect(matches[5][1]).toBe(".config/") -}) - -test("should extract hidden file", () => { - expect(matches[6][1]).toBe(".bashrc") -}) - -test("should extract a file ignoring period at end of sentence", () => { - expect(matches[7][1]).toBe("foo.md") -}) - -test("should extract an absolute path with an extension", () => { - expect(matches[8][1]).toBe("/absolute/paths.txt") -}) - -test("should extract an absolute path without an extension", () => { - expect(matches[9][1]).toBe("/without/extensions") -}) - -test("should extract an absolute path in home directory", () => { - expect(matches[10][1]).toBe("~/home-files") -}) - -test("should extract an absolute path under home directory", () => { - expect(matches[11][1]).toBe("~/paths/under/home.txt") -}) - -test("should not match when preceded by backtick", () => { - const backtickTest = "This `@should/not/match` should be ignored" - const backtickMatches = ConfigMarkdown.files(backtickTest) - expect(backtickMatches.length).toBe(0) -}) - -test("should not match email addresses", () => { - const emailTest = "Contact user@example.com for help" - const emailMatches = ConfigMarkdown.files(emailTest) - expect(emailMatches.length).toBe(0) + test("should parse without throwing", () => { + expect(result).toBeDefined() + expect(result.data).toEqual({}) + expect(result.content.trim()).toBe("Content") + }) }) From 6450ba1b79b60407d89d8849f36887619279cb8d Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 15 Jan 2026 17:12:11 +0000 Subject: [PATCH 08/34] fix: search bar in header --- .../app/src/components/session/session-header.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 62d29c9e3f..4ba5413dfd 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -54,14 +54,17 @@ export function SessionHeader() { - + +
+ +
+
From 55bd6e487e828ec2d2e98fc1f1ad869e1a9687a7 Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 15 Jan 2026 19:01:41 +0000 Subject: [PATCH 22/34] fix: workspace name color --- packages/app/src/pages/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b4b82666b9..d7a1d70eca 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1113,7 +1113,7 @@ export default function Layout(props: ParentProps) {
- {title()} + {title()} Date: Thu, 15 Jan 2026 19:03:18 +0000 Subject: [PATCH 23/34] fix: remove more options tooltip --- packages/app/src/pages/layout.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index d7a1d70eca..e8e3cd8ade 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1122,9 +1122,7 @@ export default function Layout(props: ParentProps) { From 99110d12c4d6dae58db8a30b1f56f8fae958fd3e Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 15 Jan 2026 19:13:33 +0000 Subject: [PATCH 25/34] fix: remove the active state from load more button after press --- packages/app/src/pages/layout.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 278ca04926..1c653e06af 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1160,7 +1160,10 @@ export default function Layout(props: ParentProps) { variant="ghost" class="flex w-full text-left justify-start text-14-regular text-text-weak px-10" size="large" - onClick={loadMore} + onClick={(e: MouseEvent) => { + loadMore() + ;(e.currentTarget as HTMLButtonElement).blur() + }} > Load more @@ -1205,7 +1208,10 @@ export default function Layout(props: ParentProps) { variant="ghost" class="flex w-full text-left justify-start text-14-regular text-text-weak px-10" size="large" - onClick={loadMore} + onClick={(e: MouseEvent) => { + loadMore() + ;(e.currentTarget as HTMLButtonElement).blur() + }} > Load more From dc8f8cc567fc8454ec59b769835e9121de1c3fbc Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 15 Jan 2026 19:14:34 +0000 Subject: [PATCH 26/34] fix: current session background color --- packages/app/src/pages/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 1c653e06af..deb957f183 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -871,7 +871,7 @@ export default function Layout(props: ParentProps) {
Date: Thu, 15 Jan 2026 19:15:19 +0000 Subject: [PATCH 27/34] bun/package.json updates this may not be required --- bun.lock | 2 +- packages/ui/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index cf123051db..e11ac0d8f8 100644 --- a/bun.lock +++ b/bun.lock @@ -410,7 +410,7 @@ "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", "@typescript/native-preview": "catalog:", - "dompurify": "catalog:", + "dompurify": "3.3.1", "fuzzysort": "catalog:", "katex": "0.16.27", "luxon": "catalog:", diff --git a/packages/ui/package.json b/packages/ui/package.json index 143945e408..b2d7a414ec 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -48,10 +48,10 @@ "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", "@typescript/native-preview": "catalog:", + "dompurify": "3.3.1", "fuzzysort": "catalog:", "katex": "0.16.27", "luxon": "catalog:", - "dompurify": "catalog:", "marked": "catalog:", "marked-katex-extension": "5.1.6", "marked-shiki": "catalog:", From 4b2a14c154c53a6b540c13c0970068fcfde8b520 Mon Sep 17 00:00:00 2001 From: dbpolito Date: Thu, 15 Jan 2026 14:59:49 -0300 Subject: [PATCH 28/34] chore(desktop): Question Tools Updates --- packages/app/src/pages/layout.tsx | 49 +++++++++++++-------- packages/ui/src/components/message-part.css | 46 +++++++++++-------- 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index deb957f183..cf452e9b2a 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -161,53 +161,64 @@ export default function Layout(props: ParentProps) { }) onMount(() => { + const alerts = { + "permission.asked": { + title: "Permission required", + icon: "checklist" as const, + description: (sessionTitle: string, projectName: string) => + `${sessionTitle} in ${projectName} needs permission`, + }, + "question.asked": { + title: "Question", + icon: "bubble-5" as const, + description: (sessionTitle: string, projectName: string) => `${sessionTitle} in ${projectName} has a question`, + }, + } + const toastBySession = new Map() const alertedAtBySession = new Map() - const permissionAlertCooldownMs = 5000 + const cooldownMs = 5000 const unsub = globalSDK.event.listen((e) => { - if (e.details?.type !== "permission.asked") return + if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return + const config = alerts[e.details.type] const directory = e.name - const perm = e.details.properties - if (permission.autoResponds(perm, directory)) return + const props = e.details.properties + if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return const [store] = globalSync.child(directory) - const session = store.session.find((s) => s.id === perm.sessionID) - const sessionKey = `${directory}:${perm.sessionID}` + const session = store.session.find((s) => s.id === props.sessionID) + const sessionKey = `${directory}:${props.sessionID}` const sessionTitle = session?.title ?? "New session" const projectName = getFilename(directory) - const description = `${sessionTitle} in ${projectName} needs permission` - const href = `/${base64Encode(directory)}/session/${perm.sessionID}` + const description = config.description(sessionTitle, projectName) + const href = `/${base64Encode(directory)}/session/${props.sessionID}` const now = Date.now() const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0 - if (now - lastAlerted < permissionAlertCooldownMs) return + if (now - lastAlerted < cooldownMs) return alertedAtBySession.set(sessionKey, now) - void platform.notify("Permission required", description, href) + void platform.notify(config.title, description, href) const currentDir = params.dir ? base64Decode(params.dir) : undefined const currentSession = params.id - if (directory === currentDir && perm.sessionID === currentSession) return + if (directory === currentDir && props.sessionID === currentSession) return if (directory === currentDir && session?.parentID === currentSession) return const existingToastId = toastBySession.get(sessionKey) - if (existingToastId !== undefined) { - toaster.dismiss(existingToastId) - } + if (existingToastId !== undefined) toaster.dismiss(existingToastId) const toastId = showToast({ persistent: true, - icon: "checklist", - title: "Permission required", + icon: config.icon, + title: config.title, description, actions: [ { label: "Go to session", - onClick: () => { - navigate(href) - }, + onClick: () => navigate(href), }, { label: "Dismiss", diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 71d33de318..4b83c3a06a 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -416,7 +416,30 @@ box-shadow: var(--shadow-xs-border-base); background-color: var(--surface-raised-base); overflow: visible; + overflow-anchor: none; + & > *:first-child { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + overflow: hidden; + } + + & > *:last-child { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + overflow: hidden; + } + + [data-component="collapsible"] { + border: none; + } + + [data-component="card"] { + border: none; + } + } + + &[data-permission="true"] { &::before { content: ""; position: absolute; @@ -438,26 +461,11 @@ pointer-events: none; z-index: -1; } + } - & > *:first-child { - border-top-left-radius: 6px; - border-top-right-radius: 6px; - overflow: hidden; - } - - & > *:last-child { - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - overflow: hidden; - } - - [data-component="collapsible"] { - border: none; - } - - [data-component="card"] { - border: none; - } + &[data-question="true"] { + background: var(--background-base); + border: 1px solid var(--border-base); } } From e60ded01dfeef8b1813da8672ee0660292ad4036 Mon Sep 17 00:00:00 2001 From: dbpolito Date: Thu, 15 Jan 2026 15:13:18 -0300 Subject: [PATCH 29/34] chore(desktop): Stop Killing opencode-cli on dev --- packages/desktop/src-tauri/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 484c4a8667..3cfa8a9be5 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -223,7 +223,7 @@ async fn check_server_health(url: &str, password: Option<&str>) -> bool { pub fn run() { let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); - #[cfg(target_os = "macos")] + #[cfg(all(target_os = "macos", not(debug_assertions)))] let _ = std::process::Command::new("killall") .arg("opencode-cli") .output(); From d71153eae6c3218e45c6a32ca7becd4b13ae9414 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:38:09 -0600 Subject: [PATCH 30/34] fix(core): loading models.dev in dev --- packages/opencode/src/provider/models.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 5aedce505c..c5465f9880 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -81,7 +81,11 @@ export namespace ModelsDev { const file = Bun.file(filepath) const result = await file.json().catch(() => {}) if (result) return result as Record - const json = await data() + if (typeof data === "function") { + const json = await data() + return JSON.parse(json) as Record + } + const json = await fetch("https://models.dev/api.json").then((x) => x.text()) return JSON.parse(json) as Record } From b0345284f931ad4e90f1e6e0a07e67278f1e0a6a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:43:27 -0600 Subject: [PATCH 31/34] fix(core): filter dead worktrees --- packages/opencode/src/project/project.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 35fdd4717b..4fc724be71 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -272,7 +272,11 @@ export namespace Project { export async function list() { const keys = await Storage.list(["project"]) - return await Promise.all(keys.map((x) => Storage.read(x))) + const projects = await Promise.all(keys.map((x) => Storage.read(x))) + return projects.map((project) => ({ + ...project, + sandboxes: project.sandboxes.filter((x) => existsSync(x)), + })) } export const update = fn( From beb97d21ffcdc8d578cf1515f2d755e1e4c7959c Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:55:35 -0600 Subject: [PATCH 32/34] fix(app): show session busy even for active session --- packages/app/src/pages/layout.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index cf452e9b2a..d7a491aa9e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -859,7 +859,6 @@ export default function Layout(props: ParentProps) { return false }) const isWorking = createMemo(() => { - if (props.session.id === params.id) return false if (hasPermissions()) return false const status = sessionStore.session_status[props.session.id] return status?.type === "busy" || status?.type === "retry" From 7e016fdda6199d668c203ca2f281853de1b1f62f Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:34:53 -0600 Subject: [PATCH 33/34] chore: cleanup --- packages/ui/src/components/message-part.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 4b83c3a06a..4a249ec4f4 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -33,7 +33,7 @@ border-radius: 6px; overflow: hidden; background: var(--surface-base); - border: 1px solid var(--border-base); + border: 1px solid var(--border-weak-base); transition: border-color 0.15s ease; &:hover { @@ -465,7 +465,7 @@ &[data-question="true"] { background: var(--background-base); - border: 1px solid var(--border-base); + border: 1px solid var(--border-weak-base); } } From af2a09940c5b411c27f72c039a0351a3c19efd02 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:58:39 -0600 Subject: [PATCH 34/34] fix(core): more defensive project list --- packages/opencode/src/project/project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 4fc724be71..72201636b7 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -275,7 +275,7 @@ export namespace Project { const projects = await Promise.all(keys.map((x) => Storage.read(x))) return projects.map((project) => ({ ...project, - sandboxes: project.sandboxes.filter((x) => existsSync(x)), + sandboxes: project.sandboxes?.filter((x) => existsSync(x)), })) }