From bec294b7817e4a9fd9e0fef45d70522957b4045b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:13:08 -0600 Subject: [PATCH 01/32] fix(app): remove copy button from summary --- packages/ui/src/components/session-turn.tsx | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index a918f0ae4fd8..360589f4111e 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -6,7 +6,6 @@ import { type PermissionRequest, TextPart, ToolPart, - UserMessage, } from "@opencode-ai/sdk/v2/client" import { useData } from "../context" import { useDiffComponent } from "../context/diff" @@ -21,8 +20,6 @@ import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" import { FileIcon } from "./file-icon" import { Icon } from "./icon" -import { IconButton } from "./icon-button" -import { Tooltip } from "./tooltip" import { Card } from "./card" import { Dynamic } from "solid-js/web" import { Button } from "./button" @@ -352,7 +349,6 @@ export function SessionTurn( const hasDiffs = createMemo(() => (data.store.session_diff?.[props.sessionID]?.length ?? 0) > 0) const hideResponsePart = createMemo(() => !working() && !!responsePartId()) - const [responseCopied, setResponseCopied] = createSignal(false) const [rootRef, setRootRef] = createSignal() const [stickyRef, setStickyRef] = createSignal() @@ -362,13 +358,6 @@ export function SessionTurn( const next = Math.ceil(height) root.style.setProperty("--session-turn-sticky-height", `${next}px`) } - const handleCopyResponse = async () => { - const content = response() - if (!content) return - await navigator.clipboard.writeText(content) - setResponseCopied(true) - setTimeout(() => setResponseCopied(false), 2000) - } function duration() { const msg = message() @@ -589,15 +578,6 @@ export function SessionTurn( {/* Response */}
-
- - - -

Response

Date: Mon, 19 Jan 2026 22:13:58 +0000 Subject: [PATCH 02/32] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 52f392a81791..ee7cf23b9f1b 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 5ce97dbf7e71..f69224d832a2 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From aa4b06e16548d5a1a5e8c32376987f6aa70c9844 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 19 Jan 2026 18:22:19 -0500 Subject: [PATCH 03/32] tui: fix message history cleanup to prevent memory leaks --- .../opencode/src/cli/cmd/tui/context/sync.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 0edc911344c3..392cfb7f1218 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -241,9 +241,27 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ event.properties.info.sessionID, produce((draft) => { draft.splice(result.index, 0, event.properties.info) - if (draft.length > 100) draft.shift() }), ) + const updated = store.message[event.properties.info.sessionID] + if (updated.length > 100) { + const oldest = updated[0] + batch(() => { + setStore( + "message", + event.properties.info.sessionID, + produce((draft) => { + draft.shift() + }), + ) + setStore( + "part", + produce((draft) => { + delete draft[oldest.id] + }), + ) + }) + } break } case "message.removed": { From bfa986d45e31eeeb66b86201c5e8fb470949677e Mon Sep 17 00:00:00 2001 From: DNGriffin <31415269+DNGriffin@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:38:52 -0600 Subject: [PATCH 04/32] feat(app): Add ability to select project directory text to web (#9344) --- 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 d0d7e0b2f0ca..e0cc38b9af1a 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1967,7 +1967,7 @@ export default function Layout(props: ParentProps) { transform: "translate3d(52px, 0, 0)", }} > - + {project()?.worktree.replace(homedir(), "~")} From 054ccee78daf78cf33ba01c758e844cfab8c385a Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 00:15:10 +0000 Subject: [PATCH 05/32] update review session empty state styling --- packages/app/src/pages/session.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index ec3b0ac30d9d..700d2b695a99 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1248,9 +1248,9 @@ export default function Page() { -
- -
No changes in this session yet.
+
+ +
No changes in this session yet
@@ -1524,9 +1524,9 @@ export default function Page() { -
- -
No changes in this session yet.
+
+ +
No changes in this session yet
From cf284e32aaab6e9b29d8bf919cdf5f13d95c5a8d Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 00:21:11 +0000 Subject: [PATCH 06/32] update session hover popover styling --- packages/app/src/pages/layout.tsx | 2 +- packages/ui/src/components/hover-card.css | 2 +- packages/ui/src/components/message-nav.css | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index e0cc38b9af1a..0ef608a4a8c4 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1421,7 +1421,7 @@ export default function Layout(props: ParentProps) { } > - + Loading messages…
}> Date: Tue, 20 Jan 2026 00:24:51 +0000 Subject: [PATCH 07/32] retain session hover state when popover open and update border radius --- packages/app/src/pages/layout.tsx | 2 +- packages/ui/src/components/hover-card.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 0ef608a4a8c4..62cd15709244 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1411,7 +1411,7 @@ export default function Layout(props: ParentProps) {
Date: Tue, 20 Jan 2026 00:27:03 +0000 Subject: [PATCH 08/32] position session messages popover at top --- 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 62cd15709244..23f1f05ec70f 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1421,7 +1421,7 @@ export default function Layout(props: ParentProps) { } > - + Loading messages…
}> Date: Tue, 20 Jan 2026 00:42:55 +0000 Subject: [PATCH 09/32] update thinking text styling in desktop app --- packages/ui/src/components/message-part.css | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 184565e9cb32..a5dbdf36d060 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -113,11 +113,22 @@ [data-component="reasoning-part"] { width: 100%; - opacity: 0.5; + color: var(--text-base); + opacity: 0.8; + line-height: var(--line-height-large); [data-component="markdown"] { margin-top: 24px; font-style: italic !important; + + p:has(strong) { + margin-top: 24px; + margin-bottom: 0; + + &:first-child { + margin-top: 0; + } + } } } From 7b336add88d516f44f1b71206973fdb850191113 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 01:21:36 +0000 Subject: [PATCH 10/32] update session messages popover gutter to 28px --- 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 23f1f05ec70f..2e1bbd331781 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1421,7 +1421,7 @@ export default function Layout(props: ParentProps) { } > - + Loading messages…
}> Date: Tue, 20 Jan 2026 01:36:34 +0000 Subject: [PATCH 11/32] remove top padding from edit project dialog form --- 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 7acb766f8086..a2dc7b623c7d 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -82,7 +82,7 @@ export function DialogEditProject(props: { project: LocalProject }) { return ( -
+
Date: Tue, 20 Jan 2026 01:41:36 +0000 Subject: [PATCH 12/32] add 8px padding to recent sessions popover --- 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 2e1bbd331781..dc716c03058c 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1730,7 +1730,7 @@ export default function Layout(props: ParentProps) { trigger={trigger} onOpenChange={setOpen} > -
+
{displayName(props.project)}
Recent sessions
From 4ddfa86e7fe8d6eb23ab973fdf65175cd8a750a7 Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Tue, 20 Jan 2026 02:41:42 +0100 Subject: [PATCH 13/32] fix(app): message list overflow & scrolling (#9530) --- packages/app/src/pages/session.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 700d2b695a99..458585e16957 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -824,10 +824,22 @@ export default function Page() { }) const isWorking = createMemo(() => status().type !== "idle") + const autoScroll = createAutoScroll({ - working: isWorking, + working: () => true }) + createEffect( + on( + isWorking, + (working, prev) => { + if (!working || prev) return + autoScroll.forceScrollToBottom() + }, + { defer: true }, + ), + ) + let scrollSpyFrame: number | undefined let scrollSpyTarget: HTMLDivElement | undefined @@ -1340,10 +1352,6 @@ export default function Page() { classList={{ "min-w-0 w-full max-w-full": true, "md:max-w-200": !showTabs(), - "last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]": - platform.platform !== "desktop", - "last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]": - platform.platform === "desktop", }} > Date: Tue, 20 Jan 2026 01:42:14 +0000 Subject: [PATCH 14/32] chore: generate --- packages/app/src/pages/session.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 458585e16957..89d3ec773347 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -826,7 +826,7 @@ export default function Page() { const isWorking = createMemo(() => status().type !== "idle") const autoScroll = createAutoScroll({ - working: () => true + working: () => true, }) createEffect( From 36f5ba52e9c7dc657fa7c3c856e1bafd827cd314 Mon Sep 17 00:00:00 2001 From: James Meng <35415298+jamesmengo@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:15:02 -0800 Subject: [PATCH 15/32] fix(batch): update batch tool definition to outline correct value for max tool calls (#9517) --- packages/opencode/src/tool/batch.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/batch.txt b/packages/opencode/src/tool/batch.txt index 565eb4dd4336..968a6c3f07c6 100644 --- a/packages/opencode/src/tool/batch.txt +++ b/packages/opencode/src/tool/batch.txt @@ -6,7 +6,7 @@ Payload Format (JSON array): [{"tool": "read", "parameters": {"filePath": "src/index.ts", "limit": 350}},{"tool": "grep", "parameters": {"pattern": "Session\\.updatePart", "include": "src/**/*.ts"}},{"tool": "bash", "parameters": {"command": "git status", "description": "Shows working tree status"}}] Notes: -- 1–20 tool calls per batch +- 1–25 tool calls per batch - All calls start in parallel; ordering NOT guaranteed - Partial failures do not stop other tool calls - Do NOT use the batch tool within another batch tool. From 0d49df46ef56828f3a310d1229a9ae95debff127 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 23:19:21 -0600 Subject: [PATCH 16/32] fix: ensure truncation handling applies to mcp servers too --- packages/opencode/src/session/prompt.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f4793d1a7987..9dbca30d8b35 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -44,6 +44,7 @@ import { SessionStatus } from "./status" import { LLM } from "./llm" import { iife } from "@/util/iife" import { Shell } from "@/shell/shell" +import { Truncate } from "@/tool/truncation" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -801,10 +802,17 @@ export namespace SessionPrompt { } } + const truncated = await Truncate.output(textParts.join("\n\n"), {}, input.agent) + const metadata = { + ...(result.metadata ?? {}), + truncated: truncated.truncated, + ...(truncated.truncated && { outputPath: truncated.outputPath }), + } + return { title: "", - metadata: result.metadata ?? {}, - output: textParts.join("\n\n"), + metadata, + output: truncated.content, attachments, content: result.content, // directly return content to preserve ordering when outputting to model } From 419004992d57aacab45dd17f6c435fbc12f0b910 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 23:22:57 -0600 Subject: [PATCH 17/32] chore: remove duplicate prompt file --- .../opencode/src/session/prompt/codex.txt | 73 ------------------- .../src/session/prompt/codex_header.txt | 1 + packages/opencode/src/session/system.ts | 5 +- 3 files changed, 3 insertions(+), 76 deletions(-) delete mode 100644 packages/opencode/src/session/prompt/codex.txt diff --git a/packages/opencode/src/session/prompt/codex.txt b/packages/opencode/src/session/prompt/codex.txt deleted file mode 100644 index daad82377581..000000000000 --- a/packages/opencode/src/session/prompt/codex.txt +++ /dev/null @@ -1,73 +0,0 @@ -You are OpenCode, the best coding agent on the planet. - -You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. - -## Editing constraints -- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them. -- Only add comments if they are necessary to make a non-obvious block easier to understand. -- Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase). - -## Tool usage -- Prefer specialized tools over shell for file operations: - - Use Read to view files, Edit to modify files, and Write only when needed. - - Use Glob to find files by name and Grep to search file contents. -- Use Bash for terminal operations (git, bun, builds, tests, running scripts). -- Run tool calls in parallel when neither call needs the other’s output; otherwise run sequentially. - -## Git and workspace hygiene -- You may be in a dirty git worktree. - * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user. - * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes. - * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them. - * If the changes are in unrelated files, just ignore them and don't revert them. -- Do not amend commits unless explicitly requested. -- **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user. - -## Frontend tasks -When doing frontend design tasks, avoid collapsing into bland, generic layouts. -Aim for interfaces that feel intentional and deliberate. -- Typography: Use expressive, purposeful fonts and avoid default stacks (Inter, Roboto, Arial, system). -- Color & Look: Choose a clear visual direction; define CSS variables; avoid purple-on-white defaults. No purple bias or dark mode bias. -- Motion: Use a few meaningful animations (page-load, staggered reveals) instead of generic micro-motions. -- Background: Don't rely on flat, single-color backgrounds; use gradients, shapes, or subtle patterns to build atmosphere. -- Overall: Avoid boilerplate layouts and interchangeable UI patterns. Vary themes, type families, and visual languages across outputs. -- Ensure the page loads properly on both desktop and mobile. - -Exception: If working within an existing website or design system, preserve the established patterns, structure, and visual language. - -## Presenting your work and final message - -You are producing plain text that will later be styled by the CLI. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value. - -- Default: be very concise; friendly coding teammate tone. -- Ask only when needed; suggest ideas; mirror the user's style. -- For substantial work, summarize clearly; follow final‑answer formatting. -- Skip heavy formatting for simple confirmations. -- Don't dump large files you've written; reference paths only. -- No "save/copy this file" - User is on the same machine. -- Offer logical next steps (tests, commits, build) briefly; add verify steps if you couldn't do something. -- For code changes: - * Lead with a quick explanation of the change, and then give more details on the context covering where and why a change was made. Do not start this explanation with "summary", just jump right in. - * If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps. - * When suggesting multiple options, use numeric lists for the suggestions so the user can quickly respond with a single number. -- The user does not command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result. - -## Final answer structure and style guidelines - -- Plain text; CLI handles styling. Use structure only when it helps scanability. -- Headers: optional; short Title Case (1-3 words) wrapped in **…**; no blank line before the first bullet; add only if they truly help. -- Bullets: use - ; merge related points; keep to one line when possible; 4–6 per list ordered by importance; keep phrasing consistent. -- Monospace: backticks for commands/paths/env vars/code ids and inline examples; use for literal keyword bullets; never combine with **. -- Code samples or multi-line snippets should be wrapped in fenced code blocks; include an info string as often as possible. -- Structure: group related bullets; order sections general → specific → supporting; for subsections, start with a bolded keyword bullet, then items; match complexity to the task. -- Tone: collaborative, concise, factual; present tense, active voice; self‑contained; no "above/below"; parallel wording. -- Don'ts: no nested bullets/hierarchies; no ANSI codes; don't cram unrelated keywords; keep keyword lists short—wrap/reformat if long; avoid naming formatting styles in answers. -- Adaptation: code explanations → precise, structured with code refs; simple tasks → lead with outcome; big changes → logical walkthrough + rationale + next actions; casual one-offs → plain sentences, no headers/bullets. -- File References: When referencing files in your response follow the below rules: - * Use inline code to make file paths clickable. - * Each reference should have a stand alone path. Even if it's the same file. - * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix. - * Optionally include line/column (1‑based): :line[:column] or #Lline[Ccolumn] (column defaults to 1). - * Do not use URIs like file://, vscode://, or https://. - * Do not provide range of lines - * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 diff --git a/packages/opencode/src/session/prompt/codex_header.txt b/packages/opencode/src/session/prompt/codex_header.txt index d26e2e01aa7e..daad82377581 100644 --- a/packages/opencode/src/session/prompt/codex_header.txt +++ b/packages/opencode/src/session/prompt/codex_header.txt @@ -5,6 +5,7 @@ You are an interactive CLI tool that helps users with software engineering tasks ## Editing constraints - Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them. - Only add comments if they are necessary to make a non-obvious block easier to understand. +- Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase). ## Tool usage - Prefer specialized tools over shell for file operations: diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index fff90808864b..f0e2d96b7ebf 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -13,8 +13,7 @@ import PROMPT_BEAST from "./prompt/beast.txt" import PROMPT_GEMINI from "./prompt/gemini.txt" import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt" -import PROMPT_CODEX from "./prompt/codex.txt" -import PROMPT_CODEX_INSTRUCTIONS from "./prompt/codex_header.txt" +import PROMPT_CODEX from "./prompt/codex_header.txt" import type { Provider } from "@/provider/provider" import { Flag } from "@/flag/flag" @@ -25,7 +24,7 @@ export namespace SystemPrompt { } export function instructions() { - return PROMPT_CODEX_INSTRUCTIONS.trim() + return PROMPT_CODEX.trim() } export function provider(model: Provider.Model) { From 68d1755a9ed49b2bbf29964db295817349025563 Mon Sep 17 00:00:00 2001 From: Craig Jellick Date: Mon, 19 Jan 2026 22:38:26 -0700 Subject: [PATCH 18/32] fix: add space toggle hint to tool selection prompt (#9535) --- packages/opencode/src/cli/cmd/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index b57de0ae464e..e5da9fdb386c 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -134,7 +134,7 @@ const AgentCreateCommand = cmd({ selectedTools = cliTools ? cliTools.split(",").map((t) => t.trim()) : AVAILABLE_TOOLS } else { const result = await prompts.multiselect({ - message: "Select tools to enable", + message: "Select tools to enable (Space to toggle)", options: AVAILABLE_TOOLS.map((tool) => ({ label: tool, value: tool, From 8b379329a6dcd0021709e8486c78acaf7bb52a21 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 20 Jan 2026 14:07:39 +0800 Subject: [PATCH 19/32] fix(desktop): completely disable pinch to zoom --- packages/desktop/src-tauri/tauri.conf.json | 2 +- packages/desktop/src/index.tsx | 6 +---- packages/desktop/src/webview-zoom.ts | 31 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 packages/desktop/src/webview-zoom.ts diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json index 19da295c56b9..f8df151bdf84 100644 --- a/packages/desktop/src-tauri/tauri.conf.json +++ b/packages/desktop/src-tauri/tauri.conf.json @@ -19,7 +19,7 @@ "url": "/", "decorations": true, "dragDropEnabled": false, - "zoomHotkeysEnabled": true, + "zoomHotkeysEnabled": false, "titleBarStyle": "Overlay", "hiddenTitle": true, "trafficLightPosition": { "x": 12.0, "y": 18.0 } diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index a06270b13fe6..d6ee121af6d6 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -1,4 +1,5 @@ // @refresh reload +import "./webview-zoom" import { render } from "solid-js/web" import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app" import { open, save } from "@tauri-apps/plugin-dialog" @@ -303,11 +304,6 @@ const createPlatform = (password: Accessor): Platform => ({ createMenu() -// Stops mousewheel events from reaching Tauri's pinch-to-zoom handler -root?.addEventListener("mousewheel", (e) => { - e.stopPropagation() -}) - render(() => { const [serverPassword, setServerPassword] = createSignal(null) const platform = createPlatform(() => serverPassword()) diff --git a/packages/desktop/src/webview-zoom.ts b/packages/desktop/src/webview-zoom.ts new file mode 100644 index 000000000000..9fa9bb9ed92a --- /dev/null +++ b/packages/desktop/src/webview-zoom.ts @@ -0,0 +1,31 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { invoke } from "@tauri-apps/api/core" +import { type as ostype } from "@tauri-apps/plugin-os" + +const OS_NAME = ostype() + +let zoomLevel = 1 + +const MAX_ZOOM_LEVEL = 10 +const MIN_ZOOM_LEVEL = 0.2 + +window.addEventListener("keydown", (event) => { + if (OS_NAME === "macos" ? event.metaKey : event.ctrlKey) { + if (event.key === "-") { + zoomLevel -= 0.2 + } else if (event.key === "=" || event.key === "+") { + zoomLevel += 0.2 + } else if (event.key === "0") { + zoomLevel = 1 + } else { + return + } + zoomLevel = Math.min(Math.max(zoomLevel, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL) + invoke("plugin:webview|set_webview_zoom", { + value: zoomLevel, + }) + } +}) From 9706aaf552c3d1c85160e6ba5d107207019b931a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 20 Jan 2026 00:32:29 -0600 Subject: [PATCH 20/32] rm filetime assertions from patch tool --- packages/opencode/src/tool/apply_patch.ts | 8 -------- packages/opencode/test/tool/apply_patch.test.ts | 16 ---------------- 2 files changed, 24 deletions(-) diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index 7b0ba6150ce4..ec24643fcfab 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -2,7 +2,6 @@ import z from "zod" import * as path from "path" import * as fs from "fs/promises" import { Tool } from "./tool" -import { FileTime } from "../file/time" import { Bus } from "../bus" import { FileWatcher } from "../file/watcher" import { Instance } from "../project/instance" @@ -96,8 +95,6 @@ export const ApplyPatchTool = Tool.define("apply_patch", { throw new Error(`apply_patch verification failed: Failed to read file to update: ${filePath}`) } - // Read file and update time tracking (like edit tool does) - await FileTime.assert(ctx.sessionID, filePath) const oldContent = await fs.readFile(filePath, "utf-8") let newContent = oldContent @@ -203,11 +200,6 @@ export const ApplyPatchTool = Tool.define("apply_patch", { break } - // Update file time tracking - FileTime.read(ctx.sessionID, change.filePath) - if (change.movePath) { - FileTime.read(ctx.sessionID, change.movePath) - } } // Publish file change events diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index d8f05a9d911e..6445c6845b56 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -3,7 +3,6 @@ import path from "path" import * as fs from "fs/promises" import { ApplyPatchTool } from "../../src/tool/apply_patch" import { Instance } from "../../src/project/instance" -import { FileTime } from "../../src/file/time" import { tmpdir } from "../fixture/fixture" const baseCtx = { @@ -71,8 +70,6 @@ describe("tool.apply_patch freeform", () => { const deletePath = path.join(fixture.path, "delete.txt") await fs.writeFile(modifyPath, "line1\nline2\n", "utf-8") await fs.writeFile(deletePath, "obsolete\n", "utf-8") - FileTime.read(ctx.sessionID, modifyPath) - FileTime.read(ctx.sessionID, deletePath) const patchText = "*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Delete File: delete.txt\n*** Update File: modify.txt\n@@\n-line2\n+changed\n*** End Patch" @@ -101,7 +98,6 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "multi.txt") await fs.writeFile(target, "line1\nline2\nline3\nline4\n", "utf-8") - FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch" @@ -122,7 +118,6 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "insert_only.txt") await fs.writeFile(target, "alpha\nomega\n", "utf-8") - FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: insert_only.txt\n@@\n alpha\n+beta\n omega\n*** End Patch" @@ -142,7 +137,6 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "no_newline.txt") await fs.writeFile(target, "no newline at end", "utf-8") - FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: no_newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch" @@ -166,7 +160,6 @@ describe("tool.apply_patch freeform", () => { const original = path.join(fixture.path, "old", "name.txt") await fs.mkdir(path.dirname(original), { recursive: true }) await fs.writeFile(original, "old content\n", "utf-8") - FileTime.read(ctx.sessionID, original) const patchText = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch" @@ -193,7 +186,6 @@ describe("tool.apply_patch freeform", () => { await fs.mkdir(path.dirname(destination), { recursive: true }) await fs.writeFile(original, "from\n", "utf-8") await fs.writeFile(destination, "existing\n", "utf-8") - FileTime.read(ctx.sessionID, original) const patchText = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-from\n+new\n*** End Patch" @@ -294,7 +286,6 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "modify.txt") await fs.writeFile(target, "line1\nline2\n", "utf-8") - FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: modify.txt\n@@\n-missing\n+changed\n*** End Patch" @@ -331,7 +322,6 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "tail.txt") await fs.writeFile(target, "alpha\nlast\n", "utf-8") - FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch" @@ -350,7 +340,6 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "two_chunks.txt") await fs.writeFile(target, "a\nb\nc\nd\n", "utf-8") - FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: two_chunks.txt\n@@\n-b\n+B\n\n-d\n+D\n*** End Patch" @@ -369,7 +358,6 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "multi_ctx.txt") await fs.writeFile(target, "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n", "utf-8") - FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: multi_ctx.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch" @@ -389,7 +377,6 @@ describe("tool.apply_patch freeform", () => { const target = path.join(fixture.path, "eof_anchor.txt") // File has duplicate "marker" lines - one in middle, one at end await fs.writeFile(target, "start\nmarker\nmiddle\nmarker\nend\n", "utf-8") - FileTime.read(ctx.sessionID, target) // With EOF anchor, should match the LAST "marker" line, not the first const patchText = @@ -454,7 +441,6 @@ EOF` const target = path.join(fixture.path, "trailing_ws.txt") // File has trailing spaces on some lines await fs.writeFile(target, "line1 \nline2\nline3 \n", "utf-8") - FileTime.read(ctx.sessionID, target) // Patch doesn't have trailing spaces - should still match via rstrip pass const patchText = "*** Begin Patch\n*** Update File: trailing_ws.txt\n@@\n-line2\n+changed\n*** End Patch" @@ -475,7 +461,6 @@ EOF` const target = path.join(fixture.path, "leading_ws.txt") // File has leading spaces await fs.writeFile(target, " line1\nline2\n line3\n", "utf-8") - FileTime.read(ctx.sessionID, target) // Patch without leading spaces - should match via trim pass const patchText = "*** Begin Patch\n*** Update File: leading_ws.txt\n@@\n-line2\n+changed\n*** End Patch" @@ -499,7 +484,6 @@ EOF` const rightQuote = "\u201D" const emDash = "\u2014" await fs.writeFile(target, `He said ${leftQuote}hello${rightQuote}\nsome${emDash}dash\nend\n`, "utf-8") - FileTime.read(ctx.sessionID, target) // Patch uses ASCII equivalents - should match via normalized pass // The replacement uses ASCII quotes from the patch (not preserving Unicode) From 616329ae97c975f21687f6c8c9c6d4d8018e6fd9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 20 Jan 2026 06:33:16 +0000 Subject: [PATCH 21/32] chore: generate --- packages/opencode/src/tool/apply_patch.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index ec24643fcfab..cccf2d1cf46f 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -199,7 +199,6 @@ export const ApplyPatchTool = Tool.define("apply_patch", { changedFiles.push(change.filePath) break } - } // Publish file change events From 5f0372183a1f7447de111e170b915783823f4b11 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:41:24 -0600 Subject: [PATCH 22/32] fix(app): persist quota --- packages/app/src/utils/persist.ts | 51 +++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index 0c20ee31ca67..06e80142a59f 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -17,6 +17,36 @@ type PersistTarget = { const LEGACY_STORAGE = "default.dat" const GLOBAL_STORAGE = "opencode.global.dat" +function quota(error: unknown) { + if (error instanceof DOMException) { + if (error.name === "QuotaExceededError") return true + if (error.name === "NS_ERROR_DOM_QUOTA_REACHED") return true + if (error.code === 22 || error.code === 1014) return true + return false + } + + if (!error || typeof error !== "object") return false + const name = (error as { name?: string }).name + if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return true + return false +} + +function write(storage: Storage, key: string, value: string) { + try { + storage.setItem(key, value) + return + } catch (error) { + if (!quota(error)) throw error + } + + try { + storage.removeItem(key) + storage.setItem(key, value) + } catch (error) { + if (!quota(error)) throw error + } +} + function snapshot(value: unknown) { return JSON.parse(JSON.stringify(value)) as unknown } @@ -67,10 +97,19 @@ function workspaceStorage(dir: string) { function localStorageWithPrefix(prefix: string): SyncStorage { const base = `${prefix}:` + const item = (key: string) => base + key + return { + getItem: (key) => localStorage.getItem(item(key)), + setItem: (key, value) => write(localStorage, item(key), value), + removeItem: (key) => localStorage.removeItem(item(key)), + } +} + +function localStorageDirect(): SyncStorage { return { - getItem: (key) => localStorage.getItem(base + key), - setItem: (key, value) => localStorage.setItem(base + key, value), - removeItem: (key) => localStorage.removeItem(base + key), + getItem: (key) => localStorage.getItem(key), + setItem: (key, value) => write(localStorage, key, value), + removeItem: (key) => localStorage.removeItem(key), } } @@ -99,7 +138,7 @@ export function removePersisted(target: { storage?: string; key: string }) { } if (!target.storage) { - localStorage.removeItem(target.key) + localStorageDirect().removeItem(target.key) return } @@ -120,12 +159,12 @@ export function persisted( const currentStorage = (() => { if (isDesktop) return platform.storage?.(config.storage) - if (!config.storage) return localStorage + if (!config.storage) return localStorageDirect() return localStorageWithPrefix(config.storage) })() const legacyStorage = (() => { - if (!isDesktop) return localStorage + if (!isDesktop) return localStorageDirect() if (!config.storage) return platform.storage?.() return platform.storage?.(LEGACY_STORAGE) })() From 353115a895655f3d9f3075cd0516000722e9c6b5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 20 Jan 2026 05:04:38 -0600 Subject: [PATCH 23/32] fix(app): user message expand on click --- packages/ui/src/components/message-part.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index b3fd01c2d8e2..24b1ee393264 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -357,6 +357,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp setTimeout(() => setCopied(false), 2000) } + const toggleExpanded = () => { + if (!canExpand()) return + setExpanded((value) => !value) + } + return (
0}> @@ -388,19 +393,29 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
-
(textRef = el)}> +
(textRef = el)} onClick={toggleExpanded}>
- + { + event.stopPropagation() + handleCopy() + }} + />
From b711ca57f25f393e2613e46ab5bfe2a95c42ee0d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 20 Jan 2026 05:21:27 -0600 Subject: [PATCH 24/32] fix(app): localStorage quota --- packages/app/src/utils/persist.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index 06e80142a59f..4ada0751d76b 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -21,6 +21,7 @@ function quota(error: unknown) { if (error instanceof DOMException) { if (error.name === "QuotaExceededError") return true if (error.name === "NS_ERROR_DOM_QUOTA_REACHED") return true + if (error.name === "QUOTA_EXCEEDED_ERR") return true if (error.code === 22 || error.code === 1014) return true return false } @@ -28,6 +29,14 @@ function quota(error: unknown) { if (!error || typeof error !== "object") return false const name = (error as { name?: string }).name if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return true + if (name && /quota/i.test(name)) return true + + const code = (error as { code?: number }).code + if (code === 22 || code === 1014) return true + + const message = (error as { message?: string }).message + if (typeof message !== "string") return false + if (/quota/i.test(message)) return true return false } From 347cd8ac63314ef034f434615274a905b088e14b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 20 Jan 2026 05:35:24 -0600 Subject: [PATCH 25/32] chore: cleanup --- packages/app/src/utils/persist.ts | 99 +++++++++++++++++++-- packages/ui/src/components/message-part.tsx | 7 +- 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index 4ada0751d76b..70884977c759 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -16,6 +16,9 @@ type PersistTarget = { const LEGACY_STORAGE = "default.dat" const GLOBAL_STORAGE = "opencode.global.dat" +const LOCAL_PREFIX = "opencode." +const fallback = { disabled: false } +const cache = new Map() function quota(error: unknown) { if (error instanceof DOMException) { @@ -40,10 +43,42 @@ function quota(error: unknown) { return false } +type Evict = { key: string; size: number } + +function evict(storage: Storage, keep: string, value: string) { + const total = storage.length + const indexes = Array.from({ length: total }, (_, index) => index) + const items: Evict[] = [] + + for (const index of indexes) { + const name = storage.key(index) + if (!name) continue + if (!name.startsWith(LOCAL_PREFIX)) continue + if (name === keep) continue + const stored = storage.getItem(name) + items.push({ key: name, size: stored?.length ?? 0 }) + } + + items.sort((a, b) => b.size - a.size) + + for (const item of items) { + storage.removeItem(item.key) + + try { + storage.setItem(keep, value) + return true + } catch (error) { + if (!quota(error)) throw error + } + } + + return false +} + function write(storage: Storage, key: string, value: string) { try { storage.setItem(key, value) - return + return true } catch (error) { if (!quota(error)) throw error } @@ -51,9 +86,12 @@ function write(storage: Storage, key: string, value: string) { try { storage.removeItem(key) storage.setItem(key, value) + return true } catch (error) { if (!quota(error)) throw error } + + return evict(storage, key, value) } function snapshot(value: unknown) { @@ -108,17 +146,64 @@ function localStorageWithPrefix(prefix: string): SyncStorage { const base = `${prefix}:` const item = (key: string) => base + key return { - getItem: (key) => localStorage.getItem(item(key)), - setItem: (key, value) => write(localStorage, item(key), value), - removeItem: (key) => localStorage.removeItem(item(key)), + getItem: (key) => { + const name = item(key) + const cached = cache.get(name) + if (fallback.disabled && cached !== undefined) return cached + + const stored = localStorage.getItem(name) + if (stored === null) return cached ?? null + cache.set(name, stored) + return stored + }, + setItem: (key, value) => { + const name = item(key) + cache.set(name, value) + if (fallback.disabled) return + try { + if (write(localStorage, name, value)) return + } catch { + fallback.disabled = true + return + } + fallback.disabled = true + }, + removeItem: (key) => { + const name = item(key) + cache.delete(name) + if (fallback.disabled) return + localStorage.removeItem(name) + }, } } function localStorageDirect(): SyncStorage { return { - getItem: (key) => localStorage.getItem(key), - setItem: (key, value) => write(localStorage, key, value), - removeItem: (key) => localStorage.removeItem(key), + getItem: (key) => { + const cached = cache.get(key) + if (fallback.disabled && cached !== undefined) return cached + + const stored = localStorage.getItem(key) + if (stored === null) return cached ?? null + cache.set(key, stored) + return stored + }, + setItem: (key, value) => { + cache.set(key, value) + if (fallback.disabled) return + try { + if (write(localStorage, key, value)) return + } catch { + fallback.disabled = true + return + } + fallback.disabled = true + }, + removeItem: (key) => { + cache.delete(key) + if (fallback.disabled) return + localStorage.removeItem(key) + }, } } diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 24b1ee393264..add10fea8bea 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -363,7 +363,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp } return ( -
+
0}>
@@ -371,7 +371,8 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
{ + onClick={(event) => { + event.stopPropagation() if (file.mime.startsWith("image/") && file.url) { openImagePreview(file.url, file.filename) } @@ -393,7 +394,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
-
(textRef = el)} onClick={toggleExpanded}> +
(textRef = el)}>