mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-14 11:19:42 +02:00
Merge #555: opencode drags select text and the wheel pages its transcript
This commit is contained in:
@@ -289,7 +289,7 @@ const capabilitiesSchema = z
|
||||
requiresMux: z.boolean(),
|
||||
hooks: z.enum(['none', 'always', 'supervised']),
|
||||
transcript: z.enum(['claude-jsonl', 'codex-rollout', 'deepseek-zstd', 'omp-jsonl', 'none']),
|
||||
altScreen: z.enum(['strip-full', 'strip-mux-only', 'preserve']),
|
||||
altScreen: z.enum(['strip-full', 'strip-mux-only', 'strip-mux-and-mouse', 'preserve']),
|
||||
echo: echoSchema,
|
||||
wheelForward: z
|
||||
.object({ mode: z.enum(['never', 'version-gated']), minVersion: z.string().max(20).optional() })
|
||||
|
||||
@@ -491,7 +491,7 @@ const OPENCODE: CliEntry = {
|
||||
},
|
||||
capabilities: {
|
||||
...agentDefaults(),
|
||||
altScreen: 'strip-mux-only',
|
||||
altScreen: 'strip-mux-and-mouse',
|
||||
echo: { policy: 'buffer', anchor: { kind: 'cursor' }, predictProfile: undefined },
|
||||
// Measured on a live opencode 1.3.0 pane (capture-pane every 250-300 ms through real
|
||||
// turns at 40, 60, 120 and 200 columns, plus the raw PTY stream, 2026-10-09). Every
|
||||
|
||||
@@ -439,11 +439,40 @@ export interface CliCapabilities {
|
||||
*/
|
||||
transcript: 'claude-jsonl' | 'codex-rollout' | 'deepseek-zstd' | 'omp-jsonl' | 'none';
|
||||
/**
|
||||
* 'strip-full' — alt-screen + erase-scrollback + mouse DECSETs stripped (Ink TUIs).
|
||||
* 'strip-mux-only' — only tmux's own attach-time smcup (the safe default).
|
||||
* 'preserve' — leave everything (a direct-PTY shell running vim/less/htop).
|
||||
* What the server strips from this CLI's output stream before the browser sees it.
|
||||
* The value encodes three independent choices (predicates in session.ts):
|
||||
*
|
||||
* | value | alt-screen toggles | `3J` (erase scrollback) | mouse DECSETs |
|
||||
* |-----------------------|---------------------|-------------------------|---------------------|
|
||||
* | `strip-full` | stripped | stripped | stripped |
|
||||
* | `strip-mux-and-mouse` | stripped under tmux | kept | stripped under tmux |
|
||||
* | `strip-mux-only` | stripped under tmux | kept | kept |
|
||||
* | `preserve` | stripped under tmux | kept | kept |
|
||||
*
|
||||
* `strip-full` is `isAltScreenStripMode`; `strip-mux-and-mouse` is `isMuxMouseStripMode`;
|
||||
* every other value takes `isMuxAltScreenOnlyStripMode`, so at runtime `preserve` and
|
||||
* `strip-mux-only` are the same row — `preserve` only says what such a CLI's pane
|
||||
* holds (terminal-owned scrollback: a shell, pi), not a different strip.
|
||||
*
|
||||
* - alt-screen: the tmux CLIENT emits `smcup` as its first bytes at attach, parking
|
||||
* xterm in the scrollback-less alternate buffer; a pane program's own toggles never
|
||||
* reach the client (tmux repaints instead). "Under tmux" means `useMux`: on a
|
||||
* direct-PTY fallback the `?1049h` is the program's own and must stay.
|
||||
* - `3J`: a user's `clear` is a deliberate scrollback wipe; only an Ink TUI's
|
||||
* redraw-driven `3J` (strip-full) is noise.
|
||||
* - mouse DECSETs: stripping them keeps a drag a local selection instead of a report
|
||||
* to the TUI. The browser then hand-encodes clicks (`_sendSyntheticSgrTap`), gated
|
||||
* on the `cliMouseTracking` the server records as it strips. Kept where a program's
|
||||
* own mouse support must work in the pane (htop/vim in a shell).
|
||||
*
|
||||
* Stock CLIs: `strip-full` = claude, codex, gemini (Ink TUIs); `strip-mux-and-mouse` =
|
||||
* opencode (a full-screen TUI that enables tracking itself); `strip-mux-only` =
|
||||
* antigravity, grok, deepseek, omp; `preserve` = shell, pi.
|
||||
*
|
||||
* A fourth combination is the point to split this into flags; three is still cheaper
|
||||
* as an enum.
|
||||
*/
|
||||
altScreen: 'strip-full' | 'strip-mux-only' | 'preserve';
|
||||
altScreen: 'strip-full' | 'strip-mux-only' | 'strip-mux-and-mouse' | 'preserve';
|
||||
echo: {
|
||||
policy: 'buffer' | 'predict' | 'off';
|
||||
/** How the local-echo overlay locates the composer row. */
|
||||
|
||||
+54
-16
@@ -277,10 +277,11 @@ function cliExportsTruecolor(mode: SessionMode): boolean {
|
||||
* Codex, Claude Code, and Gemini are known, controlled (Ink/React) TUIs that
|
||||
* repaint via cursor positioning, so dropping the alt-screen switch is safe —
|
||||
* content stays in the normal buffer. Excluded: `shell` (arbitrary programs like
|
||||
* vim/less/htop legitimately need the alt screen), `opencode` (renders its own
|
||||
* TUI that may rely on it), `pi` (below) and `grok` (a fullscreen alt-screen TUI
|
||||
* with mouse support, i.e. the opencode case, not the Ink case). Keep parity
|
||||
* with the replay-side strip in session-routes.ts.
|
||||
* vim/less/htop legitimately need the alt screen), `opencode` (its own MIDDLE strip,
|
||||
* isMuxMouseStripMode), `pi` (below) and `grok` (a fullscreen alt-screen TUI with
|
||||
* mouse support). Keep parity with the replay-side strip (`stripReplayBuffer` in
|
||||
* session-routes.ts); the table in `CliCapabilities.altScreen` is pinned for every
|
||||
* stock CLI in test/claude-scrollback-strip.test.ts.
|
||||
*
|
||||
* ⚠️ Being excluded here does NOT preserve the alt screen. Every excluded mode
|
||||
* falls through to isMuxAltScreenOnlyStripMode(), which strips the alt-screen
|
||||
@@ -300,8 +301,10 @@ export function isAltScreenStripMode(mode: SessionMode): boolean {
|
||||
|
||||
/**
|
||||
* Modes that need the NARROW strip: alt-screen toggles only, leaving `\x1b[3J`
|
||||
* and the mouse-tracking DECSETs alone. Applies to every mode `isAltScreenStripMode`
|
||||
* excludes, but ONLY when the session is tmux-backed (`useMux`).
|
||||
* and the mouse-tracking DECSETs alone. Applies to every mode that is neither
|
||||
* `strip-full` (isAltScreenStripMode) nor `strip-mux-and-mouse` (isMuxMouseStripMode),
|
||||
* so `strip-mux-only` and `preserve` alike, but ONLY when the session is tmux-backed
|
||||
* (`useMux`).
|
||||
*
|
||||
* The bug (issue #205): the tmux CLIENT emits `smcup` (`\x1b[?1049h`) as its first
|
||||
* bytes on attach, before any program has run. Unstripped, xterm.js parks in the
|
||||
@@ -326,7 +329,31 @@ export function isAltScreenStripMode(mode: SessionMode): boolean {
|
||||
* `\x1b[3J` from a user's own `clear` is a deliberate "wipe my scrollback".
|
||||
*/
|
||||
export function isMuxAltScreenOnlyStripMode(mode: SessionMode, useMux: boolean): boolean {
|
||||
return useMux && !isAltScreenStripMode(mode);
|
||||
if (!useMux) return false;
|
||||
const altScreen = getCli(mode)?.capabilities.altScreen;
|
||||
return altScreen !== 'strip-full' && altScreen !== 'strip-mux-and-mouse';
|
||||
}
|
||||
|
||||
/**
|
||||
* Modes whose mouse-tracking DECSETs must be stripped, leaving `3J` alone:
|
||||
* `altScreen: 'strip-mux-and-mouse'`, i.e. a mouse-capable full-screen TUI.
|
||||
*
|
||||
* Why this exists (opencode, measured 2026-09-16): the TUI enables tracking
|
||||
* DECSETs, tmux runs with `mouse off` and therefore passes the PANE's DECSETs
|
||||
* straight through to the tmux client, and the browser's xterm obeyed them —
|
||||
* `mouseTrackingMode` flipped to `'any'` and xterm then reported DRAGS to the TUI
|
||||
* instead of selecting locally. "Mark text, copy on select" silently did nothing
|
||||
* (measured 62 `none` / 18 `any` over 16s, and 5/5 dead drags while `any`), and
|
||||
* the obvious fallback — Ctrl+C — is opencode's `app_exit`, so the failure also
|
||||
* ended sessions. Stripping at the source keeps xterm in selection mode; clicks
|
||||
* still reach the CLI through the browser's hand-encoded tap, which this strip
|
||||
* publishes as `cliMouseTracking` (`_recordStrippedMouseMode`).
|
||||
*
|
||||
* Gated on `useMux` for the same reason as the narrow strip: on the direct-PTY
|
||||
* fallback the program's own DECSETs really do reach xterm and must be honoured.
|
||||
*/
|
||||
export function isMuxMouseStripMode(mode: SessionMode, useMux: boolean): boolean {
|
||||
return useMux && getCli(mode)?.capabilities.altScreen === 'strip-mux-and-mouse';
|
||||
}
|
||||
|
||||
// Note: Claude CLI PATH resolution moved to session-cli-builder.ts (buildClaudeEnv)
|
||||
@@ -2529,14 +2556,21 @@ export class Session extends EventEmitter {
|
||||
// redraws overwrite only the cells they target, so non-erased rows keep
|
||||
// their content. Gated to Codex/Claude/Gemini (isAltScreenStripMode).
|
||||
//
|
||||
// Every OTHER mode (shell/opencode/antigravity) gets the NARROW strip when it
|
||||
// is tmux-backed: alt-screen toggles only, because the sequence that breaks
|
||||
// Every OTHER mode (shell/antigravity/pi/grok/deepseek/omp) gets the NARROW strip
|
||||
// when it is tmux-backed: alt-screen toggles only, because the sequence that breaks
|
||||
// scrollback there is tmux's own client-side smcup at attach, not anything the
|
||||
// program in the pane emitted (issue #205, see isMuxAltScreenOnlyStripMode).
|
||||
// 3J and the mouse DECSETs stay, so `clear` and mouse-aware TUIs keep working.
|
||||
//
|
||||
// The MIDDLE case (isMuxMouseStripMode) is a mouse-capable full-screen TUI:
|
||||
// it needs smcup AND the mouse DECSETs gone — otherwise the pane's tracking
|
||||
// reaches xterm and every drag becomes a mouse report instead of a text
|
||||
// selection, which is what killed mark-and-copy in opencode — while 3J stays,
|
||||
// because a TUI is not a `clear` consumer.
|
||||
const fullStrip = isAltScreenStripMode(this.mode);
|
||||
const altOnlyStrip = !fullStrip && isMuxAltScreenOnlyStripMode(this.mode, this._useMux);
|
||||
if (fullStrip || altOnlyStrip) {
|
||||
const mouseStrip = isMuxMouseStripMode(this.mode, this._useMux);
|
||||
const altOnlyStrip = !fullStrip && !mouseStrip && isMuxAltScreenOnlyStripMode(this.mode, this._useMux);
|
||||
if (fullStrip || mouseStrip || altOnlyStrip) {
|
||||
// Reassemble sequences split across PTY chunk boundaries first: a chunk
|
||||
// ending mid-sequence ('\x1b[?104' now, '9h' next) would slip past the
|
||||
// strip below and leave xterm stuck in the scrollback-less alt buffer
|
||||
@@ -2555,14 +2589,18 @@ export class Session extends EventEmitter {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
data = data.replace(/\x1b\[\?(?:47|1047|1049)[hl]/g, '');
|
||||
if (fullStrip) {
|
||||
data = data
|
||||
// eslint-disable-next-line no-control-regex
|
||||
data = data.replace(/\x1b\[3J/g, '');
|
||||
}
|
||||
if (fullStrip || mouseStrip) {
|
||||
data = data.replace(
|
||||
// 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, (seq) => {
|
||||
/\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[hl]/g,
|
||||
(seq) => {
|
||||
this._recordStrippedMouseMode(seq);
|
||||
return '';
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1273,7 +1273,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
// A real mouse click normally reaches the PTY through xterm's own mouse
|
||||
// encoder, but that encoder only runs while mouseTrackingMode is ON — and
|
||||
// the server strips the enabling DECSETs from claude/codex/gemini output
|
||||
// (isAltScreenStripMode, session.ts) so the wheel keeps scrolling
|
||||
// (isAltScreenStripMode, session.ts) and from opencode's (isMuxMouseStripMode,
|
||||
// so a drag selects text) so the wheel keeps scrolling
|
||||
// scrollback. Desktop clicks therefore stopped reporting entirely (the
|
||||
// same breakage the mobile touchend tap branch above works around).
|
||||
// Hand-encode the SGR report for plain left-clicks on those sessions.
|
||||
@@ -5369,36 +5370,32 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
// Mirror of the server's isAltScreenStripMode (session.ts): session modes whose
|
||||
// output stream has mouse-tracking DECSET sequences stripped before reaching the
|
||||
// browser. For these, xterm's live mouseTrackingMode is useless as a gate — the
|
||||
// PTY-side TUI keeps tracking enabled, we just never see the enable sequence.
|
||||
/**
|
||||
* True when the browser has to hand-encode a click report for the CLI.
|
||||
* True when the browser has to hand-encode a click report for the CLI: the
|
||||
* server stripped this session's mouse-tracking DECSETs out of the stream (so
|
||||
* xterm's own encoder is permanently idle here and something has to stand in
|
||||
* for it) AND the CLI has a tracking mode on right now.
|
||||
*
|
||||
* Two conditions, and dropping either one is a bug that has already happened:
|
||||
* One flag answers both. The server sets `cliMouseTracking` only as it strips a
|
||||
* tracking DECSET (`_recordStrippedMouseMode` in session.ts, called from the
|
||||
* mouse-strip branch of `_handleTerminalOutput` and nowhere else), so it can
|
||||
* only ever be true for a mode whose DECSETs are stripped: whichever modes the
|
||||
* registry decides to strip, the browser follows, with no mode list here to
|
||||
* keep in step. For a `preserve` / `strip-mux-only` mode the flag stays false
|
||||
* and xterm keeps encoding its own reports. That invariant is pinned server-side
|
||||
* in test/claude-scrollback-strip.test.ts.
|
||||
*
|
||||
* 1. The session's mode is one whose mouse DECSETs the server STRIPS out of
|
||||
* the stream (claude/codex/gemini, `isAltScreenStripMode`), which is why
|
||||
* xterm's own encoder is permanently idle here and something has to stand
|
||||
* in for it.
|
||||
* 2. The CLI actually has a mouse-tracking mode on right now. The server
|
||||
* records that as it strips (`_recordStrippedMouseMode` in session.ts) and
|
||||
* publishes it as `cliMouseTracking`. Without this half the browser
|
||||
* reported EVERY click, so a CLI sitting at its composer with no dialog
|
||||
* open, or a pane that has fallen back to a shell prompt, received mouse
|
||||
* reports it never asked for. A shell prints those as literal text
|
||||
* (`[<0;88;20M`) and they garble the next line typed.
|
||||
* Without the flag the browser reported EVERY click, so a CLI sitting at its
|
||||
* composer with no dialog open, or a pane that has fallen back to a shell
|
||||
* prompt, received mouse reports it never asked for. A shell prints those as
|
||||
* literal text (`[<0;88;20M`) and they garble the next line typed.
|
||||
*
|
||||
* Fails toward silence: an unknown or stale flag reports nothing rather than
|
||||
* injecting bytes. After a server restart the flag is false until the CLI
|
||||
* re-emits its DECSET, which closing and reopening a dialog does.
|
||||
*/
|
||||
_shouldReportMouseToCli() {
|
||||
const session = this.sessions?.get(this.activeSessionId);
|
||||
const mode = session?.mode || 'claude';
|
||||
if (mode !== 'claude' && mode !== 'codex' && mode !== 'gemini') return false;
|
||||
return session?.cliMouseTracking === true;
|
||||
return this.sessions?.get(this.activeSessionId)?.cliMouseTracking === true;
|
||||
},
|
||||
|
||||
// True when xterm's viewport shows the live PTY screen (not scrolled up into
|
||||
@@ -5583,15 +5580,29 @@ Object.assign(CodemanApp.prototype, {
|
||||
},
|
||||
|
||||
/**
|
||||
* True when this session's LOCAL scrollback is structurally empty: a Claude
|
||||
* pane in repaint mode, where tmux reports `history_size≈0` and every frame
|
||||
* overwrites the last, so xterm's normal buffer never grows past one screen
|
||||
* True when this session's LOCAL scrollback is structurally empty: a pane whose
|
||||
* TUI repaints one full screen in place, so tmux keeps no history for it
|
||||
* (`history_size≈0`) and xterm's normal buffer never grows past one screen
|
||||
* (`baseY === 0`). Scrolling that buffer is a no-op no matter how the gesture
|
||||
* is routed — the "wheel does nothing at all" half of the #205 retest.
|
||||
*
|
||||
* Two shapes, measured separately:
|
||||
* - `claude` in repaint mode (the original, #205 round 2), and
|
||||
* - `opencode`, whose TUI runs on the ALTERNATE SCREEN (opencode 1.18.31: tmux
|
||||
* `alternate_on=1`, `history_size=0`) and so pushes nothing into the
|
||||
* terminal's scrollback at all. It pages its own transcript with the same
|
||||
* PageUp/PageDown keys (`messages_page_up/down`) but IGNORES SGR wheel
|
||||
* reports — six `\x1b[<64;…M` reports against an idle pane left the capture
|
||||
* byte-identical — so paging is the only gesture that reaches it. Without
|
||||
* this the wheel was silently dead in every opencode tab.
|
||||
*
|
||||
* Every other mode is deliberately absent: shell/pi own real terminal
|
||||
* scrollback, and codex/gemini/antigravity/grok/deepseek/omp page-key behaviour
|
||||
* is unverified (docs/scrollback-fix-plan.md).
|
||||
*/
|
||||
_localScrollbackIsHollow() {
|
||||
const mode = this.sessions?.get(this.activeSessionId)?.mode || 'claude';
|
||||
if (mode !== 'claude') return false;
|
||||
if (mode !== 'claude' && mode !== 'opencode') return false;
|
||||
const buf = this.terminal?.buffer?.active;
|
||||
if (!buf || buf.type === 'alternate') return false;
|
||||
return (buf.baseY || 0) === 0;
|
||||
@@ -5602,18 +5613,19 @@ Object.assign(CodemanApp.prototype, {
|
||||
* coalesced PageUp/PageDown key sends so the CLI pages its OWN transcript.
|
||||
*
|
||||
* The rescue path for every way `_shouldForwardWheelToApp` can come back false
|
||||
* on a Claude session that has no local history to fall back on: the CLI
|
||||
* version probe failed or is genuinely older than 2.1.187, the CLI's mouse
|
||||
* tracking flag is unset (the inline renderer, or fullscreen right after a
|
||||
* server restart), or the user turned on "Wheel scrolls local history" (which
|
||||
* pins the wheel to a buffer that, for a repaint-mode CLI, is empty: the
|
||||
* setting's footgun). Before this, all of those produced a completely dead
|
||||
* gesture; the #205 reporter proved the keyboard route works by paging back
|
||||
* through intact text with Fn+Up.
|
||||
* on a session that has no local history to fall back on: the CLI version probe
|
||||
* failed or is genuinely older than 2.1.187, the CLI's mouse tracking flag is
|
||||
* unset (the inline renderer, or fullscreen right after a server restart), the
|
||||
* user turned on "Wheel scrolls local history" (which pins the wheel to a buffer
|
||||
* that, for a repaint-mode CLI, is empty: the setting's footgun), or the CLI is
|
||||
* opencode, which never fills the buffer and never accepts the wheel. Before
|
||||
* this, all of those produced a completely dead gesture; the #205 reporter
|
||||
* proved the keyboard route works by paging back through intact text with Fn+Up.
|
||||
*
|
||||
* Triple-guarded (claude mode + gate false + `baseY === 0`), so a session with
|
||||
* real local scrollback is never touched. Shift is excluded on purpose: it is
|
||||
* the explicit "give me local scrollback" gesture and must keep that meaning.
|
||||
* Guarded by `_localScrollbackIsHollow()` plus a false forwarding gate, so a
|
||||
* session with real local scrollback is never touched. Shift is excluded on
|
||||
* purpose: it is the explicit "give me local scrollback" gesture and must keep
|
||||
* that meaning.
|
||||
*
|
||||
* @returns true when the gesture was consumed here (the caller must not also
|
||||
* scroll locally).
|
||||
@@ -5714,7 +5726,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
* The reason is that the habit and xterm's Shift mean different things once
|
||||
* the DECSETs are stripped. xterm reads Shift as "force selection" ONLY while
|
||||
* the app actually has mouse tracking on; the server strips those DECSETs for
|
||||
* claude/codex/gemini (isAltScreenStripMode), so xterm's mouseTrackingMode is
|
||||
* claude/codex/gemini (isAltScreenStripMode) and opencode (isMuxMouseStripMode),
|
||||
* so xterm's mouseTrackingMode is
|
||||
* permanently `none`, that branch is unreachable, and Shift instead falls into
|
||||
* `_onIncrementalClick` — EXTEND an existing selection. Extending is a no-op
|
||||
* when `selectionStart` is null, so the drag never anchors and no selection is
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
isAltScreenStripMode,
|
||||
isExternalCliMode,
|
||||
isMuxAltScreenOnlyStripMode,
|
||||
isMuxMouseStripMode,
|
||||
} from '../../session.js';
|
||||
import type { PaneCaptureOptions } from '../../mux-interface.js';
|
||||
import { SseEvent } from '../sse-events.js';
|
||||
@@ -225,6 +226,37 @@ const ERASE_SCROLLBACK_PATTERN = /\x1b\[3J/g;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const MOUSE_TRACKING_PATTERN = /\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[hl]/g;
|
||||
|
||||
/**
|
||||
* The replay half of the strip parity triangle: what `_handleTerminalOutput` (session.ts)
|
||||
* removes from the live stream, removed again from a stored buffer before it is replayed,
|
||||
* because a buffer recorded before the live-side strip existed (or by an older server)
|
||||
* still carries the sequences, and one replayed enable is enough to re-park xterm.
|
||||
*
|
||||
* - `strip-full` (claude/codex/gemini): alt-screen toggles, `3J` and mouse DECSETs.
|
||||
* xterm obeys the toggles by switching to its scrollback-less alt buffer and wiping
|
||||
* saved lines, so history disappeared on tab switch.
|
||||
* - `strip-mux-and-mouse` (opencode, tmux-backed): alt-screen toggles and mouse DECSETs.
|
||||
* A replayed tracking enable parks xterm in report mode, where a drag goes to the CLI
|
||||
* instead of selecting text (and Ctrl+C without a selection is opencode's app_exit).
|
||||
* `3J` stays: a TUI is not a `clear` consumer.
|
||||
* - every other mode, tmux-backed: tmux's own client smcup only (#205).
|
||||
*
|
||||
* Exported so the parity with the live strip is a test (claude-scrollback-strip.test.ts).
|
||||
*/
|
||||
export function stripReplayBuffer(buffer: string, mode: SessionMode, usesMux: boolean): string {
|
||||
if (isAltScreenStripMode(mode)) {
|
||||
return buffer
|
||||
.replace(ALT_SCREEN_TOGGLE_PATTERN, '')
|
||||
.replace(ERASE_SCROLLBACK_PATTERN, '')
|
||||
.replace(MOUSE_TRACKING_PATTERN, '');
|
||||
}
|
||||
if (isMuxMouseStripMode(mode, usesMux)) {
|
||||
return buffer.replace(ALT_SCREEN_TOGGLE_PATTERN, '').replace(MOUSE_TRACKING_PATTERN, '');
|
||||
}
|
||||
if (isMuxAltScreenOnlyStripMode(mode, usesMux)) return buffer.replace(ALT_SCREEN_TOGGLE_PATTERN, '');
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -3111,21 +3143,9 @@ export function registerSessionRoutes(
|
||||
? rawBuffer
|
||||
: stripInkRedrawBloat(rawBuffer);
|
||||
|
||||
// Strip alt-screen toggles and scrollback-erase from Codex/Claude 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. Same gate as the live-stream strip in session.ts.
|
||||
if (isAltScreenStripMode(session.mode)) {
|
||||
strippedBuffer = strippedBuffer
|
||||
.replace(ALT_SCREEN_TOGGLE_PATTERN, '')
|
||||
.replace(ERASE_SCROLLBACK_PATTERN, '')
|
||||
.replace(MOUSE_TRACKING_PATTERN, '');
|
||||
} else if (isMuxAltScreenOnlyStripMode(session.mode, session.usesMux)) {
|
||||
// tmux-backed shell/opencode/antigravity: drop tmux's own client smcup only.
|
||||
// A byte buffer recorded before the live-side strip existed can still carry
|
||||
// it, and one replayed `\x1b[?1049h` re-parks xterm in the alt buffer (#205).
|
||||
strippedBuffer = strippedBuffer.replace(ALT_SCREEN_TOGGLE_PATTERN, '');
|
||||
}
|
||||
// Same strip as the live stream (session.ts), so a buffer recorded before the
|
||||
// live-side strip existed cannot re-park xterm on replay. See stripReplayBuffer.
|
||||
strippedBuffer = stripReplayBuffer(strippedBuffer, session.mode, session.usesMux);
|
||||
|
||||
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