fix(codex): strip alt-screen + scrollback-erase from the codex byte stream - #116
Conversation
…tream Codex's TUI emits alternate-screen toggles (DECSET/DECRST 47/1047/1049), scrollback-erase (CSI 3 J), and mouse-tracking enables (?1000-1007) during startup and on every repaint. xterm.js obeys them: it switches to the scrollback-less alternate buffer, wipes saved lines, and forwards the scroll wheel to codex — so the user's conversation history both disappears and becomes unreachable on each tab switch / pane refresh. Strip these sequences in two places, leaving the visible-viewport erases (2J / J) intact so codex can still repaint its own rows: - Session._handleTerminalOutput: filter the live SSE/WS stream and the persisted terminal buffer at the source, for mode === 'codex'. - GET /api/sessions/:id/terminal: apply the same strip to the replayed buffer (ALT_SCREEN_TOGGLE_PATTERN / ERASE_SCROLLBACK_PATTERN) so a tab-switch replay keeps full scrollback. Adds test/codex-terminal-output.test.ts covering the strip (alt-screen and 3J removed, 2J/J preserved, Ctrl+L redraws preserved) and confirming codex output passes through without Ink row-repair mangling. Co-Authored-By: Saqeb Akhter <saqeb.akhter@gmail.com>
…parity on replay Review fixes: - Hold back a trailing partial CSI (digit-only intro, ≤7 chars) in _handleTerminalOutput and prepend it to the next chunk. PTY chunk boundaries are arbitrary, so '\x1b[?1049h' can arrive as '\x1b[?104' + '9h' — the per-chunk strip misses it, xterm obeys the reassembled toggle, and (with the matching ?1049l stripped) stays stuck in the scrollback-less alt buffer until the next replay. Complete sequences are never held; the carry resets with the other buffers in _resetBuffers. - Replay path now also strips mouse-tracking enables (?1000-?1007), matching the live strip: buffers persisted BEFORE the live strip existed can still carry them, and a replayed ?1006h re-hijacks the scroll wheel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ark0N
left a comment
There was a problem hiding this comment.
Reviewed. The approach is right — strip at the source so neither the persisted buffer nor the live stream carries the scrollback-killing sequences, with the replay strip as backstop — and the 13 captured-stream tests are genuinely good coverage. Two gaps, both fixed in 7eda39e pushed to this branch:
1. Sequences split across PTY chunk boundaries slipped past the live strip._handleTerminalOutput filters per chunk, but chunk boundaries are arbitrary: \x1b[?1049h can arrive as \x1b[?104 + 9h. xterm's parser is streaming, so it reassembles and obeys the leaked toggle — and since the matching ?1049l (arriving intact) does get stripped, the terminal is left stuck in the scrollback-less alt buffer until the next tab-switch replay: exactly the bug this PR fixes, recurring intermittently. At ~8 bytes per sequence against ~4KB chunk boundaries the per-occurrence odds are small, but codex re-emits these on every repaint, so over a long session a leak is near-certain. Fixed with a small carry: a trailing partial digit-only CSI intro (≤7 chars) is held back and prepended to the next chunk; complete sequences are never held, and the carry resets in _resetBuffers. Two tests added.
2. Replay didn't strip mouse-tracking enables. The live strip removes ?1000–?1007, but the GET /terminal replay only stripped alt-screen + 3J — so a codex buffer persisted before this PR still re-hijacks the scroll wheel on every tab switch. Added MOUSE_TRACKING_PATTERN to the replay strip for parity.
Verified in a clean worktree: tsc, eslint, prettier --check clean; full test:ci green (2737 passed, +2 new tests).
Uh oh!
There was an error while loading. Please reload this page.
Problem
When running a Codex session, the conversation history disappears (and becomes unscrollable) on every tab switch / pane refresh.
Codex's TUI emits, during startup and on each repaint:
47/1047/1049CSI 3 J?1000–1007xterm.js faithfully obeys these: it switches to the alternate buffer (which has no scrollback), wipes the saved lines, and forwards the scroll wheel to the codex process. The net effect is that the user's prior conversation/shell output both vanishes and — even when still present in the buffer — can't be scrolled back to.
Fix
Strip just those sequences from the codex byte stream in two places, leaving the visible-viewport erases (
2J/J) intact so codex can still repaint its own rows:Session._handleTerminalOutput— filters the live SSE/WS stream and the persisted terminal buffer at the source, formode === 'codex'.GET /api/sessions/:id/terminal— applies the same strip (ALT_SCREEN_TOGGLE_PATTERN/ERASE_SCROLLBACK_PATTERN) to the replayed buffer, so a tab-switch replay keeps full scrollback. Mirrors the live-stream strip.Only
mode === 'codex'is affected; Claude/OpenCode/shell paths are untouched.Tests
Adds
test/codex-terminal-output.test.ts:3Jare removed;2J/J(visible-viewport erases) are preserved?1049h … 2J … 3J … ?1049lredraw collapses to just the visible contentnpm run test:cipasses locally (2735 passed, 0 failures);tsc --noEmit,eslint,prettier --check, and the frontend-syntax check are all clean.