fix(tui): eliminate resume-path terminal corruption in OpenTUI sessions - #9
Merged
Merged
Conversation
Three compounding races corrupted the screen when attaching or resuming a session (clai --resume, /history switch): 1. The runtime host snapshotted the replay buffer before requesting a child repaint and wrote that stale snapshot after the (possibly slow) reply, overwriting newer live frames. TerminalAttachOutput now gates attachment output: live bytes are buffered in order while the repaint request is in flight, the stale snapshot is only written when the repaint is declined, and the buffer is bounded and disposed with its socket so late replies can never write to a dead terminal. 2. Attach repaints suspended the OpenTUI renderer, which leaves the alternate screen, removes the stdin handler, and drops kernel raw mode - exactly while mouse reporting is still enabled, so terminal mouse reports echoed as visible garbage. Repaints now stay inside the renderer: a shape-guarded adapter sets the native one-shot forceFullRepaintRequested latch and lets the existing scheduler emit a complete frame without surrendering terminal ownership. The bridge repaint handler is registered immediately after the loading screen renders, before any slow resume work, so attaches during the load window get an immediate authoritative frame. 3. The durable client restored cooked/echo input mode before writing the terminal reset, and briefly dropped raw mode between session switches. Raw mode now spans the entire runtime-client lifetime, a discard pump drains input during connection transitions, and the reset sequence is flushed (with bounded error/timeout handling) before the previous raw state is restored. Regression coverage: ordered/late/oversized/disconnected attach-output unit tests, real-PTY host integration for delayed repaint replies, client handoff tests for reset ordering across detach/exit/switch/ signal/auth-failure paths, native renderer tests proving full-frame repaint (including blank cells) without suspend, and a PTY continuity test (with a negative control against the old suspend approach) that runs where the native tty writer is available and skips in sandboxes where it is inert. Also records the sandbox setup command (npm ci + project-local Bun 1.4.2, required for Bun.Terminal in session-runtime integration tests). Co-authored-by: Aniket Pandey <aniket00736@gmail.com>
pentoshi007
marked this pull request as ready for review
September 5, 2026 10:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes the intermittent terminal corruption when resuming sessions via
clai --resumeor/historyin the OpenTUI UI: blank screen during the session-load window, mouse-event escape sequences printing as garbage, and a broken layout after load (disappearing elements, UI text mixed with transcripts, broken composer).Root causes (three compounding races)
Stale replay overwrote live frames — the runtime host snapshotted the replay buffer before requesting a child repaint and wrote that old snapshot after the reply arrived (up to 5s later), clobbering newer output the child had produced in between.
TerminalAttachOutputnow gates attachment output: live bytes buffer in order while a repaint request is in flight, the stale snapshot is written only when the repaint is declined, and the buffer is bounded (1 MiB) and disposed with its socket so late replies can never write into a dead terminal.Repaints suspended the renderer mid-attach — the old coordinated flush called OpenTUI
suspend(), which leaves the alternate screen, removes the stdin handler, and drops kernel raw mode — exactly while mouse reporting is still enabled, so terminal mouse reports echoed as visible garbage on the blank screen. Repaints now stay inside the renderer: a shape-guarded adapter sets the native one-shotforceFullRepaintRequestedlatch and lets the existing scheduler emit a complete frame, never surrendering terminal ownership. The bridge repaint handler is registered immediately after the loading screen renders — before any slow resume work — so attaches during the load window get an immediate authoritative frame instead of a timeout + stale replay.Client raw-mode gaps — the durable client restored cooked/echo input mode before writing the terminal reset, and dropped raw mode entirely between
/historysession switches. Raw mode now spans the entire runtime-client lifetime, a discard pump drains input during connection transitions, and the reset sequence is flushed (bounded error/timeout) before restoring the previous raw state.Verification
npm run typecheck— clean.test/session-runtime+test/tui-v2+test/os— 401 tests pass, including new real-PTY integration coverage for delayed-repaint ordering and client handoff reset ordering across detach/exit/switch/signal/auth-failure paths.resize-repaint.pty.py) samples kernel raw mode across 12 repaints with continuous mouse input and includes a negative control proving the detector rejects the old suspend/resume approach. It runs where the native tty writer is available; in sandboxes where OpenTUI's native tty output is inert (verified: frames render withcellsUpdated: 0even on unmodified code), it skips with an explicit marker. Pixel-level visual verification was not performed; correctness is established through byte-stream, termios, and native-buffer evidence./etcwrites fail or sudo prompts — both behave differently as root), none touch the changed code.Sandbox setup
.hoplite/settings.jsonnow records the effective setup (npm ci+ project-local Bun 1.4.2). The system Bun 1.3.1 predatesBun.Terminal, which the session-runtime PTY integration tests require.