diff --git a/packages/pi-tui/src/ledger/engine.ts b/packages/pi-tui/src/ledger/engine.ts index ca50b56c3..baf60c9a8 100644 --- a/packages/pi-tui/src/ledger/engine.ts +++ b/packages/pi-tui/src/ledger/engine.ts @@ -7,6 +7,8 @@ import { getNativeScrollbackCommitSafeEnd, getNativeScrollbackLiveRegionStart, g import { coalesceAdjacentSgr } from "./sgr-coalesce.ts"; import { type CursorControlResult, + ALT_SCREEN_ENTER, + ALT_SCREEN_EXIT, DISABLE_AUTOWRAP, ENABLE_AUTOWRAP, ERASE_LINE, @@ -53,7 +55,6 @@ export class LedgerTuiEngine { // ---- resize viewport defer (Phase B Task 4) ---- #resizeViewportActive = false; #resizeViewportSettleTimer: ReturnType | undefined; - #resizeAltActive = false; #resizeViewportPaintCount = 0; static readonly #RESIZE_SETTLE_MS = 120; @@ -700,9 +701,8 @@ export class LedgerTuiEngine { public beginResizeViewport(): void { if (isMultiplexerSession()) return; // mux: no alt-screen if (!this.#resizeViewportActive) { - this.terminal.write("\x1b[?1049h"); + this.terminal.write(ALT_SCREEN_ENTER); this.#resizeViewportActive = true; - this.#resizeAltActive = true; } this.#armResizeSettle(); } @@ -717,8 +717,7 @@ export class LedgerTuiEngine { this.#resizeViewportSettleTimer = undefined; if (this.#resizeViewportActive) { this.#resizeViewportActive = false; - this.#resizeAltActive = false; - this.terminal.write("\x1b[?1049l"); + this.terminal.write(ALT_SCREEN_EXIT); } // authoritative full repaint after settle (clear scrollback, rewrap) this.requestFullPaint(true); @@ -746,15 +745,23 @@ export class LedgerTuiEngine { clearTimeout(this.#resizeViewportSettleTimer); this.#resizeViewportSettleTimer = undefined; } - if (this.#resizeAltActive) { + if (this.#resizeViewportActive) { this.#resizeViewportActive = false; - this.#resizeAltActive = false; - this.terminal.write("\x1b[?1049l"); + this.terminal.write(ALT_SCREEN_EXIT); } } public reset(): void { this.#stopped = false; + if (this.#resizeViewportSettleTimer) { + clearTimeout(this.#resizeViewportSettleTimer); + this.#resizeViewportSettleTimer = undefined; + } + if (this.#resizeViewportActive) { + this.#resizeViewportActive = false; + this.terminal.write(ALT_SCREEN_EXIT); + } + this.#resizeViewportPaintCount = 0; this.#committedRows = 0; this.#committedPrefix = []; this.#committedPrefixAuditRows = 0; diff --git a/packages/pi-tui/test/resize-defer.test.ts b/packages/pi-tui/test/resize-defer.test.ts index ccac577ff..065b0523d 100644 --- a/packages/pi-tui/test/resize-defer.test.ts +++ b/packages/pi-tui/test/resize-defer.test.ts @@ -16,8 +16,9 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, // Env vars that make isMultiplexerSession() return true. The non-mux fast path is // gated on `!isMultiplexerSession()`, so a non-mux test must clear these — the // developer/CI shell often runs inside tmux/cmux, which would otherwise force the -// mux path and make the assertions vacuous. -const MUX_ENV_KEYS = ["TMUX", "STY", "ZELLIJ", "CMUX_WORKSPACE_ID", "CMUX_SURFACE_ID"] as const; +// mux path and make the assertions vacuous. TERM is included because tmux/screen +// set it to `tmux-256color`/`screen-256color`, which alone flips isMultiplexerSession(). +const MUX_ENV_KEYS = ["TMUX", "STY", "ZELLIJ", "CMUX_WORKSPACE_ID", "CMUX_SURFACE_ID", "TERM"] as const; function snapshotMuxEnv(): Record { const snap: Record = {}; @@ -67,6 +68,9 @@ async function withLedgerMux(run: () => Promise): Promise { const ALT_ENTER = "\x1b[?1049h"; const ALT_EXIT = "\x1b[?1049l"; const ED3 = "\x1b[3J"; +// Per-row erase emitted only by the resize viewport fast path (#renderResizeViewport); +// the full/update paint paths use ED2/ED3 or ERASE_TO_END_OF_LINE (\x1b[K), never \x1b[2K. +const ERASE_LINE = "\x1b[2K"; describe("ledger resize viewport defer", () => { it("uses the alt-screen fast path during a resize drag (no ED3)", async () => { @@ -89,6 +93,10 @@ describe("ledger resize viewport defer", () => { const writes = terminal.getWrites(); assert.ok(writes.includes(ALT_ENTER), `drag should enter the alt-screen; got: ${JSON.stringify(writes)}`); + assert.ok( + writes.includes(ERASE_LINE), + `drag should paint via the viewport fast path (per-row \\x1b[2K); got: ${JSON.stringify(writes)}`, + ); assert.ok(!writes.includes(ED3), `drag must not clear scrollback (ED3); got: ${JSON.stringify(writes)}`); tui.stop(); });