Skip to content

fix(tui): eliminate resume-path terminal corruption in OpenTUI sessions - #9

Merged
pentoshi007 merged 1 commit into
mainfrom
hoplite/pantikapaion-bosporos-fdef2ee2
Sep 5, 2026
Merged

fix(tui): eliminate resume-path terminal corruption in OpenTUI sessions#9
pentoshi007 merged 1 commit into
mainfrom
hoplite/pantikapaion-bosporos-fdef2ee2

Conversation

@usehoplite

@usehoplite usehoplite Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What

Fixes the intermittent terminal corruption when resuming sessions via clai --resume or /history in 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)

  1. 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. TerminalAttachOutput now 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.

  2. 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-shot forceFullRepaintRequested latch 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.

  3. Client raw-mode gaps — the durable client restored cooked/echo input mode before writing the terminal reset, and dropped raw mode entirely between /history 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 (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.
  • New native renderer regression (real OpenTUI 0.4.5, buffered output) proves a forced repaint rewrites every cell — including blank cells — without suspend/resume, raw-mode drops, or alt-screen churn.
  • New PTY continuity test (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 with cellsUpdated: 0 even 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.
  • Full suite: 6099 passed; the 17 failures are pre-existing root/sandbox environment issues in unrelated privilege/OS tests (e.g. tests asserting /etc writes fail or sudo prompts — both behave differently as root), none touch the changed code.

Sandbox setup

.hoplite/settings.json now records the effective setup (npm ci + project-local Bun 1.4.2). The system Bun 1.3.1 predates Bun.Terminal, which the session-runtime PTY integration tests require.

Open in Hoplite

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
pentoshi007 marked this pull request as ready for review September 5, 2026 10:31
@pentoshi007
pentoshi007 merged commit 2aeab7d into main Sep 5, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant