Repository navigation
Description
Summary
On Windows, exiting the TUI leaves the shared console in raw mode with mouse
reporting still enabled. On ConPTY stacks (Alacritty + zellij-windows) this does
not only corrupt the shell prompt — it can take down the whole multiplexer
session.
This is the V2 report. The V1 report (#48776) and the V1 fix attempt (#48878)
were both closed during the 1.x -> 2.0 migration, and the teardown code in 2.0 is
structurally identical to the V1 code that reproduces the bug.
Affected version
2.0.x (verified against 2.0 @ 7a6ce05d09, @opentui/core 0.1.97)
Reproduction
- Windows 11, Alacritty 0.17.0, zellij-windows 0.43.1-win32.5
- Run
opencode, then/exit - Observed, non-deterministically:
- the whole zellij session goes down, or
- a clean exit, or
- leaked alternate screen plus raw-mode corruption (stray characters,
mouse-report sequences, mouse mapped as keyboard input)
Why it still happens on 2.0
The TUI never restores the terminal itself — every teardown path delegates to
@opentui, which hands the terminal back from inside process teardown, which is
where the ConPTY race happens.
packages/opencode/src/cli/cmd/tui/context/exit.tsx:
renderer.setTerminalTitle("")
renderer.destroy()
...
process.on("SIGHUP", () => exit())packages/opencode/src/cli/cmd/tui/component/error-component.tsx has the same
renderer.destroy() call, so the error boundary is a second affected path.
In @opentui/core 0.1.97, CliRenderer.destroy() -> finalizeDestroy() ->
this.lib.destroyRenderer(this.rendererPtr) -> opentui.symbols.destroyRenderer(),
which is the Zig-native call that owns setupTerminal() and the
\x1B[?1049h / \x1B[?1049l alternate-screen transitions.
There is no SIGTERM handler and no process.on("exit") restore, so a crash or
an unhandled rejection leaves the console dirty too.
Related issues
All reported against 1.x and closed as not planned in the V2 triage; none has a
fix landed:
- After closing the OpenCode CLI, scrolling the mouse wheel and moving the mouse in the powershell will cause garbled characters. #11748 — garbled characters on mouse move after closing the TUI
- [Windows] Terminal left with raw ANSI escape codes after crash (PowerShell) #21277 — raw ANSI escape codes left in terminal after a crash
- WSL2 Ubuntu-26.04 terminal crashes after /exit #30133 — WSL2 terminal crash after
/exit - Running in RMUX: terminal unresponsive after /exit #38068 — terminal unresponsive after
/exitinside a multiplexer - TUI not responding and mapping mouse as keyboard in the console #46216 — TUI mapping mouse as keyboard in the console
- macOS: exit/cleanup path broadcasts SIGTERM to ALL user processes (kill -1) — kills Ghostty, Terminal, Chrome, entire session (1.18.24 & 1.18.25) #45938 — exit cleanup kills the whole session on macOS
- TUI leaves terminal in raw/corrupted state on /exit under Alacritty + zellij-windows (Windows ConPTY) #48776 — the original ConPTY canonical report (closed 2026-10-11)
- fix(tui): force terminal reset on exit for Windows ConPTY #48878 — a V1 fix attempt, closed by the V1->V2 promotion bot
What I would like
Happy to send a PR. The approach for V2 is to restore the terminal modes from
opencode itself and skip the native teardown on Windows, since the native handle
is released on process exit anyway. Two things worth a steer before I open it:
- Should this live in the TUI (as above), or is it better fixed in
@opentui/core's teardown? libisprivateonCliRenderer, so the skip has to reach for it. Is
there a supported way to do that?
Environment
- OS: Windows 11
- Terminal: Alacritty + zellij-windows
- opencode: 2.0.x, built from source
Activity
opencode-agent commentedon Oct 13, 2026
Thanks for the detailed write-up and for offering a PR!
Before we try to reproduce this, we need to confirm it happens on a current v2 build. Some details in the report don't match v2's code:
v2.0.28anddev) uses@opentui/core0.6.2, not 0.1.97.packages/tui/src/....packages/tui/src/context/exit.tsxdoesn't callrenderer.destroy(). Teardown goes throughdestroyRenderer()inpackages/tui/src/util/renderer.ts.7a6ce05d09in the repository.The paths and versions you cited match v1, so the build you tested may have been 1.x. Could you please:
npm i -g @opencode/cli, oropencode upgrade) and paste the output ofopencode --version.opencodein Alacritty + zellij-windows, then/exit. Does the problem still happen? Roughly how often do you get each result (zellij session goes down / clean exit / leaked alternate screen or mouse sequences)?You can reply here with the details, or edit the issue. If you edit it, please also leave a comment saying you've updated it, so we know you're done.
Related issues, ordered by likelihood. None of them is a v2 duplicate:
kraso commentedon Oct 13, 2026
You were right on all three points, and the mistake was mine.
What I got wrong. I based the report on the
2.0branch, not ondev.2.0still exists —7a6ce05d0939826aa6c8e1c481489a713b2d633fis its head — but it is an abandoned pre-restructure layout: the TUI lived underpackages/opencode/src/cli/cmd/tui/and pinned@opentui/core0.1.97. Currentdevise9def88a77, with the TUI back underpackages/tui/and a newer opentui. So the paths and version I cited describe2.0, not current v2. The commit does exist in the repository, just not reachable fromdev.I also checked npm for
opencode-aiwhen the v2 package is@opencode/cli. That is why I wrongly concluded v2 was unreleased.1. Official build
Installed into a scratch directory rather than globally, so it did not shadow the 1.x install.
2. Observed behaviour
With a local patch applied, on the
2.0branch build (2.0.11, opentui 0.1.97): across repeated open/exit cycles in Alacritty + zellij-windows, zellij stayed alive and the exit epilogue stayed visible every time, including when starting and resuming sessions.Without the patch (1.18.31 and earlier) the outcome was non-deterministic — across repeated cycles I observed the zellij session going down, a clean exit, and leaked alternate screen / mouse sequences. I do not have a measured frequency distribution for each outcome, only that all three occur.
3. Without a multiplexer
Not isolated. I have not tested Alacritty alone or Windows Terminal without zellij, so I cannot say whether ConPTY alone triggers it or whether the multiplexer is required. I will test that and report back.
Is it still present in current v2?
By code inspection on
devit looks still live:packages/tui/src/util/renderer.tshas adestroyRenderer()with no terminal reset and no platform guard, andpackages/tui/src/app.tsxregisters onlySIGHUP, with noSIGTERMand noprocess.on("exit")restore. I have not yet run the official 2.0.28 binary under Alacritty + zellij-windows to confirm at runtime, so please treat that as a code-level reading rather than a reproduction.PR
#55165 targets
2.0, so it is against the wrong tree. I will close it and open a corrected one againstdev.opencode-agent commentedon Oct 13, 2026
Thanks for tracking that down. It helps a lot to know the earlier details came from the old
2.0branch.One thing is still missing before we try to reproduce: none of the runs so far used current v2. The broken exits were seen on 1.18.31 and earlier, and the clean ones on a patched build of the old
2.0branch (2.0.11,@opentui/core0.1.97). 2.0.28 ships@opentui/core0.6.2, and OpenTUI's teardown has changed a lot between those versions. So the missing reset you spotted inpackages/tui/src/util/renderer.tsmay or may not still cause the problem.Could you run the unpatched official 2.0.28 in Alacritty + zellij-windows and tell us:
opencode→/exitcycles, does any of the three outcomes still happen (zellij session goes down, leaked alternate screen, mouse or escape sequences in the shell)? A rough count is fine, e.g. "2 of 10"./exit?If it still happens on 2.0.28, a PR against
devis welcome. Linking it here with the repro results will make it easier to review.Related issues, ordered by likelihood:
kraso commentedon Oct 13, 2026
Reproduced on the official build. Answering all three of your questions.
Build:
opencode v2.0.28, installed from@opencode/cli(no local patches, no source build).1 / 2 — Does it still happen, and how often
In the Alacritty-alone run that did not fail, the exit epilogue was printed and the terminal stayed alive.
So roughly 5 of 6 cycles fail. This is the common case, not a rare race.
3 — Does it need a multiplexer
No. It reproduces in Alacritty on its own, with no multiplexer involved. That rules out the multiplexer as a necessary ingredient and points at the ConPTY host console itself being taken down during teardown.
What that changes about the diagnosis
My earlier assumption was that the multiplexer was required. It is not. The teardown reaches the console through OpenTUI's native path, and it is enough on its own.
For reference, the code on
devstill has no terminal restore:packages/tui/src/util/renderer.tsdestroyRenderer()is just anisDestroyedguard, a title reset andrenderer.destroy(), andpackages/tui/src/app.tsxregisters onlySIGHUP— noSIGTERM, noprocess.on("exit")restore.On the fix I was going to propose
The approach from #48878 patched
renderer.lib.destroyRendererto suppress OpenTUI's native teardown. That specific method no longer exists in@opentui/core0.6.2 — the lib surface is nowdestroySession/sessionClose/sessionPumpExit. So that patch does not carry over cleanly, and pinning a PR to a private method that has already changed shape once is a poor trade.Given the reproduction rate and that it does not need a multiplexer, I would rather fix this in OpenTUI than work around it here — it owns the native teardown and now also appears to own process exit handling (
exitSignals,addExitListeners). Happy to still open something on our side if you would rather land a mitigation first, but I wanted to flag the tradeoff before spending another cycle on it.kraso commentedon Oct 13, 2026
Follow-up: Ctrl+C versus
/exit, and a correction to how I phrased the epilogue.Correction first. Where I wrote "no epilogue" for the hard-close runs, that was wrong — I only know the terminal died before the epilogue could be seen. In a hard close the epilogue is indeterminate, not absent. I have not verified whether it printed.
Ctrl+C vs
/exit/exit/exitCtrl+C and
/exitbehave the same: 5 of 6 hard closes each. The close method is not a factor.Across both methods, 10 of 12 cycles ended with the pane or the whole terminal window dying. A hard close means the epilogue state is unknown, so the clean-exit column above only counts the cycles where the shell actually survived.
What a hard close actually is, on Windows
In the Alacritty-alone runs the entire terminal window exits, not just the pane. That is the host ConPTY console going away with the process rather than being handed back. It matches the teardown running inside process shutdown: OpenTUI 0.6.2's
destroy()schedulesnativeSession.close()in a microtask and returns, so the native teardown lands while the process is already on its way out.On that basis I would not pin a PR to OpenTUI internals. The part of the old #48878 patch that is portable is the
process.on("exit")restore, which does not depend on any private API and runs synchronously last. Proposing that as a mitigation, and treating the rest as an OpenTUI-side fix.opencode-agent commentedon Oct 13, 2026
Thanks for the careful runs on 2.0.28. The cycle counts and the result without zellij are very helpful.
We can't reproduce this in our automated environment. It needs Windows with ConPTY and Alacritty, and our sandbox is Linux only. I've flagged it for a maintainer who has a Windows setup.
What you've shown so far:
opencode v2.0.28, on Windows 11 with Alacritty 0.17.0./exitand Ctrl+C alike.Your point about where the fix belongs (OpenTUI's native teardown, possibly plus a synchronous
process.on("exit")restore in OpenCode) is noted for the maintainers to decide. If you test other Windows terminals (e.g. Windows Terminal or WezTerm), whether they're affected would help narrow it down further.Related: #48776 (the original v1 report for this setup).
kraso commentedon Oct 15, 2026
Root cause found, and it is not
clearOnShutdown.I was wrong in my previous comment about the mechanism, so to be explicit about it: setting
clearOnShutdown: falseon the interactive renderer changes nothing. That option gates only one branch of the teardown, and the interactive TUI never reaches it.The TUI does not set
screenMode, so OpenTUI resolves it to its default:So the interactive TUI runs in alternate-screen mode and its shutdown takes the first branch of the teardown, where
resetStateexits the alternate screen, rather than the branchclearOnShutdowngates:That also explains why only this renderer is affected.
packages/cli/src/ui/timeline.tsxandpackages/tui/src/mini/runtime.lifecycle.tsboth setscreenMode: "split-footer", so they never touch the alternate screen at all.Experiment. One variable changed, same build, same host.
screenModealternate-screen(default)/exitand Ctrl+Cmain-screenSix runs either way: 3 in Alacritty + zellij and 3 in Alacritty alone, split across
/exitand Ctrl+C. This is againstdev(0.5.17, after #55300), not only 2.0.28/0.6.2.So what takes the host console down is leaving the alternate screen during process teardown, not a screen-region clear.
What I have and have not established. I have established that
screenModeis the deciding variable, cleanly, 6/6 versus 0/6. I have not established the exact mechanism: whether the trigger is specifically the?1049lsequence, the ordering of that write relative to process exit, or a ConPTY-host interaction. Narrowing that needs a capture of the raw byte stream at exit, which I do not have.Where a fix belongs.
screenMode: "main-screen"does resolve it, but it changes TUI behaviour on every platform, no fullscreen and accumulating scrollback, so that is a product decision rather than a bug fix. Making it Windows-only would preserve current behaviour everywhere it currently works. Alternatively, if leaving the alternate screen during teardown is what breaks ConPTY hosts, that is an OpenTUI-side fix rather than an opencode one.I am deliberately not attaching a speculative patch to this comment. #55223 was my attempt at the
clearOnShutdownmechanism and it is a no-op, so I closed it.kraso commentedon Oct 15, 2026
Resolution: this is not an opencode bug, and there is a workaround available today.
I misattributed this twice before getting it right, so here is the whole chain with what is actually established.
The trigger. The interactive TUI never sets
screenMode, so OpenTUI resolves it to its defaultalternate-screen. Its shutdown then leaves the alternate screen. That exit is what takes the host terminal down.The knob already exists. OpenTUI registers and documents it:
Setting it to
0forcesmain-screenand the failure disappears. Verified ondev(0.5.17) and on the packaged@opencode/cli2.0.30, 3 of 3 runs surviving, epilogue visible, across/exitand Ctrl+C.It is host-specific, not ConPTY-wide. Same machine, same build:
Windows Terminal is also ConPTY and is unaffected, which is what rules out ConPTY as the cause. zellij is not required.
Two attempts of mine that did nothing, recorded so nobody re-derives them:
clearOnShutdown: false. That option only gates the non-alternate-screen branch of the teardown, so with the alternate screen active it is never consulted. 0 of 6 runs improved.screenMode: "main-screen"on Windows. Correct behaviour, but the knob already existed upstream, so it was redundant.Both closed.
Where this belongs. Reported at alacritty/alacritty#9077 with the matrix above. The mechanism is still unknown: whether it is the
?1049lsequence, its ordering against process exit, or Alacritty's ConPTY handling during teardown.Leaving this open rather than closing it, since opencode users on Alacritty are still affected and the workaround carries a real cost, no fullscreen TUI and accumulating scrollback. That cost is a consequence of an upstream bug, not something opencode can fix on its own.
Tarquinen commentedon Nov 2, 2026
Thanks for tracking this down! Since this is specific to the terminal host and
OTUI_USE_ALTERNATE_SCREEN=0works around it, I'm closing this issue.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊