Skip to content

fix(tui): align wrapped composer cursor#25860

Closed
glesperance wants to merge 1 commit into
NousResearch:mainfrom
glesperance:fix/tui-wrapped-paste-cursor
Closed

fix(tui): align wrapped composer cursor#25860
glesperance wants to merge 1 commit into
NousResearch:mainfrom
glesperance:fix/tui-wrapped-paste-cursor

Conversation

@glesperance

Copy link
Copy Markdown
Contributor

Rationale

Long pasted composer input can wrap across multiple terminal rows. The TUI previously parked the native terminal cursor separately from Ink's wrapped text layout, which could make the cursor drift or show a second cursor after paste. Switching wrapped input to an in-band cursor fixed the double cursor, but the fallback inverse-video cursor lost the Hermes cursor color.

Summary

  • Use the native terminal cursor only for focused, unselected single-line TTY input.
  • Render wrapped composer cursors in-band with the same text stream so they stay aligned with word wrapping.
  • Hide the hardware cursor while the in-band cursor is active to prevent double cursors.
  • Pass the Hermes prompt color into TextInput so the synthetic cursor remains visually consistent on multiline input.
  • Add focused cursor-mode and render-part regression coverage.

Test Plan

  • npm run build --prefix packages/hermes-ink
  • npm run test -- textInputWrap.test.ts
  • npm run type-check
  • npm run build
  • git diff --check
  • npm exec eslint -- src/components/textInput.tsx src/components/appLayout.tsx src/__tests__/textInputWrap.test.ts (0 errors; existing warnings only)

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels May 14, 2026
XpeRIall pushed a commit to XpeRIall/hermes-agent that referenced this pull request May 17, 2026
…e 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 (NousResearch#26717, NousResearch#25860, NousResearch#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.
aashizpoudel pushed a commit to aashizpoudel/hermes-agent that referenced this pull request May 21, 2026
…e 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 (NousResearch#26717, NousResearch#25860, NousResearch#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.

(cherry picked from commit 3b4dd68)
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…e 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 (NousResearch#26717, NousResearch#25860, NousResearch#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.
@teknium1

Copy link
Copy Markdown
Contributor

Automated hermes-sweeper review found this is now implemented on main by the later merged cursor-drift fix.

Evidence:

  • PR fix(tui): align composer cursorLayout with wrap-ansi to kill multiline cursor drift #27489 merged with commit 3b4dd683263c5895bb6144564e4bea8881d79993 and explicitly calls out fix(tui): align wrapped composer cursor #25860 as one of the earlier PRs chasing this multiline composer cursor bug class.
  • ui-tui/src/lib/inputMetrics.ts:23 now derives visual line breaks from wrapAnsi(value, width, { hard: true, trim: false }), the same wrapping path used by Ink rendering.
  • ui-tui/src/lib/inputMetrics.ts:122 now computes cursorLayout from those wrap-ansi-derived visual lines and avoids the previous phantom-row overflow behavior.
  • ui-tui/src/__tests__/cursorDriftRegression.test.ts:48 covers the multiline composer drift by checking typed prefixes against wrap-ansi at multiple terminal widths.
  • The fixing commit is contained in v2026.5.28 and later tags.

Thanks for the original fix attempt — it helped identify the TUI cursor area that the later root-cause fix resolved.

@teknium1 teknium1 closed this Jun 12, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants