fix(pi-tui): anchor cursor and pin height for inline images#1973
Open
Coiggahou2002 wants to merge 1 commit into
Open
fix(pi-tui): anchor cursor and pin height for inline images#1973Coiggahou2002 wants to merge 1 commit into
Coiggahou2002 wants to merge 1 commit into
Conversation
The iTerm2 path emitted height=auto, so the terminal derived the drawn row count from its own cell metrics while the TUI reserved rows from its own (possibly default or capped) cell dimensions. Any mismatch left the hardware cursor on a different row than the TUI's model after the draw, and every subsequent differential write landed on the wrong rows — rendered as text fragments overlapping the image or stale rows that are never cleared. The same class of desync hits terminals that move the cursor during image placement despite Kitty's C=1. - renderImage() now pins height=<reserved rows> cells for iTerm2 so the drawn image always fits the rows the TUI accounted for. - Image component wraps both Kitty and iTerm2 draws in DEC save/restore cursor (ESC 7 / ESC 8), so the cursor deterministically returns to the expected row no matter where the terminal leaves it after drawing. On spec-compliant terminals this is a no-op.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Inline images (pasted screenshots rendered in the transcript) can corrupt the TUI's row accounting: after an image is drawn, the hardware cursor ends up on a different row than the renderer's model expects. Every subsequent differential write then lands on the wrong rows — visible as text fragments overlapping the image area and stale rows that never get cleared, until the next full redraw.
Two independent assumptions in the iTerm2 path cause this, plus one hardening gap in the Kitty path:
Root cause
1.
height=automakes the drawn row count unpredictable (renderImageinpackages/pi-tui/src/terminal-image.ts)The TUI reserves
size.rowslines for the image, computed bycalculateImageCellSizefrom pi-tui's cell dimensions (default9x18until the terminal answers the cell-size query, and capped bymaxHeightCells). But the escape sequence tells the terminalheight=auto, so the terminal derives the drawn height fromwidth=<columns>cells and its own cell metrics and aspect handling. These need not agree. Example: a 2000x1514 screenshot capped atmaxHeightCells: 12reserves 12 rows, but with9x18cells andwidth=32an aspect-derived draw isceil(32 * 9/18 * 1514/2000) = 13rows — one row more than reserved.2. The post-draw cursor position is assumed, not anchored (
Image.renderinpackages/pi-tui/src/components/image.ts)For iTerm2 the component moves the cursor up to the first reserved row, emits the image, and assumes the cursor comes back to the last reserved row. That assumption holds only when the terminal leaves the cursor on the last drawn row and drawn rows == reserved rows. Real terminals differ: iTerm2 advances the cursor past the drawn image, and some emulators (e.g. SwiftTerm) line-feed once per drawn row, landing one row below the image. Any deviation desyncs every relative cursor move the differential renderer issues afterwards.
3. Kitty
C=1is not honored by all emulators (same file)The Kitty path relies on
C=1("don't move the cursor") — spec-correct, and fine on kitty/Ghostty/WezTerm. Emulators that move the cursor during placement anyway break the same accounting.Fix
renderImagenow emitsheight=<size.rows>(cells) instead ofheight=autofor iTerm2, so the drawn image always fits exactly the rows the TUI reserved (preserveAspectRatiostill applies — the image is fit inside thecolumns x rowsbox, same as the Kittyc=/r=box).Image.renderwraps both the Kitty and the iTerm2 draw in DEC save/restore cursor (ESC 7/ESC 8): save on the last reserved row, move to the draw position, draw, restore. The cursor deterministically returns to the row the TUI's model expects, regardless of terminal behavior. On spec-compliant terminals this is a no-op.No other callers of
renderImageexist besides theImagecomponent.Test plan
packages/pi-tuisuite passes: 712 tests (node --test test/*.test.ts)packages/pi-tui/test/terminal-image.test.ts:renderImagepinsheight=<rows>, neverheight=auto(uses the 2000x1514 / 12-row-cap overflow case)Image.renderemitsESC 7+ cursor-up + OSC 1337 +ESC 8on the last ofrowslines, and the wrapped line is still detected byisImageLineapps/kimi-codeimage tests pass (image-thumbnail,user-message,image-placeholder: 25 tests)tsc --noEmitclean forpi-tui;oxlint --type-awarereports no new warnings on changed filesBefore/after: iTerm2 draw line for a 4-row image
Before:
After:
The cursor is now guaranteed to be back on the last reserved row after the draw, and the image cannot exceed its 4 reserved rows.