From 561d73d3aa10f024fddfd3b354c847132ac42b06 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 7 Apr 2026 22:34:17 -0500 Subject: [PATCH] fix(pi-tui): use contentCursorRow for render movement baseline instead of cursorRow PR #3744 fixed autocomplete ghost lines by introducing a local contentCursorRow initialized from this.cursorRow, but this.cursorRow tracks the content end (last line), not where the cursor actually ended up after rendering. This caused computeLineDiff to compute wrong movement deltas, making content clear and jump on every keystroke. Fix: add an instance field contentCursorRow that stores finalCursorRow after content rendering but before positionHardwareCursor moves the cursor for IME. This correctly separates three cursor concepts: - cursorRow: logical content end (viewport calculation) - contentCursorRow: post-render cursor position (movement baseline) - hardwareCursorRow: actual terminal cursor (may differ due to IME) Closes #3764 --- packages/pi-tui/src/tui.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/pi-tui/src/tui.ts b/packages/pi-tui/src/tui.ts index 3408016a0..b0240aad3 100644 --- a/packages/pi-tui/src/tui.ts +++ b/packages/pi-tui/src/tui.ts @@ -239,6 +239,7 @@ export class TUI extends Container { public onDebug?: () => void; private renderRequested = false; private cursorRow = 0; // Logical cursor row (end of rendered content) + private contentCursorRow = 0; // Cursor row after content rendering, before IME repositioning private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning) private inputBuffer = ""; // Buffer for parsing terminal responses private cellSizeQueryPending = false; @@ -498,6 +499,7 @@ export class TUI extends Container { this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear this.cursorRow = 0; + this.contentCursorRow = 0; this.hardwareCursorRow = 0; this.maxLinesRendered = 0; this.previousViewportTop = 0; @@ -616,10 +618,9 @@ export class TUI extends Container { const height = this.terminal.rows; let viewportTop = Math.max(0, this.maxLinesRendered - height); let prevViewportTop = this.previousViewportTop; - let contentCursorRow = this.cursorRow; - let hardwareCursorRow = this.hardwareCursorRow; + let hardwareCursorRow = this.contentCursorRow; const computeLineDiff = (targetRow: number): number => { - const currentScreenRow = contentCursorRow - prevViewportTop; + const currentScreenRow = hardwareCursorRow - prevViewportTop; const targetScreenRow = targetRow - viewportTop; return targetScreenRow - currentScreenRow; }; @@ -664,6 +665,7 @@ export class TUI extends Container { buffer += "\x1b[?2026l"; // End synchronized output this.terminal.write(buffer); this.cursorRow = Math.max(0, newLines.length - 1); + this.contentCursorRow = this.cursorRow; this.hardwareCursorRow = this.cursorRow; // Reset max lines when clearing, otherwise track growth if (clear) { @@ -771,6 +773,7 @@ export class TUI extends Container { buffer += "\x1b[?2026l"; this.terminal.write(buffer); this.cursorRow = targetRow; + this.contentCursorRow = targetRow; this.hardwareCursorRow = targetRow; } this.positionHardwareCursor(cursorPos, newLines.length); @@ -806,7 +809,6 @@ export class TUI extends Container { buffer += "\r\n".repeat(scroll); prevViewportTop += scroll; viewportTop += scroll; - contentCursorRow = moveTargetRow; hardwareCursorRow = moveTargetRow; } @@ -889,8 +891,10 @@ export class TUI extends Container { // Track cursor position for next render // cursorRow tracks end of content (for viewport calculation) - // hardwareCursorRow tracks actual terminal cursor position (for movement) + // contentCursorRow tracks cursor after content rendering (before IME repositioning) + // hardwareCursorRow tracks actual terminal cursor position (may differ due to IME) this.cursorRow = Math.max(0, newLines.length - 1); + this.contentCursorRow = finalCursorRow; this.hardwareCursorRow = finalCursorRow; // Track terminal's working area (grows but doesn't shrink unless cleared) this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);