You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TUI does not re-layout on terminal shrink (only on grow), leaving stale width #42225
When the terminal is resized, the opencode TUI re-layouts on grow but not on shrink, and not on the initial attach. The drawn width stays at its previous
value, so the TUI either leaves blank space or overflows the terminal.
Measured in a browser-hosted terminal (xterm.js over a WebSocket PTY, opencode
1.18.16):
terminal columns
columns opencode actually draws
result
100 (initial)
78
under-fills — blank space on the right
119 (grown)
117
correct, follows the resize
68 (shrunk)
117
stale — overflows the terminal
The resize does reach the process
This is not a missing-notification problem. The chain was verified layer by layer:
the terminal element resizes
xterm.js recomputes its grid (26 -> 15 -> 31 rows)
the frontend sends the new size over the WebSocket: {"type":"resize","data":{"cols":68,"rows":27}}
the PTY receives it and delivers SIGWINCH — a plain bash on the same host over
the same transport re-wraps correctly, so the signal is being delivered
Only the last step fails: opencode receives the new size and re-layouts on grow, but
keeps the old width on shrink and on the initial draw.
Workaround
Forcing a redraw inside the TUI (e.g. Ctrl+L) restores the correct layout, which is
consistent with "the size is known, but no re-layout is scheduled".
Environment
opencode-ai 1.18.16 (release notes for 1.18.17 and 1.18.18 contain no
resize-related changes, so this is likely still present)
Linux x86_64; the terminal is xterm.js in a browser, over a WebSocket PTY bridge
Reproduces on every attach; not specific to a model or provider
Same root cause here, confirmed from a second browser-hosted deployment (xterm.js over a WebSocket PTY bridge, opencode 1.18.18 on Linux). Just wanted to add the concrete symptom + evidence and confirm the workaround, in case it helps the fix land.
Symptom: the stale width doesn't only leave blank space/overflow — it garbles the sidebar and status bar, because they're drawn at the stale width's absolute columns. In a real session the sidebar showed:
dropped letters: Host client → H s client, local + remote backend → l cal + remote back nd
a token counter split across rows with digits at every-2-column positions, e.g. \x1b[6;201H 9, \x1b[6;203H 8, \x1b[6;205H 3
the status-bar cwd /home/lenovo/ai-remote-hub drawn as [49;4H /home/lenovo/a + [49;19H -r + [49;22H mote-hub (columns 18 and 21 never written)
All of it is in the raw PTY output — the terminal emulator and transport relay it faithfully. It develops over time (prompts + session switches = resizes), never on the welcome screen.
Workaround confirmed + automated: injecting Ctrl+L (\x0c) into the PTY after each geometry change forces opencode to repaint at the freshly-applied size and clears the garble every time. We shipped that server-side for our opencode sessions and it resolves it reliably.
Impact: this hits any deployment embedding opencode in a browser/web PTY (xterm.js, hterm, VS Code web, tmate-style bridges), and "shrink" happens constantly there (tab switches, keyboard, resize). A maintainer fix to re-layout on shrink / initial attach would be very welcome — happy to provide more trace data.
Correction to my earlier comment: I claimed injecting Ctrl+L after resizes resolves it reliably — that did not hold up on opencode 1.18.18. Measured against a live session:
sending \x0c (Ctrl+L) into the PTY produces zero bytes of output — opencode ignores the unbound key entirely, no repaint happens
Ctrl+P (command palette) does trigger a full re-render, but it re-renders at the stale width: after shrinking the PTY to 150 cols the redraw still draws at columns 197–230, so the sidebar/status stay wrong
the welcome/first screen does re-layout correctly on shrink, but the full conversation view does not
So an input-injected workaround won't work in 1.18.18 — the TUI needs to actually re-read its size and re-layout on SIGWINCH (or a real terminal redraw), not just get a keystroke. Only restarting the session reliably recovers it. This looks like the more general form of #42225 (no re-layout on shrink), so fixing it properly is worth it — happy to provide the capture data.
I can take this. I’ll first add a focused regression test for terminal shrink/re-layout, then keep the fix scoped to the resize path. Could you assign this issue to me?
The failing path is OpenTUI's SIGWINCH debounce retaining cached stdout dimensions when the signal arrives before the runtime cache reflects the new PTY size. The patch refreshes and samples the TTY dimensions when the debounce expires before triggering re-layout. OpenCode itself did not need a separate TUI scheduling change in this reproduction.
PR opened: #42330. @HermanShi@fluffyspace, if you can still reproduce this setup, please test the fix and add a 👍/❤️/🎉/🚀 reaction to the PR body so it stays visible to the cleanup automation.
Same bug on Windows 11 with Orca IDE (xterm.js terminal, same as VS Code), opencode-ai 1.18.18 (npm global install).
Repro pattern: artifacts appear right after a task finishes / final output is rendered — leftover overlapping panels, garbled sidebar/status bar. The only reliable recovery is changing the tab zoom/scale in Orca (forces a grow resize → re-layout). Restarting the opencode session also recovers.
Additional data point for the maintainers:
Terminal renderer is NOT the cause: I switched Orca's terminal from WebGL to DOM renderer (GPU Acceleration → Off, xterm.js DOM renderer) and the artifacts still appear. This is the OpenTUI stale-width/shrink issue, independent of the renderer.
The shrink happens constantly in Orca (tab switches, panel layout changes after session end), so the bug is hit on every session completion.
Voting for the fix in PR #42474 / opentui PR #1363 to land — this makes opencode unusable-ish inside Orca on Windows.
Still reproduces on the V2 TUI (v2.0.18) — macOS 26.6.2 + Ghostty 1.3.1 — and the frozen state is worse than stale width: typed input stops being drawn
Adding a macOS field report (native terminal, not a browser PTY bridge). On the V2 TUI the shrink case doesn't just leave stale geometry: after the layout gets pinned to the old size, the prompt/input area ends up drawn outside the visible viewport, so typing produces nothing visible. The process stays alive and keeps receiving keys. For a non-technical user the only reliable recovery is killing the TUI client and resuming the session.
Environment
opencode version: 2.0.18 (opencode --version → v2.0.18; V2 channel, latest published at the time of writing)
OS: macOS 26.6.2 (build 25G83), Darwin 25.6.0 arm64 (Apple M2)
Install: ~/.opencode/bin/opencode (v2 binary), background service healthy (opencode service status → http://127.0.0.1:49374)
Active TUI plugins: local plugin bridges under ~/.config/opencode/plugins/ (context-usage, rundev). They are loaded in the repro below; the root cause described here is independent of them.
Reproduction (user flow, intermittent but frequent)
Open Ghostty, cd to a project, launch opencode.
Toggle native macOS fullscreen and work in the session (happens with both short and long sessions).
Leave fullscreen → the window shrinks (also triggers on split, and on drag-to-edge macOS tiling: half/quarter screen).
The TUI keeps the previous geometry. Content is clipped/misplaced and the input line no longer renders. Typing produces no visible characters.
Further resizes, Ctrl+P (command palette), and Ghostty super+shift+e (equalize_splits) do not recover it.
Only restarting the TUI client recovers; the session is intact server-side (opencode -c resumes it).
Automated reproduction (isolated PTY, same host)
opencode --standalone in a private PTY with a temporary database (OPENCODE_DB), animations disabled, driven by a Python harness:
Resize storm: 170 TIOCSWINSZ events over ~4s while typing, then settle at 62x18. The app kept drawing (2.8 KB of output) but a subsequently typed marker string never appeared on screen.
A forced grow re-render resumed drawing immediately.
Slow, single-step shrink/grow cycles (including 110x32 → 62x18 bursts) did not reproduce on their own — consistent with the debounce/race, not with a missing-signal problem.
Root cause
Matches the analysis already in this issue and opentui#1363: the debounced SIGWINCH path samples cached stdout dimensions that are still stale when the signal arrives ahead of the runtime cache update. Shrink is the worst case because the stale geometry is larger than the real viewport, so content (including the prompt) is positioned off-screen.
Workarounds measured against the frozen state (least → most disruptive)
Action
Result
Ghostty super+shift+e (equalize_splits)
no effect — only rebalances Ghostty splits, does not repaint the TUI or resend a correct size
Ctrl+P (command palette) + Esc
repaints, but sometimes at the stale geometry; occasionally unfreezes input
Ctrl+Z (terminal.suspend) then fg
full repaint with correct geometry ✅
Force a grow: cmd+- (smaller font → more columns) then cmd+0
resumes drawing ✅
pkill -WINCH -x opencode from another tab while the window already has the new size
resumes drawing ✅
Kill TUI client + opencode -c in a new tab
always recovers (session survives on the server)
Impact
On Ghostty/macOS this is a hard usability break rather than a cosmetic artifact: the session becomes unusable until the client is restarted. The only mitigations that work from inside the frozen TUI are forcing a grow re-layout (font size change) or suspend/resume.
A standalone reproduction harness (Python, PTY + TIOCSWINSZ) is available on request and can be attached to this issue.
Thanks for the V2/Ghostty reproduction and the resize-storm measurements. I checked the current OpenCode dev source (1.18.33): it delegates resize handling to @opentui/core 0.4.5 and has no separate SIGWINCH handler. The related OpenTUI PR #1363 remains open; its head refreshes the selected TTY after the resize debounce, and the checks recorded for that head passed. I have not verified your v2.0.18/Ghostty harness against it. OpenCode PRs #42330 and #42474 both closed unmerged. If you can attach the standalone harness or a result against the current #1363 head here, that would show whether the frozen-input case is covered by the dependency fix or needs an OpenCode-specific follow-up.
Against the real v2.0.18 binary (isolated XDG_CONFIG_HOME, temp DB, opencode --standalone, animations: false, only the probe plugin), driving a raw PTY with TIOCSWINSZ. The probe is a CLI plugin using context.renderer that samples every 250 ms:
renderer.width/height (the geometry the TUI lays out with),
plus a passive SIGWINCH counter and optional rescue hooks.
Scenarios per iteration, each followed by typing a marker and checking whether the TUI draws it: (1) shrink burst 150→62 cols, (2) fullscreen-like grow then shrink, (3) random drag (14 steps), (4) 600-word paste immediately followed by a resize burst (4 ms steps).
Version corrections
v2.0.18's TUI resolves @opentui/core 0.5.12 (workspace catalog at tag v2.0.18), not 0.4.5 — 0.4.5 is the V1 branch (1.18.33) you inspected.
opentui#1363 head is based on core 0.5.10 (packages/core/package.json on the branch): behind the core that ships in v2.0.18.
process.stdout._refreshSizedoes exist in the shipping runtime (Bun 1.4.2, node=v26.3.0), so the refresh API the PR uses is available; availability is not the blocker.
Results
arm
it1–it3
it4 (paste + burst)
it5–it8
final explicit grow/shrink
markers drawn
stock (embedded 0.5.12)
ok
frozen at 56x18
never recovers
no recovery
5/8 invisible
PR-style handler installed at runtime (fresh dims from stty + processResize, same 100 ms debounce)
The freeze is sticky and identical in all three arms: once triggered, subsequent grow, shrink and drag resizes have no effect.
What the frozen state actually looks like
Renderer geometry stuck at 56x18 while the PTY is 62x18; process.stdout.columns/rows equally stale.
The probe's timers stop firing entirely after the trigger (250 ms sampler and a 4 s heartbeat go silent while the run continues ~30 s longer). That is also why the periodic rescue never ran: setInterval callbacks stopped executing, not because the size check failed.
SIGWINCH events were still being delivered before the silence.
So the fragile path does not look like "returns after the debounce and keeps sampling stale dimensions" — that would still tick. The TUI's JS loop appears to wedge under a resize burst while a heavy render is in flight, and no dimension refresh recovers it afterwards.
Caveats (so this is not over-read)
I could not rebuild the shipped binary against the #1363 head; I installed the head's handler semantics at runtime instead (replacing renderer.sigwinchHandler, with stty as the refresh source). It does not reach the failure mode we see (timers stop), so I do not think hydration ordering is the deciding factor, but a true head build is still the only way to fully exclude it.
With the runtime patch, SIGWINCH listener count stays at 3 — I removed and replaced the renderer's handler, but cannot prove no other listener still runs the old path.
Reproduction is deterministic in this harness but load/timing dependent: gentle single-step resizes alone do not trigger it.
User-facing symptoms that match this
The reporter's flow (fullscreen → work → leave fullscreen, sometimes with splits or drag-to-edge) matches scenario 2/4. From the same machine: Ctrl+Z/fg repaints, forcing a grow relayout (font-size change) recovers when the loop still responds, and restarting the TUI client (opencode -c) always recovers — session state is server-side.
Happy to run a true #1363 build inside this harness if someone can point at a built artifact (or confirm the branch rebased onto 0.5.12+).
@angeloper86 Incredible investigation — thanks for putting together the harness and running the A/B test! That's a key finding: it's not stale dimensions after all, but the TUI JS loop itself wedging under a resize burst during a heavy render (timers stop firing entirely, which explains why the periodic rescue never ran either). That completely changes where to look — the problem is in the event loop getting blocked, not in dimension refresh. I'll dig into the render path for places where synchronous work or a missing microtask yield could stall the loop under paste+resize load.
Activity
fluffyspace commentedon Aug 13, 2026
Same root cause here, confirmed from a second browser-hosted deployment (xterm.js over a WebSocket PTY bridge, opencode 1.18.18 on Linux). Just wanted to add the concrete symptom + evidence and confirm the workaround, in case it helps the fix land.
Symptom: the stale width doesn't only leave blank space/overflow — it garbles the sidebar and status bar, because they're drawn at the stale width's absolute columns. In a real session the sidebar showed:
Host client→H s client,local + remote backend→l cal + remote back nd\x1b[6;201H 9,\x1b[6;203H 8,\x1b[6;205H 3/home/lenovo/ai-remote-hubdrawn as[49;4H /home/lenovo/a+[49;19H -r+[49;22H mote-hub(columns 18 and 21 never written)All of it is in the raw PTY output — the terminal emulator and transport relay it faithfully. It develops over time (prompts + session switches = resizes), never on the welcome screen.
Workaround confirmed + automated: injecting Ctrl+L (
\x0c) into the PTY after each geometry change forces opencode to repaint at the freshly-applied size and clears the garble every time. We shipped that server-side for ouropencodesessions and it resolves it reliably.Impact: this hits any deployment embedding opencode in a browser/web PTY (xterm.js, hterm, VS Code web, tmate-style bridges), and "shrink" happens constantly there (tab switches, keyboard, resize). A maintainer fix to re-layout on shrink / initial attach would be very welcome — happy to provide more trace data.
fluffyspace commentedon Aug 13, 2026
Correction to my earlier comment: I claimed injecting Ctrl+L after resizes resolves it reliably — that did not hold up on opencode 1.18.18. Measured against a live session:
\x0c(Ctrl+L) into the PTY produces zero bytes of output — opencode ignores the unbound key entirely, no repaint happensSo an input-injected workaround won't work in 1.18.18 — the TUI needs to actually re-read its size and re-layout on SIGWINCH (or a real terminal redraw), not just get a keystroke. Only restarting the session reliably recovers it. This looks like the more general form of #42225 (no re-layout on shrink), so fixing it properly is worth it — happy to provide the capture data.
fancive commentedon Aug 13, 2026
I can take this. I’ll first add a focused regression test for terminal shrink/re-layout, then keep the fix scoped to the resize path. Could you assign this issue to me?
dajiaohuang commentedon Aug 13, 2026
I reproduced this at the OpenTUI layer, including initial attach, grow, and shrink, and opened an upstream fix:
anomalyco/opentui#1363
The failing path is OpenTUI's
SIGWINCHdebounce retaining cachedstdoutdimensions when the signal arrives before the runtime cache reflects the new PTY size. The patch refreshes and samples the TTY dimensions when the debounce expires before triggering re-layout. OpenCode itself did not need a separate TUI scheduling change in this reproduction.fancive commentedon Aug 13, 2026
PR opened: #42330. @HermanShi @fluffyspace, if you can still reproduce this setup, please test the fix and add a 👍/❤️/🎉/🚀 reaction to the PR body so it stays visible to the cleanup automation.
4elovekyc commentedon Aug 20, 2026
Same bug on Windows 11 with Orca IDE (xterm.js terminal, same as VS Code), opencode-ai 1.18.18 (npm global install).
Repro pattern: artifacts appear right after a task finishes / final output is rendered — leftover overlapping panels, garbled sidebar/status bar. The only reliable recovery is changing the tab zoom/scale in Orca (forces a grow resize → re-layout). Restarting the opencode session also recovers.
Additional data point for the maintainers:
Voting for the fix in PR #42474 / opentui PR #1363 to land — this makes opencode unusable-ish inside Orca on Windows.
angeloper86 commentedon Sep 28, 2026
Still reproduces on the V2 TUI (v2.0.18) — macOS 26.6.2 + Ghostty 1.3.1 — and the frozen state is worse than stale width: typed input stops being drawn
Adding a macOS field report (native terminal, not a browser PTY bridge). On the V2 TUI the shrink case doesn't just leave stale geometry: after the layout gets pinned to the old size, the prompt/input area ends up drawn outside the visible viewport, so typing produces nothing visible. The process stays alive and keeps receiving keys. For a non-technical user the only reliable recovery is killing the TUI client and resuming the session.
Environment
opencode --version→v2.0.18; V2 channel, latest published at the time of writing)TERM_PROGRAM=ghostty,TERM=xterm-256color,COLORTERM=truecolor)~/.opencode/bin/opencode(v2 binary), background service healthy (opencode service status→http://127.0.0.1:49374)~/.config/opencode/plugins/(context-usage,rundev). They are loaded in the repro below; the root cause described here is independent of them.Reproduction (user flow, intermittent but frequent)
cdto a project, launchopencode.Ctrl+P(command palette), and Ghosttysuper+shift+e(equalize_splits) do not recover it.opencode -cresumes it).Automated reproduction (isolated PTY, same host)
opencode --standalonein a private PTY with a temporary database (OPENCODE_DB), animations disabled, driven by a Python harness:TIOCSWINSZevents over ~4s while typing, then settle at 62x18. The app kept drawing (2.8 KB of output) but a subsequently typed marker string never appeared on screen.110x32 → 62x18bursts) did not reproduce on their own — consistent with the debounce/race, not with a missing-signal problem.Root cause
Matches the analysis already in this issue and
opentui#1363: the debouncedSIGWINCHpath samples cachedstdoutdimensions that are still stale when the signal arrives ahead of the runtime cache update. Shrink is the worst case because the stale geometry is larger than the real viewport, so content (including the prompt) is positioned off-screen.Workarounds measured against the frozen state (least → most disruptive)
super+shift+e(equalize_splits)Ctrl+P(command palette) +EscCtrl+Z(terminal.suspend) thenfgcmd+-(smaller font → more columns) thencmd+0pkill -WINCH -x opencodefrom another tab while the window already has the new sizeopencode -cin a new tabImpact
On Ghostty/macOS this is a hard usability break rather than a cosmetic artifact: the session becomes unusable until the client is restarted. The only mitigations that work from inside the frozen TUI are forcing a grow re-layout (font size change) or suspend/resume.
A standalone reproduction harness (Python, PTY +
TIOCSWINSZ) is available on request and can be attached to this issue.dajiaohuang commentedon Sep 28, 2026
Thanks for the V2/Ghostty reproduction and the resize-storm measurements. I checked the current OpenCode dev source (1.18.33): it delegates resize handling to @opentui/core 0.4.5 and has no separate SIGWINCH handler. The related OpenTUI PR #1363 remains open; its head refreshes the selected TTY after the resize debounce, and the checks recorded for that head passed. I have not verified your v2.0.18/Ghostty harness against it. OpenCode PRs #42330 and #42474 both closed unmerged. If you can attach the standalone harness or a result against the current #1363 head here, that would show whether the frozen-input case is covered by the dependency fix or needs an OpenCode-specific follow-up.
angeloper86 commentedon Sep 29, 2026
Follow-up: harness attached + A/B results — on v2.0.18 the frozen-input case is not a stale-dimensions problem, it is a TUI JS-loop stall
Thanks for the pointers. Harness: https://gist.github.com/angeloper86/15f078cfb853b531dc0f89b77d4795d7 (PTY +
TIOCSWINSZdriver, plus an in-process probe plugin).Method
Against the real v2.0.18 binary (isolated
XDG_CONFIG_HOME, temp DB,opencode --standalone,animations: false, only the probe plugin), driving a raw PTY withTIOCSWINSZ. The probe is a CLI plugin usingcontext.rendererthat samples every 250 ms:renderer.width/height(the geometry the TUI lays out with),process.stdout.columns/rows(Bun's cached TTY view),stty size(independent source, separate process),SIGWINCHcounter and optional rescue hooks.Scenarios per iteration, each followed by typing a marker and checking whether the TUI draws it: (1) shrink burst 150→62 cols, (2) fullscreen-like grow then shrink, (3) random drag (14 steps), (4) 600-word paste immediately followed by a resize burst (4 ms steps).
Version corrections
@opentui/core 0.5.12(workspace catalog at tagv2.0.18), not 0.4.5 — 0.4.5 is the V1 branch (1.18.33) you inspected.opentui#1363head is based on core 0.5.10 (packages/core/package.jsonon the branch): behind the core that ships in v2.0.18.process.stdout._refreshSizedoes exist in the shipping runtime (Bun 1.4.2,node=v26.3.0), so the refresh API the PR uses is available; availability is not the blocker.Results
stty+processResize, same 100 ms debounce)stdout._refreshSize()+renderer.processResize()+renderer.resize())The freeze is sticky and identical in all three arms: once triggered, subsequent grow, shrink and drag resizes have no effect.
What the frozen state actually looks like
56x18while the PTY is62x18;process.stdout.columns/rowsequally stale.setIntervalcallbacks stopped executing, not because the size check failed.SIGWINCHevents were still being delivered before the silence.So the fragile path does not look like "returns after the debounce and keeps sampling stale dimensions" — that would still tick. The TUI's JS loop appears to wedge under a resize burst while a heavy render is in flight, and no dimension refresh recovers it afterwards.
Caveats (so this is not over-read)
#1363head; I installed the head's handler semantics at runtime instead (replacingrenderer.sigwinchHandler, withsttyas the refresh source). It does not reach the failure mode we see (timers stop), so I do not think hydration ordering is the deciding factor, but a true head build is still the only way to fully exclude it.SIGWINCHlistener count stays at 3 — I removed and replaced the renderer's handler, but cannot prove no other listener still runs the old path.User-facing symptoms that match this
The reporter's flow (fullscreen → work → leave fullscreen, sometimes with splits or drag-to-edge) matches scenario 2/4. From the same machine:
Ctrl+Z/fgrepaints, forcing a grow relayout (font-size change) recovers when the loop still responds, and restarting the TUI client (opencode -c) always recovers — session state is server-side.Happy to run a true
#1363build inside this harness if someone can point at a built artifact (or confirm the branch rebased onto 0.5.12+).dajiaohuang commentedon Oct 7, 2026
@angeloper86 Incredible investigation — thanks for putting together the harness and running the A/B test! That's a key finding: it's not stale dimensions after all, but the TUI JS loop itself wedging under a resize burst during a heavy render (timers stop firing entirely, which explains why the periodic rescue never ran either). That completely changes where to look — the problem is in the event loop getting blocked, not in dimension refresh. I'll dig into the render path for places where synchronous work or a missing microtask yield could stall the loop under paste+resize load.
thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊