Skip to content

V2: TUI leaves terminal in raw mode after /exit on Windows ConPTY (Alacritty + zellij) #55163

Closed as not planned

Description

@kraso

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:

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:

  1. Should this live in the TUI (as above), or is it better fixed in
    @opentui/core's teardown?
  2. lib is private on CliRenderer, 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

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    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 (e.g. v2.0.28 and dev) uses @opentui/core 0.6.2, not 0.1.97.
    • The v2 TUI lives in packages/tui/src/.... packages/tui/src/context/exit.tsx doesn't call renderer.destroy(). Teardown goes through destroyRenderer() in packages/tui/src/util/renderer.ts.
    • We can't find commit 7a6ce05d09 in the repository.

    The paths and versions you cited match v1, so the build you tested may have been 1.x. Could you please:

    1. Install an official 2.x release (npm i -g @opencode/cli, or opencode upgrade) and paste the output of opencode --version.
    2. In your own words, briefly: run opencode in 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)?
    3. Does it also happen without zellij, in Alacritty alone or in Windows Terminal? That tells us whether it's specific to ConPTY plus a multiplexer.

    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:

  2. kraso commented on Oct 13, 2026

    @kraso
    Author

    You were right on all three points, and the mistake was mine.

    What I got wrong. I based the report on the 2.0 branch, not on dev. 2.0 still exists — 7a6ce05d0939826aa6c8e1c481489a713b2d633f is its head — but it is an abandoned pre-restructure layout: the TUI lived under packages/opencode/src/cli/cmd/tui/ and pinned @opentui/core 0.1.97. Current dev is e9def88a77, with the TUI back under packages/tui/ and a newer opentui. So the paths and version I cited describe 2.0, not current v2. The commit does exist in the repository, just not reachable from dev.

    I also checked npm for opencode-ai when the v2 package is @opencode/cli. That is why I wrongly concluded v2 was unreleased.

    1. Official build

    opencode v2.0.28
    

    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.0 branch 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 dev it looks still live: packages/tui/src/util/renderer.ts has a destroyRenderer() with no terminal reset and no platform guard, and packages/tui/src/app.tsx registers only SIGHUP, with no SIGTERM and no process.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 against dev.

  3. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for tracking that down. It helps a lot to know the earlier details came from the old 2.0 branch.

    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.0 branch (2.0.11, @opentui/core 0.1.97). 2.0.28 ships @opentui/core 0.6.2, and OpenTUI's teardown has changed a lot between those versions. So the missing reset you spotted in packages/tui/src/util/renderer.ts may or may not still cause the problem.

    Could you run the unpatched official 2.0.28 in Alacritty + zellij-windows and tell us:

    1. Over a handful of opencode → /exit cycles, 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".
    2. If you get the chance, the same check in Alacritty alone or Windows Terminal without zellij.
    3. Does closing with Ctrl+C behave any differently from /exit?

    If it still happens on 2.0.28, a PR against dev is welcome. Linking it here with the repro results will make it easier to review.

    Related issues, ordered by likelihood:

  4. kraso commented on Oct 13, 2026

    @kraso
    Author

    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

    Setup Cycles Failures What happened
    Alacritty + zellij-windows 3 3 the zellij pane closed
    Alacritty alone, no zellij 3 2 Alacritty closed entirely

    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 dev still has no terminal restore: packages/tui/src/util/renderer.ts destroyRenderer() is just an isDestroyed guard, a title reset and renderer.destroy(), and packages/tui/src/app.tsx registers only SIGHUP — no SIGTERM, no process.on("exit") restore.

    On the fix I was going to propose

    The approach from #48878 patched renderer.lib.destroyRenderer to suppress OpenTUI's native teardown. That specific method no longer exists in @opentui/core 0.6.2 — the lib surface is now destroySession / 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.

  5. kraso commented on Oct 13, 2026

    @kraso
    Author

    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

    Closing method Setup Cycles Hard close Clean exit, epilogue visible
    /exit Alacritty + zellij 3 3 0
    /exit Alacritty alone 3 2 1
    Ctrl+C Alacritty + zellij 3 2 1
    Ctrl+C Alacritty alone 3 3 0

    Ctrl+C and /exit behave 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() schedules nativeSession.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.

  6. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    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:

    • It happens on the unpatched official opencode v2.0.28, on Windows 11 with Alacritty 0.17.0.
    • It fails both inside zellij-windows (the pane closes) and in Alacritty alone (the whole window closes).
    • It happened in 10 of 12 cycles, with /exit and 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).

  7. kraso commented on Oct 15, 2026

    @kraso
    Author

    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: false on 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:

    let screenMode = config.screenMode ?? "alternate-screen";

    So the interactive TUI runs in alternate-screen mode and its shutdown takes the first branch of the teardown, where resetState exits the alternate screen, rather than the branch clearOnShutdown gates:

    if (self.useAlternateScreen) {
        // Alt screen: resetState already exited alt screen; just flush.
    } else if (self.clearOnShutdown and self.renderOffset == 0) {
        writer.writeAll("\x1b[H\x1b[J") catch {};
    }

    That also explains why only this renderer is affected. packages/cli/src/ui/timeline.tsx and packages/tui/src/mini/runtime.lifecycle.ts both set screenMode: "split-footer", so they never touch the alternate screen at all.

    Experiment. One variable changed, same build, same host.

    screenMode outcome result
    alternate-screen (default) 0 of 6 survived host terminal window or zellij pane exited with the process, on both /exit and Ctrl+C
    main-screen 6 of 6 survived terminal survived, epilogue visible every time

    Six runs either way: 3 in Alacritty + zellij and 3 in Alacritty alone, split across /exit and Ctrl+C. This is against dev (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 screenMode is the deciding variable, cleanly, 6/6 versus 0/6. I have not established the exact mechanism: whether the trigger is specifically the ?1049l sequence, 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 clearOnShutdown mechanism and it is a no-op, so I closed it.

  8. kraso commented on Oct 15, 2026

    @kraso
    Author

    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 default alternate-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:

    OTUI_USE_ALTERNATE_SCREEN   boolean   unset   Override screenMode
    

    Setting it to 0 forces main-screen and the failure disappears. Verified on dev (0.5.17) and on the packaged @opencode/cli 2.0.30, 3 of 3 runs surviving, epilogue visible, across /exit and Ctrl+C.

    It is host-specific, not ConPTY-wide. Same machine, same build:

    Host Result
    Alacritty 0.17.0 (94e7c88), alone exits with the process
    Alacritty 0.17.0 + zellij exits with the process
    Windows Terminal (PowerShell 7.6.6) survives, 3 of 3

    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:

    Both closed.

    Where this belongs. Reported at alacritty/alacritty#9077 with the matrix above. The mechanism is still unknown: whether it is the ?1049l sequence, 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.

  9. Tarquinen commented on Nov 2, 2026

    @Tarquinen
    Member

    Thanks for tracking this down! Since this is specific to the terminal host and OTUI_USE_ALTERNATE_SCREEN=0 works around it, I'm closing this issue.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions