diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index b1108dee..91b52474 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -6,6 +6,10 @@ - Added terminal background color query support for OSC 11 replies. +### Fixed + +- Fixed WezTerm inline Kitty image rendering during full redraw fallbacks so image padding rows are reserved before the placement is drawn without regressing tall-image placement ([#5618](https://github.com/earendil-works/pi/issues/5618), [#4415](https://github.com/earendil-works/pi/issues/4415)). + ## [0.79.3] - 2026-06-13 ## [0.79.2] - 2026-06-12 diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 2b5d05cd..1fff6545 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -1244,7 +1244,20 @@ export class TUI extends Container { } for (let i = 0; i < newLines.length; i++) { if (i > 0) buffer += "\r\n"; - buffer += newLines[i]; + const line = newLines[i]; + const isImage = isImageLine(line); + const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i) : 1; + if (imageReservedRows > 1 && imageReservedRows <= height) { + for (let row = 1; row < imageReservedRows; row++) { + buffer += "\r\n"; + } + buffer += `\x1b[${imageReservedRows - 1}A`; + buffer += line; + buffer += `\x1b[${imageReservedRows - 1}B`; + i += imageReservedRows - 1; + continue; + } + buffer += line; } buffer += "\x1b[?2026l"; // End synchronized output this.terminal.write(buffer); diff --git a/packages/tui/test/tui-render.test.ts b/packages/tui/test/tui-render.test.ts index 4e31e8f4..ab038ac7 100644 --- a/packages/tui/test/tui-render.test.ts +++ b/packages/tui/test/tui-render.test.ts @@ -152,6 +152,95 @@ describe("TUI Kitty image cleanup", () => { } }); + it("reserves Kitty image rows before drawing during full redraw fallbacks", async () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const terminal = new LoggingVirtualTerminal(40, 5); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + component.lines = ["l0", "l1", "l2", "l3", "l4"]; + tui.start(); + await terminal.waitForRender(); + const redrawsBeforeImage = tui.fullRedraws; + terminal.clearWrites(); + + const image = new Image( + "AAAA", + "image/png", + { fallbackColor: (value) => value }, + { maxWidthCells: 3 }, + { widthPx: 30, heightPx: 30 }, + ); + const imageLines = image.render(40); + const imageSequence = imageLines[0]; + component.lines = ["l0", "l1", "l2", "l3", "l4", ...imageLines, "after"]; + tui.requestRender(); + await terminal.waitForRender(); + + const writes = terminal.getWrites(); + assert.ok(tui.fullRedraws > redrawsBeforeImage, "scrolling image append should force a full redraw"); + assert.ok( + writes.includes(`\r\n\r\n\x1b[2A${imageSequence}\x1b[2B`), + "full redraw should reserve visible image rows before drawing the placement", + ); + assert.ok( + !writes.includes(`${imageSequence}\r\n\x1b[0m`), + "full redraw must not write reserved padding rows after drawing the placement", + ); + + tui.stop(); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + + it("does not use cursor-up placement for Kitty images taller than the viewport", async () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const terminal = new LoggingVirtualTerminal(40, 5); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + component.lines = ["before"]; + tui.start(); + await terminal.waitForRender(); + terminal.clearWrites(); + + const image = new Image( + "AAAA", + "image/png", + { fallbackColor: (value) => value }, + { maxWidthCells: 6 }, + { widthPx: 60, heightPx: 60 }, + ); + const imageLines = image.render(40); + const imageSequence = imageLines[0]; + assert.ok(imageLines.length > terminal.rows, "test image should exceed the viewport height"); + + component.lines = ["before", ...imageLines, "after"]; + tui.requestRender(true); + await terminal.waitForRender(); + + const writes = terminal.getWrites(); + assert.ok(writes.includes(imageSequence), "image placement should be drawn"); + assert.ok( + !writes.includes(`\x1b[${imageLines.length - 1}A${imageSequence}`), + "taller-than-viewport images must keep the #4461 first-row placement path", + ); + + tui.stop(); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + it("deletes changed image ids before drawing moved placements", async () => { const terminal = new LoggingVirtualTerminal(40, 10); const tui = new TUI(terminal);