fix(tui): align composer cursorLayout with wrap-ansi to kill multiline cursor drift
The composer's `cursorLayout` (in `ui-tui/src/lib/inputMetrics.ts`) used a hand-rolled word-wrap algorithm to decide where `useDeclaredCursor` should park the hardware cursor. But Ink's `<Text wrap="wrap">` renders the same text via `wrap-ansi`. The two algorithms disagreed on common real-world inputs — `"branch investigate"` at cols=20, `"hello world"` at cols=8, exact-fill strings like `"abcdefgh"` at cols=8 — so the hardware cursor parked several cells past where Ink actually rendered the last character. Users saw a multi-cell blank gap between their last-typed letter and the cursor block, especially on narrow terminals (the Cursor IDE built-in terminal was the worst offender). Three previous PRs (#26717, #25860, #22197) chased fast-echo displayCursor/cursorDeclaration drift and in-band-vs-native cursor heuristics. None of them touched the underlying wrap-algorithm mismatch, which is why the bug kept resurfacing. Fix: source cursorLayout's line breaks from wrap-ansi directly. Walk its emitted string char-by-char, tracking original-string offsets, push a VisualLine at each '\n'. Also drop the buggy `column >= w` overflow rule in cursorLayout — that's what pushed exact-fill text onto a phantom next row. canFastBackspaceShape now detects the wrap boundary in BOTH coordinate conventions (column === 0 OR column >= columns), since exact-fill now reports as (0, columns) instead of the previous (1, 0). The physical state is identical — the terminal auto-wraps at column N either way — but the layout function reports the position more honestly. Tests: - ui-tui/src/__tests__/textInputWrap.test.ts: 3 tests that pinned the BUGGY behavior were updated to assert wrap-ansi parity (the real invariant). Added a typing-prefix invariant: cursorLayout must agree with wrap-ansi at every character of a long input. - ui-tui/src/__tests__/cursorDriftRegression.test.ts: new file. Walks the user-reported bug message char-by-char at 7 widths and asserts agreement with wrap-ansi at every prefix. Verification: - 791/791 vitest tests pass. - 84/84 tui-gateway pytest tests pass via scripts/run_tests.sh. - PTY repro (typing into a real `hermes --tui` PTY at cols=50/55/60): cursor lands exactly 1 cell past the last typed char in every case the bug previously drifted.
This commit is contained in:
@@ -272,10 +272,22 @@ export function canFastBackspaceShape(current: string, cursor: number, columns?:
|
||||
}
|
||||
|
||||
// If we know the wrap width, reject at the soft-wrap boundary: the
|
||||
// caret's visual column is 0, so "\b \b" can't represent the physical
|
||||
// move back to the previous visual line.
|
||||
if (columns !== undefined && cursorLayout(current, cursor, columns).column === 0) {
|
||||
return false
|
||||
// caret's physical column would be at (or past) the terminal's right
|
||||
// edge, so the terminal has already auto-wrapped to the next row.
|
||||
// "\b \b" can't represent the physical move back across that wrap.
|
||||
//
|
||||
// We check `column === 0` for the "wrap-ansi broke onto a new line"
|
||||
// case AND `column >= columns` for the "exact-fill, terminal auto-wraps"
|
||||
// case. Both manifest as the same physical state (cursor parked at
|
||||
// col 0 of the next row) but cursorLayout reports them differently
|
||||
// because it now mirrors wrap-ansi's break points exactly (see the
|
||||
// cursor-drift-multiline fix in lib/inputMetrics.ts).
|
||||
if (columns !== undefined) {
|
||||
const layout = cursorLayout(current, cursor, columns)
|
||||
|
||||
if (layout.column === 0 || layout.column >= columns) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const removed = current.slice(prevPos(current, cursor), cursor)
|
||||
|
||||
Reference in New Issue
Block a user