diff --git a/packages/pi-tui/src/ledger/engine.ts b/packages/pi-tui/src/ledger/engine.ts index fd823c416..fa5a0a275 100644 --- a/packages/pi-tui/src/ledger/engine.ts +++ b/packages/pi-tui/src/ledger/engine.ts @@ -688,4 +688,29 @@ export class LedgerTuiEngine { this.#preparedMeta = []; this.#preparedValidRows = 0; } + + // Park the hardware cursor just past the last content row so the host's shell + // prompt does not overwrite painted rows on exit. Mirrors TUI.stop()'s legacy + // cursor-parking, but sourced from the ledger's own window/cursor state. + public parkCursorForExit(): void { + const window = this.#previousWindow; + if (window.length === 0) return; + let lastContentRow = -1; + for (let i = window.length - 1; i >= 0; i--) { + if ((window[i] ?? "").length > 0) { + lastContentRow = i; + break; + } + } + const targetRow = lastContentRow + 1; + const height = this.terminal.rows; + const rawCursorRow = this.#hardwareCursorRow - this.#windowTopRow; + const cursorScreenRow = Math.max(0, Math.min(height - 1, rawCursorRow)); + let seq = ""; + const lineDiff = targetRow - cursorScreenRow; + if (lineDiff > 0) seq += `\x1b[${lineDiff}B`; + else if (lineDiff < 0) seq += `\x1b[${-lineDiff}A`; + seq += "\r\n"; + this.terminal.write(seq); + } } diff --git a/packages/pi-tui/src/tui.ts b/packages/pi-tui/src/tui.ts index ae3e6614b..8bd38c652 100644 --- a/packages/pi-tui/src/tui.ts +++ b/packages/pi-tui/src/tui.ts @@ -706,7 +706,9 @@ export class TUI extends Container { this.terminal.write("\x1b[?2031l"); } // Move cursor to the end of the content to prevent overwriting/artifacts on exit - if (this.previousLines.length > 0) { + if (TUI.LEDGER_ENABLED && this.ledgerEngine) { + this.ledgerEngine.parkCursorForExit(); + } else if (this.previousLines.length > 0) { const targetRow = this.previousLines.length; // Line after the last content const lineDiff = targetRow - this.hardwareCursorRow; if (lineDiff > 0) { diff --git a/packages/pi-tui/test/ledger-engine-golden.test.ts b/packages/pi-tui/test/ledger-engine-golden.test.ts index ed51f9415..5366b39b3 100644 --- a/packages/pi-tui/test/ledger-engine-golden.test.ts +++ b/packages/pi-tui/test/ledger-engine-golden.test.ts @@ -96,4 +96,26 @@ describe("ledger engine golden", () => { tui.stop(); }); }); + + it("parks cursor past content on stop()", async () => { + await withLedger(async () => { + const terminal = new LoggingVirtualTerminal(40, 10); + const tui = new TUI(terminal); + const c = new TestComponent(); + tui.addChild(c); + c.lines = ["Hello", "World"]; + tui.start(); + await terminal.waitForRender(); + terminal.clearWrites(); + tui.stop(); + const writes = terminal.getWrites(); + // stop() must emit a cursor-parking sequence ending with CRLF so the host + // shell prompt lands on a fresh line below the painted content (not + // overwriting it). This regresses the ledger-path exit artifact. + assert.ok( + writes.includes("\r\n"), + `stop() should park cursor with a trailing CRLF; got: ${JSON.stringify(writes)}`, + ); + }); + }); });