Skip to content

fix(tui): prevent cursor marker leaks - #9441

Open
muyiyr wants to merge 3 commits into
earendil-works:mainfrom
muyiyr:fix/9257-cursor-marker-leak
Open

muyiyr wants to merge 3 commits into
earendil-works:mainfrom
muyiyr:fix/9257-cursor-marker-leak

Conversation

@muyiyr

@muyiyr muyiyr commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix cursor-marker leakage across the full-screen selection and terminal rendering paths.

  • Treat APC cursor markers as positional metadata instead of persistent styling, so sliceWithWidth() does not replay them into later selection slices.
  • Preserve the bottommost valid visible marker as the hardware-cursor position and remove duplicate markers from that cursor line.
  • Enforce the output-boundary invariant by stripping all remaining cursor markers from every non-image rendered line, including other visible lines and scrollback lines written during a main-screen full render.

This combines the duplicate-marker cleanup reported in #9257 with the upstream selection-slicing cause identified in #9332. The sliceWithWidth() root cause and Ghostty behavior were identified by @Andy8647 in #9332. The duplicate-marker cleanup direction was also independently proposed by @bluefateludi in #9257.

Closes #9257
Closes #9332

Test Plan

  • Confirmed the sliceWithWidth() regression test fails before the fix because later slices contain CURSOR_MARKER.
  • Confirmed APC cursor markers are not carried into later slices while persistent SGR styling still is.
  • Confirmed the regular-screen duplicate-marker test preserves the first marker's cursor column and emits no marker bytes.
  • Confirmed a 2-row main-screen render with markers above the viewport and on multiple visible lines preserves the content, uses the bottom visible marker for cursor positioning, and emits no marker bytes.
  • Confirmed the full-screen regression starts with one caret marker, performs a real mouse drag after the caret, renders the selection highlight, and emits no marker bytes.
  • npm test --workspace @earendil-works/pi-tui
  • tsgo -p packages/tui/tsconfig.build.json --noEmit
  • biome check --error-on-warnings on the five changed files

The current GitHub Actions failure occurs in the unrelated packages/ai/src/api/google-shared.ts build on FinishReason.TOO_MANY_TOOL_CALLS; current main fails on the same error. The pi-tui build, complete package test suite, type check, and changed-file formatting checks pass.

This PR was prepared with AI assistance and reviewed by me.

Avoid carrying APC cursor markers across selection slices and strip all remaining markers before terminal output.

Closes earendil-works#9257
Closes earendil-works#9332
Keep marker cleanup at the slicing and cursor extraction boundaries, and document why APC state must not carry across slices.
@muyiyr
muyiyr force-pushed the fix/9257-cursor-marker-leak branch from 4bc8f73 to b82f251 Compare September 10, 2026 13:32

@gaoanze888 gaoanze888 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is still a cursor-marker output leak outside the selected line. extractCursorPosition() scans only the visible viewport and returns immediately after finding the bottommost marked line; the patch removes duplicates only from that one line. Any marker on another visible line, or above the viewport during a full render, remains in lines and is passed to the terminal writer.

I reproduced this at exact head b82f251 with a TuiMainScreen whose 2-row terminal renders three lines: offscreen + marker, visible-a + marker, and visible-b + marker. The bottom marker determines the cursor and is removed, but terminal.writes.join("") still contains CURSOR_MARKER; the focused node:test fails on that assertion. This is the same output-boundary invariant the PR intends to guarantee, not just a synthetic duplicate on the selected line.

Please preserve the first valid visible marker for cursor positioning, but strip CURSOR_MARKER from every non-image rendered line before terminal output, and add coverage for markers on multiple lines / above the visible viewport. (The first commit appears to have had broader cleanup and an offscreen regression before the narrowing follow-up removed them.)

@muyiyr
muyiyr force-pushed the fix/9257-cursor-marker-leak branch from dfcbc56 to f2e059b Compare September 14, 2026 07:02
@muyiyr

muyiyr commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

@gaoanze888 Addressed in f2e059b80.

Cursor detection is unchanged: extractCursorPosition() still scans the visible viewport bottom-up, so the bottommost valid visible marker determines the hardware cursor position. After extraction, applyLineResets() now strips CURSOR_MARKER from every non-image rendered line before either renderer can write it to the terminal. This covers markers on other visible lines and above the viewport during a main-screen full render.

The new regression uses a 2-row terminal with three marked lines: one above the viewport, one upper visible marker at column 1, and one bottom visible marker at column 9. It verifies that:

  • offscreen and other visible content are preserved;
  • zero CURSOR_MARKER bytes reach terminal output;
  • ESC[10G is emitted, proving the bottom visible marker still determines the cursor column.

I also rechecked the original issue paths: same-line duplicates are stripped while the first marker remains the cursor source (#9257), sliceWithWidth() no longer carries APC into later slices while SGR still carries, and the fullscreen drag-selection path emits no marker bytes (#9332).

Verification:

  • node --test test/tab-width.test.ts test/tui-render.test.ts test/tui-alt-screen.test.ts — 97 passed
  • npm test --workspace @earendil-works/pi-tui — passed
  • tsgo -p packages/tui/tsconfig.build.json --noEmit — passed
  • Biome on the five changed files — passed

The current GitHub CI failure is in the unrelated packages/ai/src/api/google-shared.ts build, and current main fails on the same FinishReason.TOO_MANY_TOOL_CALLS error; the TUI build completes before that failure.

This comment is AI-generated by Trae CLI.

@gaoanze888 gaoanze888 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the fix at exact head f2e059b. Cursor selection still uses the bottommost valid visible marker, while applyLineResets() now strips markers from every non-image rendered line at the terminal-output boundary. The new 3-line/2-row regression covers both another visible marker and one above the viewport, preserves their content, emits no marker bytes, and still positions the cursor from the bottom marker. The full @earendil-works/pi-tui suite passes locally. The GitHub failure is the unrelated packages/ai/src/api/google-shared.ts exhaustiveness error also tracked by #9570, not this TUI change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants