From c47699536fe9742cf5aa37f2c51168abeb90cb8d Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:56:24 -0500 Subject: [PATCH 01/23] fix: Don't unnecessarily wrap lines and introduce an unneeded empty line (resolves #9489) (#9488) --- packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 5c37a493df..a8671f4669 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -337,6 +337,7 @@ function Option(props: { fg={props.active ? fg : props.current ? theme.primary : theme.text} attributes={props.active ? TextAttributes.BOLD : undefined} overflow="hidden" + wrapMode="none" paddingLeft={3} > {Locale.truncate(props.title, 61)} From 889c60d63b585a276080f20c40c2d73ff715ea94 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Mon, 19 Jan 2026 15:04:59 -0500 Subject: [PATCH 02/23] fix(web): rename favicons to v2 for cache busting (#9492) --- packages/app/index.html | 8 ++--- packages/app/public/apple-touch-icon-v2.png | 1 + packages/app/public/favicon-96x96-v2.png | 1 + packages/app/public/favicon-v2.ico | 1 + packages/app/public/favicon-v2.svg | 1 + packages/app/src/entry.tsx | 2 +- packages/app/src/pages/layout.tsx | 2 +- packages/console/function/src/auth.ts | 2 +- packages/desktop/index.html | 8 ++--- packages/desktop/src/index.tsx | 2 +- packages/docs/docs.json | 2 +- packages/docs/favicon-v2.svg | 19 ++++++++++++ .../assets/favicon/apple-touch-icon-v2.png | Bin 0 -> 1541 bytes .../src/assets/favicon/favicon-96x96-v2.png | Bin 0 -> 536 bytes packages/ui/src/assets/favicon/favicon-v2.ico | Bin 0 -> 15086 bytes packages/ui/src/assets/favicon/favicon-v2.svg | 7 +++++ packages/ui/src/components/favicon.tsx | 6 ++-- packages/web/astro.config.mjs | 28 ++++++++++++++++++ packages/web/public/apple-touch-icon-v2.png | 1 + packages/web/public/favicon-96x96-v2.png | 1 + packages/web/public/favicon-v2.ico | 1 + packages/web/public/favicon-v2.svg | 1 + 22 files changed, 78 insertions(+), 16 deletions(-) create mode 120000 packages/app/public/apple-touch-icon-v2.png create mode 120000 packages/app/public/favicon-96x96-v2.png create mode 120000 packages/app/public/favicon-v2.ico create mode 120000 packages/app/public/favicon-v2.svg create mode 100644 packages/docs/favicon-v2.svg create mode 100644 packages/ui/src/assets/favicon/apple-touch-icon-v2.png create mode 100644 packages/ui/src/assets/favicon/favicon-96x96-v2.png create mode 100644 packages/ui/src/assets/favicon/favicon-v2.ico create mode 100644 packages/ui/src/assets/favicon/favicon-v2.svg create mode 120000 packages/web/public/apple-touch-icon-v2.png create mode 120000 packages/web/public/favicon-96x96-v2.png create mode 120000 packages/web/public/favicon-v2.ico create mode 120000 packages/web/public/favicon-v2.svg diff --git a/packages/app/index.html b/packages/app/index.html index 450807a42e..1e516cbbb1 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -4,10 +4,10 @@ OpenCode - - - - + + + + diff --git a/packages/app/public/apple-touch-icon-v2.png b/packages/app/public/apple-touch-icon-v2.png new file mode 120000 index 0000000000..c0d4353db4 --- /dev/null +++ b/packages/app/public/apple-touch-icon-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/apple-touch-icon-v2.png \ No newline at end of file diff --git a/packages/app/public/favicon-96x96-v2.png b/packages/app/public/favicon-96x96-v2.png new file mode 120000 index 0000000000..b3129f6bf9 --- /dev/null +++ b/packages/app/public/favicon-96x96-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-96x96-v2.png \ No newline at end of file diff --git a/packages/app/public/favicon-v2.ico b/packages/app/public/favicon-v2.ico new file mode 120000 index 0000000000..d8527270af --- /dev/null +++ b/packages/app/public/favicon-v2.ico @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.ico \ No newline at end of file diff --git a/packages/app/public/favicon-v2.svg b/packages/app/public/favicon-v2.svg new file mode 120000 index 0000000000..2600394cea --- /dev/null +++ b/packages/app/public/favicon-v2.svg @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.svg \ No newline at end of file diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 28741098c8..8c4662926a 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -37,7 +37,7 @@ const platform: Platform = { .then(() => { const notification = new Notification(title, { body: description ?? "", - icon: "https://opencode.ai/favicon-96x96.png", + icon: "https://opencode.ai/favicon-96x96-v2.png", }) notification.onclick = () => { window.focus() diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 81177d1386..2f3b39d862 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1284,7 +1284,7 @@ export default function Layout(props: ParentProps) {
OpenCode - - - - + + + + diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 6cd77d7d55..a06270b13f 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -253,7 +253,7 @@ const createPlatform = (password: Accessor): Platform => ({ .then(() => { const notification = new Notification(title, { body: description ?? "", - icon: "https://opencode.ai/favicon-96x96.png", + icon: "https://opencode.ai/favicon-96x96-v2.png", }) notification.onclick = () => { const win = getCurrentWindow() diff --git a/packages/docs/docs.json b/packages/docs/docs.json index 4461f8253b..93dff10f8c 100644 --- a/packages/docs/docs.json +++ b/packages/docs/docs.json @@ -7,7 +7,7 @@ "light": "#07C983", "dark": "#15803D" }, - "favicon": "/favicon.svg", + "favicon": "/favicon-v2.svg", "navigation": { "tabs": [ { diff --git a/packages/docs/favicon-v2.svg b/packages/docs/favicon-v2.svg new file mode 100644 index 0000000000..b785c738bf --- /dev/null +++ b/packages/docs/favicon-v2.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/favicon/apple-touch-icon-v2.png b/packages/ui/src/assets/favicon/apple-touch-icon-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..70fd01b0ea38ea8b3d0429a6438f5b8e85aba94a GIT binary patch literal 1541 zcmaJ>eKb^Q7{4Ur0=8rcXMfNrMx!Z`2J5r}m{7q=oI5Z4;q`f(Bo2BGNsu z_o*M^xIMiCe~;%%@XdW@9C6U|=3EF(Xy3J&Mnv)+onDt<%fnSpp|orWz33X39|qvAImHaraYT6ua6s5eeo7(qmH2LMX#~Pu2;Tnp)=eVBAS5%uY|{(3wuB zAapme=5QR?*2MU+i^ao>T30eu5Zd{wQ@+ZGno1dH3BTocen+^M)MNL^v8uk#$Zq?I z%@%6)NV%y0;oD~kM$#S1?GRjgwP!pQ{x`l^OZ@Y-Sb<3otdEN&B0Gw!W}D2I@{l2u zuwn>3!;aHkC&9xO4Yv!~RpprP?wSlwBI39;_4!?-dnmpw?!Q=fcUHoZd-O?O@D$h* z=K60K1pZXi%Q6$dLa4UWf-c&ORa8`Hrl;5SPRgF&Q|Ccw(z0?*!*ML*@LQ+JW*hD? z?2vx^2D4PXwiOfte^e8Al1$(yFq5Bs%ER5I;%cyvNtNy-14|BNmk*%)-+J>w&c+K* zW&AZr?A1wdT(c&ASOJ96+iQQ)0*#~_viFD-7&-Fs-ZXFmEm^+iZ{yhneh(>TdI0q1 zphvmNM}e(UhHh{p@bicBH8DVEei4QF{>+VWB|@NSEyiUGGldQ^HRWMybq+#oPK!0uXpM50A2a#48$6R-P=pz-9TF_%;W)Wir+`pO z;&-{gD-Y?eIszbgl4xEEB)ld$b}mzG+8CiJvNPw%n)GWv#32?OS;Zv~kk3xHn{&b| zE}4Ibb)U&^ePw0=+`*3yIV#4D9X`1)`WssZ;sKSF^R_e95r8e<4G#X6TGF;U2gwt) zKDZ1cGYrlbdhM?EJiL7E{(8`j%3;MHDxlY(61Om5hyb@Ik-@-yk4tU@fFAj*wDAVH z-*y$fV-fi0zyIZ*;8}->ds25n1QnR7434S=nsQtG(gg;+2t=G8gLy*FJ7|SKsNwRR ztp>l{!s@~ZOy7fcw+>K3a) z2t9JGBVr0PnAp|meV%Gu3p!47j_uJa=@{{jVjoU;G` literal 0 HcmV?d00001 diff --git a/packages/ui/src/assets/favicon/favicon-96x96-v2.png b/packages/ui/src/assets/favicon/favicon-96x96-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..15266d28f159a74d5c54634d1b3ef548e97f6c8c GIT binary patch literal 536 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U_9&T;uuoF_-2kH*C7W17V-E6 zFXn$)Jn1}VWz)Rk!VA93%@(%Up4f0XPqg-HiS7LRa(xF^{&eKjU^sez7Ly2raKq!= z2Brk2f<3$e3_c8f+ZnkSICw-dnG+r;>$uDw$0;LPsnB{na7mCjKzib*uaihe}mzNs&#KTgN`2~7sCBeS(pZpU>|VHo`hk0|4|7)d1$)nnOUS-2?|0x9zfA3XrUYknTj~WYE{IAqoDKnpTg@KE zlmGwSOe(+SALH)78&qol`;+06E9QCjbBd literal 0 HcmV?d00001 diff --git a/packages/ui/src/assets/favicon/favicon-v2.ico b/packages/ui/src/assets/favicon/favicon-v2.ico new file mode 100644 index 0000000000000000000000000000000000000000..34ca0b9c01b23ca30f64ed17bd0705bb08cca4b1 GIT binary patch literal 15086 zcmeHOK~BRk5L`qb5Qz`ug4APAoOwpYiDO^FTX+>;32UH|yXxAWdV|$=ELq9glikTM zu~I9DJjkQ$cB02wKA%NiL}Z@-;z{I7$0j|d`NKx!LjhaG=q2*5J&k*9nw}PRYCsRr z1M~nraO)meR&HzSw=(_J-v!9OtN{H}UW3c(_0Nvdf4_fQknY_n`CsNAMEivG8uh3C zEncF3%0BU)BTu&$&2x;G$EzDP?$yn?wVZo-qIi0Fyt*x%bBveAs~a`$)y=uJoO^kq zczSufx-Fb@jF-o&8#NwPH!2T$Ur}|V@{s?plB0AORhRyWeSh%uM8{D-pm>xIibwgN zc$5!{NBN+5ln;tW`Ji}|4~j?mpm>xIibwenf7v=r-KZNqKo8IZV?E$~7sT=ZQ6PP% zHEVy>&hNCwJ_}rnu2+BbJ>TMc|MoT?8nb?%_pILeUiZ0ki;vd-T+Mup^-mR`{`7y3 z2Tt9zRKH_G@ilH}J`~UT<>$ldiN-a3Yx8J5i?8Lc&D(rvT+3gZN9$R9EkEbW{--{- zt&eUG3~gzjLjT3=XRqR(_9^T-W#ig&XYn>}y*~f#kj{_oUaq(IiQa$j^~ZYlX!F|G N;%#j4*B + + + + \ No newline at end of file diff --git a/packages/ui/src/components/favicon.tsx b/packages/ui/src/components/favicon.tsx index 3462384d45..abb0e1f78c 100644 --- a/packages/ui/src/components/favicon.tsx +++ b/packages/ui/src/components/favicon.tsx @@ -3,9 +3,9 @@ import { Link, Meta } from "@solidjs/meta" export const Favicon = () => { return ( <> - - - + + + diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 99a1c3bd80..9be189cede 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -32,6 +32,34 @@ export default defineConfig({ solidJs(), starlight({ title: "OpenCode", + favicon: "/favicon-v2.svg", + head: [ + { + tag: "link", + attrs: { + rel: "icon", + href: "/favicon-v2.ico", + sizes: "32x32", + }, + }, + { + tag: "link", + attrs: { + rel: "icon", + type: "image/png", + href: "/favicon-96x96-v2.png", + sizes: "96x96", + }, + }, + { + tag: "link", + attrs: { + rel: "apple-touch-icon", + href: "/apple-touch-icon-v2.png", + sizes: "180x180", + }, + }, + ], lastUpdated: true, expressiveCode: { themes: ["github-light", "github-dark"] }, social: [ diff --git a/packages/web/public/apple-touch-icon-v2.png b/packages/web/public/apple-touch-icon-v2.png new file mode 120000 index 0000000000..c0d4353db4 --- /dev/null +++ b/packages/web/public/apple-touch-icon-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/apple-touch-icon-v2.png \ No newline at end of file diff --git a/packages/web/public/favicon-96x96-v2.png b/packages/web/public/favicon-96x96-v2.png new file mode 120000 index 0000000000..b3129f6bf9 --- /dev/null +++ b/packages/web/public/favicon-96x96-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-96x96-v2.png \ No newline at end of file diff --git a/packages/web/public/favicon-v2.ico b/packages/web/public/favicon-v2.ico new file mode 120000 index 0000000000..d8527270af --- /dev/null +++ b/packages/web/public/favicon-v2.ico @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.ico \ No newline at end of file diff --git a/packages/web/public/favicon-v2.svg b/packages/web/public/favicon-v2.svg new file mode 120000 index 0000000000..2600394cea --- /dev/null +++ b/packages/web/public/favicon-v2.svg @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.svg \ No newline at end of file From c3393ecc6c0a1482669b945e109af1d98f25a5ee Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:16:25 +0100 Subject: [PATCH 03/23] fix(app): give feedback when trying to paste a unsupported filetype (#9452) --- packages/app/src/components/prompt-input.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index c74edd94e6..56bbdc8cb5 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -300,7 +300,8 @@ export const PromptInput: Component = (props) => { event.stopPropagation() const items = Array.from(clipboardData.items) - const imageItems = items.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type)) + const fileItems = items.filter((item) => item.kind === "file") + const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type)) if (imageItems.length > 0) { for (const item of imageItems) { @@ -310,7 +311,16 @@ export const PromptInput: Component = (props) => { return } + if (fileItems.length > 0) { + showToast({ + title: "Unsupported paste", + description: "Only images or PDFs can be pasted here.", + }) + return + } + const plainText = clipboardData.getData("text/plain") ?? "" + if (!plainText) return addPart({ type: "text", content: plainText, start: 0, end: 0 }) } From d19e76d96c7316dffb1bca1593fcc80bcdc0a9ff Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:43:32 +0100 Subject: [PATCH 04/23] fix: keyboard nav when mouse hovered over list (#9500) --- packages/ui/src/components/list.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 874638c5a5..6929f6b734 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -58,6 +58,8 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) const searchProps = () => (typeof props.search === "object" ? props.search : {}) + const moved = (event: MouseEvent) => event.movementX !== 0 || event.movementY !== 0 + createEffect(() => { if (props.filter !== undefined) { onInput(props.filter) @@ -227,7 +229,8 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) data-selected={item === props.current} onClick={() => handleSelect(item, i())} type="button" - onMouseMove={() => { + onMouseMove={(event) => { + if (!moved(event)) return setStore("mouseActive", true) setActive(props.key(item)) }} From 091e88c1e157c53552e215efd05122ddd973de4f Mon Sep 17 00:00:00 2001 From: Joseph Campuzano Date: Mon, 19 Jan 2026 14:46:17 -0600 Subject: [PATCH 05/23] fix(opencode): sets input mode based on whether mouse vs keyboard is in use to prevent mouse events firing (#9449) --- .../cmd/tui/component/prompt/autocomplete.tsx | 23 ++++++++++++++++++- .../src/cli/cmd/tui/ui/dialog-select.tsx | 21 +++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index e27c32dfb2..b6ca88410c 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -85,6 +85,7 @@ export function Autocomplete(props: { index: 0, selected: 0, visible: false as AutocompleteRef["visible"], + input: "keyboard" as "keyboard" | "mouse", }) const [positionTick, setPositionTick] = createSignal(0) @@ -128,6 +129,14 @@ export function Autocomplete(props: { return props.input().getTextRange(store.index + 1, props.input().cursorOffset) }) + // When the filter changes due to how TUI works, the mousemove might still be triggered + // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard so + // that the mouseover event doesn't trigger when filtering. + createEffect(() => { + filter(); + setStore("input", "keyboard") + }) + function insertPart(text: string, part: PromptInfo["parts"][number]) { const input = props.input() const currentCursorOffset = input.cursorOffset @@ -525,11 +534,13 @@ export function Autocomplete(props: { const isNavDown = name === "down" || (ctrlOnly && name === "n") if (isNavUp) { + setStore("input", "keyboard") move(-1) e.preventDefault() return } if (isNavDown) { + setStore("input", "keyboard") move(1) e.preventDefault() return @@ -612,7 +623,17 @@ export function Autocomplete(props: { paddingRight={1} backgroundColor={index === store.selected ? theme.primary : undefined} flexDirection="row" - onMouseOver={() => moveTo(index)} + onMouseMove={() => { + setStore("input", "mouse") + }} + onMouseOver={() => { + if (store.input !== "mouse") return + moveTo(index) + }} + onMouseDown={() => { + setStore("input", "mouse") + moveTo(index) + }} onMouseUp={() => select()} > diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index a8671f4669..f7c2fed85c 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -52,6 +52,7 @@ export function DialogSelect(props: DialogSelectProps) { const [store, setStore] = createStore({ selected: 0, filter: "", + input: "keyboard" as "keyboard" | "mouse", }) createEffect( @@ -83,6 +84,14 @@ export function DialogSelect(props: DialogSelectProps) { return result }) + // When the filter changes due to how TUI works, the mousemove might still be triggered + // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard + // that the mouseover event doesn't trigger when filtering. + createEffect(() => { + filtered(); + setStore("input", "keyboard") + }) + const grouped = createMemo(() => { const result = pipe( filtered(), @@ -157,12 +166,15 @@ export function DialogSelect(props: DialogSelectProps) { const keybind = useKeybind() useKeyboard((evt) => { + setStore("input", "keyboard") + if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1) if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1) if (evt.name === "pageup") move(-10) if (evt.name === "pagedown") move(10) if (evt.name === "home") moveTo(0) if (evt.name === "end") moveTo(flat().length - 1) + if (evt.name === "return") { const option = selected() if (option) { @@ -259,11 +271,20 @@ export function DialogSelect(props: DialogSelectProps) { { + setStore("input", "mouse") + }} onMouseUp={() => { option.onSelect?.(dialog) props.onSelect?.(option) }} onMouseOver={() => { + if (store.input !== "mouse") return + const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) + if (index === -1) return + moveTo(index) + }} + onMouseDown={() => { const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) if (index === -1) return moveTo(index) From 88c5a7fe9e7b8fd14bc2065051779ffa89789387 Mon Sep 17 00:00:00 2001 From: Ronan Kearns <90280289+kearns-cu@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:46:32 -0500 Subject: [PATCH 06/23] fix(tui): clarify resume session tip (#9490) --- packages/opencode/src/cli/cmd/tui/component/tips.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.tsx b/packages/opencode/src/cli/cmd/tui/component/tips.tsx index fe2e7ca216..3f0318e269 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/tips.tsx @@ -106,7 +106,7 @@ const TIPS = [ "Use plugins to send OS notifications when sessions complete", "Create a plugin to prevent OpenCode from reading sensitive files", "Use {highlight}opencode run{/highlight} for non-interactive scripting", - "Use {highlight}opencode run --continue{/highlight} to resume the last session", + "Use {highlight}opencode --continue{/highlight} to resume the last session", "Use {highlight}opencode run -f file.ts{/highlight} to attach files via CLI", "Use {highlight}--format json{/highlight} for machine-readable output in scripts", "Run {highlight}opencode serve{/highlight} for headless API access to OpenCode", From e29120317f8820a86054bb20e8982f0947fa03cd Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 20:47:09 +0000 Subject: [PATCH 07/23] chore: generate --- .../opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx | 2 +- packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index b6ca88410c..718929d445 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -133,7 +133,7 @@ export function Autocomplete(props: { // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard so // that the mouseover event doesn't trigger when filtering. createEffect(() => { - filter(); + filter() setStore("input", "keyboard") }) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index f7c2fed85c..618bf3b3cb 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -88,7 +88,7 @@ export function DialogSelect(props: DialogSelectProps) { // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard // that the mouseover event doesn't trigger when filtering. createEffect(() => { - filtered(); + filtered() setStore("input", "keyboard") }) From 769c97af086e5edf0efb431e902eceb54dc668cb Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 14:49:51 -0600 Subject: [PATCH 08/23] chore: rm double conditional --- packages/opencode/src/file/ripgrep.ts | 50 +++++++++++++-------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 834cbee1ed..0d18173565 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -162,34 +162,32 @@ export namespace Ripgrep { }) } if (config.extension === "zip") { - if (config.extension === "zip") { - const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()]))) - const entries = await zipFileReader.getEntries() - let rgEntry: any - for (const entry of entries) { - if (entry.filename.endsWith("rg.exe")) { - rgEntry = entry - break - } + const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()]))) + const entries = await zipFileReader.getEntries() + let rgEntry: any + for (const entry of entries) { + if (entry.filename.endsWith("rg.exe")) { + rgEntry = entry + break } - - if (!rgEntry) { - throw new ExtractionFailedError({ - filepath: archivePath, - stderr: "rg.exe not found in zip archive", - }) - } - - const rgBlob = await rgEntry.getData(new BlobWriter()) - if (!rgBlob) { - throw new ExtractionFailedError({ - filepath: archivePath, - stderr: "Failed to extract rg.exe from zip archive", - }) - } - await Bun.write(filepath, await rgBlob.arrayBuffer()) - await zipFileReader.close() } + + if (!rgEntry) { + throw new ExtractionFailedError({ + filepath: archivePath, + stderr: "rg.exe not found in zip archive", + }) + } + + const rgBlob = await rgEntry.getData(new BlobWriter()) + if (!rgBlob) { + throw new ExtractionFailedError({ + filepath: archivePath, + stderr: "Failed to extract rg.exe from zip archive", + }) + } + await Bun.write(filepath, await rgBlob.arrayBuffer()) + await zipFileReader.close() } await fs.unlink(archivePath) if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755) From ecc51ddb4e8a04495da45126a706a7effee5bf8d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:23:59 -0600 Subject: [PATCH 09/23] fix(app): hash nav --- packages/app/src/pages/layout.tsx | 5 +- packages/app/src/pages/session.tsx | 141 +++++++++++++++++++++++------ 2 files changed, 115 insertions(+), 31 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2f3b39d862..a8f9b162fe 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1429,10 +1429,11 @@ export default function Layout(props: ParentProps) { getLabel={messageLabel} onMessageSelect={(message) => { if (!isActive()) { - navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`) + sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`) + navigate(`${props.slug}/session/${props.session.id}`) return } - window.location.hash = `message-${message.id}` + window.history.replaceState(null, "", `#message-${message.id}`) window.dispatchEvent(new HashChangeEvent("hashchange")) }} size="normal" diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 31f9eac9c2..fdb9f268cd 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" @@ -167,6 +167,7 @@ export default function Page() { const sdk = useSDK() const prompt = usePrompt() const permission = usePermission() + const [pendingMessage, setPendingMessage] = createSignal(undefined) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) @@ -943,17 +944,30 @@ export default function Page() { window.history.replaceState(null, "", `#${anchor(id)}`) } + createEffect(() => { + const sessionID = params.id + if (!sessionID) return + const raw = sessionStorage.getItem("opencode.pendingMessage") + if (!raw) return + const parts = raw.split("|") + const pendingSessionID = parts[0] + const messageID = parts[1] + if (!pendingSessionID || !messageID) return + if (pendingSessionID !== sessionID) return + + sessionStorage.removeItem("opencode.pendingMessage") + setPendingMessage(messageID) + }) + const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { const root = scroller - if (!root) { - el.scrollIntoView({ behavior, block: "start" }) - return - } + if (!root) return false const a = el.getBoundingClientRect() const b = root.getBoundingClientRect() const top = a.top - b.top + root.scrollTop root.scrollTo({ top, behavior }) + return true } const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { @@ -967,7 +981,15 @@ export default function Page() { requestAnimationFrame(() => { const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) + if (!el) { + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + scrollToElement(next, behavior) + }) + return + } + scrollToElement(el, behavior) }) updateHash(message.id) @@ -975,10 +997,57 @@ export default function Page() { } const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) + if (!el) { + updateHash(message.id) + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + if (!scrollToElement(next, behavior)) return + }) + return + } + if (scrollToElement(el, behavior)) { + updateHash(message.id) + return + } + + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + if (!scrollToElement(next, behavior)) return + }) updateHash(message.id) } + const applyHash = (behavior: ScrollBehavior) => { + const hash = window.location.hash.slice(1) + if (!hash) { + autoScroll.forceScrollToBottom() + return + } + + const match = hash.match(/^message-(.+)$/) + if (match) { + const msg = visibleUserMessages().find((m) => m.id === match[1]) + if (msg) { + scrollToMessage(msg, behavior) + return + } + + // If we have a message hash but the message isn't loaded/rendered yet, + // don't fall back to "bottom". We'll retry once messages arrive. + return + } + + const target = document.getElementById(hash) + if (target) { + scrollToElement(target, behavior) + return + } + + autoScroll.forceScrollToBottom() + } + const getActiveMessageId = (container: HTMLDivElement) => { const cutoff = container.scrollTop + 100 const nodes = container.querySelectorAll("[data-message-id]") @@ -1019,31 +1088,45 @@ export default function Page() { if (!sessionID || !ready) return requestAnimationFrame(() => { - const hash = window.location.hash.slice(1) - if (!hash) { - autoScroll.forceScrollToBottom() - return - } - - const hashTarget = document.getElementById(hash) - if (hashTarget) { - scrollToElement(hashTarget, "auto") - return - } - - const match = hash.match(/^message-(.+)$/) - if (match) { - const msg = visibleUserMessages().find((m) => m.id === match[1]) - if (msg) { - scrollToMessage(msg, "auto") - return - } - } - - autoScroll.forceScrollToBottom() + applyHash("auto") }) }) + // Retry message navigation once the target message is actually loaded. + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return + + // dependencies + visibleUserMessages().length + store.turnStart + + const targetId = pendingMessage() ?? (() => { + const hash = window.location.hash.slice(1) + const match = hash.match(/^message-(.+)$/) + if (!match) return undefined + return match[1] + })() + if (!targetId) return + if (store.messageId === targetId) return + + const msg = visibleUserMessages().find((m) => m.id === targetId) + if (!msg) return + if (pendingMessage() === targetId) setPendingMessage(undefined) + requestAnimationFrame(() => scrollToMessage(msg, "auto")) + }) + + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return + + const handler = () => requestAnimationFrame(() => applyHash("auto")) + window.addEventListener("hashchange", handler) + onCleanup(() => window.removeEventListener("hashchange", handler)) + }) + createEffect(() => { document.addEventListener("keydown", handleKeyDown) }) From cac35bc52d9a8a2c9ca673510b2266d1c13ee141 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:05:49 -0600 Subject: [PATCH 10/23] fix(app): global terminal/review pane toggles --- packages/app/src/context/layout.tsx | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index a8da156092..a8a8ce1e9f 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -33,8 +33,6 @@ type SessionTabs = { type SessionView = { scroll: Record reviewOpen?: string[] - terminalOpened?: boolean - reviewPanelOpened?: boolean } export type LocalProject = Partial & { worktree: string; expanded: boolean } @@ -78,9 +76,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, terminal: { height: 280, + opened: false, }, review: { diffStyle: "split" as ReviewDiffStyle, + panelOpened: true, }, session: { width: 600, @@ -172,7 +172,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const current = store.sessionView[sessionKey] const keep = meta.active ?? sessionKey if (!current) { - setStore("sessionView", sessionKey, { scroll: next, terminalOpened: false, reviewPanelOpened: true }) + setStore("sessionView", sessionKey, { scroll: next }) prune(keep) return } @@ -379,31 +379,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( touch(sessionKey) scroll.seed(sessionKey) const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} }) - const terminalOpened = createMemo(() => s().terminalOpened ?? false) - const reviewPanelOpened = createMemo(() => s().reviewPanelOpened ?? true) + const terminalOpened = createMemo(() => store.terminal?.opened ?? false) + const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true) function setTerminalOpened(next: boolean) { - const current = store.sessionView[sessionKey] + const current = store.terminal if (!current) { - setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: next, reviewPanelOpened: true }) + setStore("terminal", { height: 280, opened: next }) return } - const value = current.terminalOpened ?? false + const value = current.opened ?? false if (value === next) return - setStore("sessionView", sessionKey, "terminalOpened", next) + setStore("terminal", "opened", next) } function setReviewPanelOpened(next: boolean) { - const current = store.sessionView[sessionKey] + const current = store.review if (!current) { - setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: false, reviewPanelOpened: next }) + setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next }) return } - const value = current.reviewPanelOpened ?? true + const value = current.panelOpened ?? true if (value === next) return - setStore("sessionView", sessionKey, "reviewPanelOpened", next) + setStore("review", "panelOpened", next) } return { @@ -444,8 +444,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( if (!current) { setStore("sessionView", sessionKey, { scroll: {}, - terminalOpened: false, - reviewPanelOpened: true, reviewOpen: open, }) return From a4d1824412c57d733c33c58e19551f0818c82e8a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:59:41 -0600 Subject: [PATCH 11/23] fix(app): no more favicons --- .../src/components/dialog-edit-project.tsx | 4 +- packages/app/src/context/layout.tsx | 62 ++++++++++++------- packages/app/src/pages/layout.tsx | 2 +- packages/opencode/src/project/project.ts | 4 ++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 1 + packages/sdk/js/src/v2/gen/types.gen.ts | 2 + 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 091f00702f..7acb766f80 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -22,7 +22,7 @@ export function DialogEditProject(props: { project: LocalProject }) { const [store, setStore] = createStore({ name: defaultName(), color: props.project.icon?.color || "pink", - iconUrl: props.project.icon?.url || "", + iconUrl: props.project.icon?.override || "", saving: false, }) @@ -74,7 +74,7 @@ export function DialogEditProject(props: { project: LocalProject }) { await globalSDK.client.project.update({ projectID: props.project.id, name, - icon: { color: store.color, url: store.iconUrl }, + icon: { color: store.color, override: store.iconUrl }, }) setStore("saving", false) dialog.close() diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index a8a8ce1e9f..d7d09aa399 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -208,10 +208,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) }) - const usedColors = new Set() + const [colors, setColors] = createStore>({}) - function pickAvailableColor(): AvatarColorKey { - const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c)) + function pickAvailableColor(used: Set): AvatarColorKey { + const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c)) if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)] return available[Math.floor(Math.random() * available.length)] } @@ -222,24 +222,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const metadata = projectID ? globalSync.data.project.find((x) => x.id === projectID) : globalSync.data.project.find((x) => x.worktree === project.worktree) - return [ - { - ...(metadata ?? {}), - ...project, - icon: { url: metadata?.icon?.url, color: metadata?.icon?.color }, + return { + ...(metadata ?? {}), + ...project, + icon: { + url: metadata?.icon?.url, + override: metadata?.icon?.override, + color: metadata?.icon?.color, }, - ] - } - - function colorize(project: LocalProject) { - if (project.icon?.color) return project - const color = pickAvailableColor() - usedColors.add(color) - project.icon = { ...project.icon, color } - if (project.id) { - globalSdk.client.project.update({ projectID: project.id, icon: { color } }) } - return project } const roots = createMemo(() => { @@ -277,8 +268,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) }) - const enriched = createMemo(() => server.projects.list().flatMap(enrich)) - const list = createMemo(() => enriched().flatMap(colorize)) + const enriched = createMemo(() => server.projects.list().map(enrich)) + const list = createMemo(() => { + const projects = enriched() + return projects.map((project) => { + const color = project.icon?.color ?? colors[project.worktree] + if (!color) return project + const icon = project.icon ? { ...project.icon, color } : { color } + return { ...project, icon } + }) + }) + + createEffect(() => { + const projects = enriched() + if (projects.length === 0) return + + const used = new Set() + for (const project of projects) { + const color = project.icon?.color ?? colors[project.worktree] + if (color) used.add(color) + } + + for (const project of projects) { + if (project.icon?.color) continue + if (colors[project.worktree]) continue + const color = pickAvailableColor(used) + used.add(color) + setColors(project.worktree, color) + if (!project.id) continue + void globalSdk.client.project.update({ projectID: project.id, icon: { color } }) + } + }) onMount(() => { Promise.all( diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index a8f9b162fe..9daac949e4 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1284,7 +1284,7 @@ export default function Layout(props: ParentProps) {
Date: Mon, 19 Jan 2026 14:46:32 -0600 Subject: [PATCH 12/23] fix(app): fade under sticky elements --- packages/ui/src/components/session-turn.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 5f8c0a16f6..034d302470 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -75,6 +75,17 @@ background-color: var(--background-stronger); z-index: -1; } + + &::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 32px; + background: linear-gradient(to bottom, var(--background-stronger), transparent); + pointer-events: none; + } } [data-slot="session-turn-response-trigger"] { From 69b3b35ea5ab576439e2d919b6b2c19b360b910d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 21:00:39 +0000 Subject: [PATCH 13/23] chore: generate --- packages/app/src/pages/session.tsx | 14 ++++++++------ packages/sdk/openapi.json | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index fdb9f268cd..3b405ef077 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1102,12 +1102,14 @@ export default function Page() { visibleUserMessages().length store.turnStart - const targetId = pendingMessage() ?? (() => { - const hash = window.location.hash.slice(1) - const match = hash.match(/^message-(.+)$/) - if (!match) return undefined - return match[1] - })() + const targetId = + pendingMessage() ?? + (() => { + const hash = window.location.hash.slice(1) + const match = hash.match(/^message-(.+)$/) + if (!match) return undefined + return match[1] + })() if (!targetId) return if (store.messageId === targetId) return diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 08dd98fd9b..c1be820f26 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -231,6 +231,9 @@ "url": { "type": "string" }, + "override": { + "type": "string" + }, "color": { "type": "string" } @@ -5796,6 +5799,9 @@ "url": { "type": "string" }, + "override": { + "type": "string" + }, "color": { "type": "string" } From d605a78a0547bee0ac1883eb09509f2ee0c5b815 Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:15:43 +0100 Subject: [PATCH 14/23] fix(app): change keybind for cycling thinking effort (#9508) --- 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 3b405ef077..ec3b0ac30d 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -531,7 +531,7 @@ export default function Page() { title: "Cycle thinking effort", description: "Switch to the next effort level", category: "Model", - keybind: "shift+mod+t", + keybind: "shift+mod+d", onSelect: () => { local.model.variant.cycle() }, From 79ae749ed8d72fc55cbb47435a717c0653511980 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:28:33 -0600 Subject: [PATCH 15/23] fix(app): don't change resize handle on hover --- packages/ui/src/components/resize-handle.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ui/src/components/resize-handle.css b/packages/ui/src/components/resize-handle.css index 088bf92157..e4c8d474e5 100644 --- a/packages/ui/src/components/resize-handle.css +++ b/packages/ui/src/components/resize-handle.css @@ -5,10 +5,8 @@ &::after { content: ""; position: absolute; - background-color: var(--color-border-strong-base); opacity: 0; transition: opacity 0.15s ease-in-out; - border-radius: 2px; } &:hover::after, From 673e79f457ed75f6077d7c5ad71906a7c8ce415c Mon Sep 17 00:00:00 2001 From: Spoon <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:44:58 +0100 Subject: [PATCH 16/23] tweak(batch): up restrictive max batch tool from `10` to `25` (#9275) --- packages/opencode/src/tool/batch.ts | 8 ++++---- packages/opencode/src/tool/batch.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/tool/batch.ts b/packages/opencode/src/tool/batch.ts index 8bffbd54a2..ba34eb48f5 100644 --- a/packages/opencode/src/tool/batch.ts +++ b/packages/opencode/src/tool/batch.ts @@ -33,8 +33,8 @@ export const BatchTool = Tool.define("batch", async () => { const { Session } = await import("../session") const { Identifier } = await import("../id/id") - const toolCalls = params.tool_calls.slice(0, 10) - const discardedCalls = params.tool_calls.slice(10) + const toolCalls = params.tool_calls.slice(0, 25) + const discardedCalls = params.tool_calls.slice(25) const { ToolRegistry } = await import("./registry") const availableTools = await ToolRegistry.tools({ modelID: "", providerID: "" }) @@ -139,14 +139,14 @@ export const BatchTool = Tool.define("batch", async () => { state: { status: "error", input: call.parameters, - error: "Maximum of 10 tools allowed in batch", + error: "Maximum of 25 tools allowed in batch", time: { start: now, end: now }, }, }) results.push({ success: false as const, tool: call.tool, - error: new Error("Maximum of 10 tools allowed in batch"), + error: new Error("Maximum of 25 tools allowed in batch"), }) } diff --git a/packages/opencode/src/tool/batch.txt b/packages/opencode/src/tool/batch.txt index b1b6a6010f..565eb4dd43 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–10 tool calls per batch +- 1–20 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 4e04bee0c91e445e692ef312a9b8136189fceb16 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:45:55 -0600 Subject: [PATCH 17/23] fix(app): favicon --- packages/app/src/pages/layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 9daac949e4..d0d7e0b2f0 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -540,7 +540,7 @@ export default function Layout(props: ParentProps) { running: number } - const prefetchChunk = 200 + const prefetchChunk = 600 const prefetchConcurrency = 1 const prefetchPendingLimit = 6 const prefetchToken = { value: 0 } @@ -1284,7 +1284,7 @@ export default function Layout(props: ParentProps) {
Date: Mon, 19 Jan 2026 21:55:27 +0000 Subject: [PATCH 18/23] release: v1.1.26 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index e5892a7745..53d80630b8 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -71,7 +71,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -105,7 +105,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -132,7 +132,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -156,7 +156,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -180,7 +180,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -209,7 +209,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -238,7 +238,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -254,7 +254,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.25", + "version": "1.1.26", "bin": { "opencode": "./bin/opencode", }, @@ -358,7 +358,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -378,7 +378,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.25", + "version": "1.1.26", "devDependencies": { "@hey-api/openapi-ts": "0.90.4", "@tsconfig/node22": "catalog:", @@ -389,7 +389,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -402,7 +402,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -443,7 +443,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "zod": "catalog:", }, @@ -454,7 +454,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 2a754c9673..736a262504 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.25", + "version": "1.1.26", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 2f44637fc8..83ff605cff 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.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index a9bb2706d4..eb86fb525b 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.25", + "version": "1.1.26", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 6ada8abb05..6cfdaab757 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.25", + "version": "1.1.26", "$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 7fe57cc79a..ab2fd76f8b 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.25", + "version": "1.1.26", "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 9114ff5621..0b95f75487 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index e80a58b2d7..1a40af1c79 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.25", + "version": "1.1.26", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index dccac3f933..2327c61e66 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.25" +version = "1.1.26" 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.25/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/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.25/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/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.25/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/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.25/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index ad83a519c2..e5b0f62b92 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.25", + "version": "1.1.26", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index e191819347..297abc8598 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.25", + "version": "1.1.26", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 2734901808..52f392a817 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.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { @@ -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 f3b12aa8c9..5ce97dbf7e 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.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index d544b89e38..37b116ec2b 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 0b490591c3..7079384a54 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index a1d2ac6b56..45c6d43f0a 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.25", + "version": "1.1.26", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index aef7c0c706..db1adbf926 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.1.25", + "version": "1.1.26", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index ad4735765e..0a48840db0 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.1.25", + "version": "1.1.26", "publisher": "sst-dev", "repository": { "type": "git", 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 19/23] 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 a918f0ae4f..360589f411 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 20/23] 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 52f392a817..ee7cf23b9f 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 5ce97dbf7e..f69224d832 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 21/23] 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 0edc911344..392cfb7f12 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 22/23] 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 d0d7e0b2f0..e0cc38b9af 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 23/23] 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 ec3b0ac30d..700d2b695a 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