diff --git a/.github/last-synced-tag b/.github/last-synced-tag index 7f9004ba0b8a..be0ee501c637 100644 --- a/.github/last-synced-tag +++ b/.github/last-synced-tag @@ -1 +1 @@ -v1.1.26 +v1.1.27 diff --git a/STATS.md b/STATS.md index 88046dd8f341..a7af9bd251ad 100644 --- a/STATS.md +++ b/STATS.md @@ -205,3 +205,4 @@ | 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) | | 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) | | 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) | +| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) | diff --git a/packages/app/package.json b/packages/app/package.json index 5694e2771a4c..5ca9a82c26e1 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.26", + "version": "1.1.27", "description": "", "type": "module", "exports": { 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 ( -
+
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 @@ -1127,26 +1139,36 @@ export default function Page() { when={!mobileReview()} fallback={
- Loading changes...
} - > - { - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - classes={{ - root: "pb-[calc(var(--prompt-height,8rem)+32px)]", - header: "px-4", - container: "px-4", - }} - /> - + + + Loading changes...
} + > + { + const value = file.tab(path) + tabs().open(value) + file.load(path) + }} + classes={{ + root: "pb-[calc(var(--prompt-height,8rem)+32px)]", + header: "px-4", + container: "px-4", + }} + /> + + + +
+ +
No changes in this session yet
+
+
+ } > @@ -1214,6 +1236,7 @@ export default function Page() { data-message-id={message.id} 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)]": @@ -1398,22 +1421,32 @@ export default function Page() {
- Loading changes...
} - > - { - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - /> -
+ + + Loading changes...} + > + { + const value = file.tab(path) + tabs().open(value) + file.load(path) + }} + /> + + + +
+ +
No changes in this session yet
+
+
+
diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index 0c20ee31ca67..70884977c759 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -16,6 +16,83 @@ 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) { + 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 + } + + 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 +} + +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 true + } catch (error) { + if (!quota(error)) throw error + } + + 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) { return JSON.parse(JSON.stringify(value)) as unknown @@ -67,10 +144,66 @@ function workspaceStorage(dir: string) { function localStorageWithPrefix(prefix: string): SyncStorage { const base = `${prefix}:` + const item = (key: string) => base + key + return { + 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(base + key), - setItem: (key, value) => localStorage.setItem(base + key, value), - removeItem: (key) => localStorage.removeItem(base + 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) + }, } } @@ -99,7 +232,7 @@ export function removePersisted(target: { storage?: string; key: string }) { } if (!target.storage) { - localStorage.removeItem(target.key) + localStorageDirect().removeItem(target.key) return } @@ -120,12 +253,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) })() diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 83ff605cffaa..7638b54d2db0 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.26", + "version": "1.1.27", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index eb86fb525b19..f5620a54f388 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.1.26", + "version": "1.1.27", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 6cfdaab7579f..0a79603fdc4f 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.26", + "version": "1.1.27", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index ab2fd76f8b18..fd1f5702d5e6 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.26", + "version": "1.1.27", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index f0a6a369974d..9024a6b61009 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@shuvcode/desktop", "private": true, - "version": "1.1.26", + "version": "1.1.27", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json index 10b40abb2b1f..a0f71aaefb84 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..7d41fd8f457c 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" @@ -94,6 +95,21 @@ const createPlatform = (password: Accessor): Platform => ({ const apiCache = new Map Promise }>() const memoryCache = new Map() + const flushAll = async () => { + const apis = Array.from(apiCache.values()) + await Promise.all(apis.map((api) => api.flush().catch(() => undefined))) + } + + if ("addEventListener" in globalThis) { + const handleVisibility = () => { + if (document.visibilityState !== "hidden") return + void flushAll() + } + + window.addEventListener("pagehide", () => void flushAll()) + document.addEventListener("visibilitychange", handleVisibility) + } + const createMemoryStore = () => { const data = new Map() const store: StoreLike = { @@ -303,11 +319,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, + }) + } +}) diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 1a40af1c799a..c96acbadda5b 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.26", + "version": "1.1.27", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 2327c61e6670..d7762cf1375c 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.26" +version = "1.1.27" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index e5b0f62b927d..7f581dcb5f2f 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.26", + "version": "1.1.27", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index f60132a2cd60..d5809a7eb1d9 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.26", + "version": "1.1.27", "name": "opencode", "type": "module", "license": "MIT", 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, diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 674f8ebe7dfd..d9deb0a84cda 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -249,9 +249,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": { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6e4fd008dfab..ee2d786d2fa1 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 @@ -812,10 +813,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 } 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) { diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index 7b0ba6150ce4..cccf2d1cf46f 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 @@ -202,12 +199,6 @@ export const ApplyPatchTool = Tool.define("apply_patch", { changedFiles.push(change.filePath) 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/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. 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) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 52f392a81791..5cc3191e1e26 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.1.26", + "version": "1.1.27", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 5ce97dbf7e71..93d498490b00 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.1.26", + "version": "1.1.27", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 37b116ec2b70..c3ae3ea91ccf 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.26", + "version": "1.1.27", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 7079384a54a5..e940631468f6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.26", + "version": "1.1.27", "type": "module", "license": "MIT", "exports": { diff --git a/packages/ui/src/components/hover-card.css b/packages/ui/src/components/hover-card.css index f1172dfc7df4..70dcdd7a2ffc 100644 --- a/packages/ui/src/components/hover-card.css +++ b/packages/ui/src/components/hover-card.css @@ -8,7 +8,7 @@ z-index: 50; min-width: 200px; max-width: 320px; - border-radius: var(--radius-md); + border-radius: 8px; background-color: var(--surface-raised-stronger-non-alpha); pointer-events: auto; @@ -31,7 +31,7 @@ } [data-slot="hover-card-body"] { - padding: 12px; + padding: 4px; } } diff --git a/packages/ui/src/components/message-nav.css b/packages/ui/src/components/message-nav.css index f55d5a774c54..2a1c3a45be84 100644 --- a/packages/ui/src/components/message-nav.css +++ b/packages/ui/src/components/message-nav.css @@ -73,7 +73,7 @@ [data-slot="message-nav-title-preview"] { font-size: 14px; /* text-14-regular */ - color: var(--text-weak); + color: var(--text-base); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; 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; + } + } } } 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() + }} + />
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