mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
fix(codex): strip alt-screen + scrollback-erase from the codex byte stream
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>
This commit is contained in:
co-authored by
Saqeb Akhter
parent
055f18fb66
commit
d7606366a2
@@ -1052,6 +1052,32 @@ export class Session extends EventEmitter {
|
||||
}
|
||||
|
||||
private _handleTerminalOutput(data: string): void {
|
||||
// Codex emits sequences that wipe xterm.js scrollback, plus mouse-tracking
|
||||
// enables that hijack the scroll wheel so the user can't reach scrollback:
|
||||
// - \x1b[?1049h / \x1b[?47h / \x1b[?1047h: switch to the alt buffer (no
|
||||
// scrollback) — \x1b[?...l switches back.
|
||||
// - \x1b[3J: erase saved lines (scrollback). (\x1b[2J / \x1b[J — erase
|
||||
// the visible viewport — are left intact; the TUI repaints those rows.)
|
||||
// - \x1b[?1000h / 1002h / 1003h / 1005h / 1006h / 1007h: mouse-tracking
|
||||
// modes (X10, button-event, any-event, UTF-8, SGR, alt-scroll). Once on,
|
||||
// xterm.js forwards wheel events to codex instead of scrolling the
|
||||
// viewport, so the conversation is in scrollback but unreachable.
|
||||
// (Focus events at ?1004 are left alone — codeman uses them for
|
||||
// active-tab detection.)
|
||||
// Strip them at the source so neither the persisted buffer nor the live
|
||||
// SSE/WS stream carries them, keeping everything in the main buffer with
|
||||
// scrollback intact. Codex's cursor-positioned redraws overwrite only the
|
||||
// cells they actually target, so the non-erased rows keep their content.
|
||||
if (this.mode === 'codex') {
|
||||
data = data
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/\x1b\[\?(?:47|1047|1049)[hl]/g, '')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/\x1b\[3J/g, '')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[hl]/g, '');
|
||||
}
|
||||
|
||||
// BufferAccumulator handles auto-trimming when max size exceeded
|
||||
this._terminalBuffer.append(data);
|
||||
this._lastActivityAt = Date.now();
|
||||
|
||||
@@ -65,6 +65,25 @@ const CLAUDE_BANNER_PATTERN = /\x1b\[1mClaud/;
|
||||
const CTRL_L_PATTERN = /\x0c/g;
|
||||
const LEADING_WHITESPACE_PATTERN = /^[\s\r\n]+/;
|
||||
|
||||
/**
|
||||
* Match xterm alternate-screen mode toggles + the standalone scrollback-erase.
|
||||
*
|
||||
* - DECSET/DECRST 47, 1047, 1049 = enter/exit alternate screen buffer
|
||||
* (1049 also saves cursor and clears the alt buffer).
|
||||
* - CSI 3 J = erase saved lines (scrollback).
|
||||
*
|
||||
* Codex emits `\x1b[?1049h` and clear-scrollback sequences during startup and
|
||||
* on repaint. xterm.js obeys them by switching to the alt buffer (no native
|
||||
* scrollback) and wiping saved lines, so the user's conversation history
|
||||
* disappears on every tab switch / pane refresh. Stripping these from the
|
||||
* replayed byte stream keeps everything in the main buffer with scrollback
|
||||
* intact. Mirrors the live-stream strip in Session._handleTerminalOutput.
|
||||
*/
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const ALT_SCREEN_TOGGLE_PATTERN = /\x1b\[\?(?:47|1047|1049)[hl]/g;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const ERASE_SCROLLBACK_PATTERN = /\x1b\[3J/g;
|
||||
|
||||
/**
|
||||
* Strip redundant Ink spinner/status-bar redraw frames from the terminal buffer.
|
||||
* Ink (Claude Code's TUI) uses absolute cursor positioning (CSI n d = VPA) to animate
|
||||
@@ -916,7 +935,14 @@ export function registerSessionRoutes(
|
||||
// During long thinking phases, Ink rewrites the same rows thousands of times
|
||||
// (500KB+). Without stripping, tail mode returns only spinner frames and
|
||||
// the terminal appears empty when switching tabs.
|
||||
const strippedBuffer = stripInkRedrawBloat(rawBuffer);
|
||||
let strippedBuffer = stripInkRedrawBloat(rawBuffer);
|
||||
|
||||
// Strip alt-screen toggles and scrollback-erase from codex byte streams.
|
||||
// xterm.js obeys them by switching to its scrollback-less alt buffer and
|
||||
// wiping saved lines, so conversation history disappears on tab switch.
|
||||
if (session.mode === 'codex') {
|
||||
strippedBuffer = strippedBuffer.replace(ALT_SCREEN_TOGGLE_PATTERN, '').replace(ERASE_SCROLLBACK_PATTERN, '');
|
||||
}
|
||||
|
||||
if (tailBytes > 0 && strippedBuffer.length > tailBytes) {
|
||||
// Fast path: tail from the end, skip expensive banner search on full 2MB buffer.
|
||||
|
||||
Reference in New Issue
Block a user