Revert "fix(tui): avoid tall dialog redraw loops"

This reverts commit 392bae6b67.
This commit is contained in:
Vegard Stikbakke 2026-06-23 11:48:03 +02:00
parent 392bae6b67
commit 590482cf6d
3 changed files with 1 additions and 125 deletions

View file

@ -6,10 +6,6 @@
- Added `Ctrl+J` as a default newline keybinding alongside `Shift+Enter`.
### Fixed
- Fixed full redraw loops when offscreen lines change above a tall visible viewport, avoiding flicker for oversized dialogs ([#5990](https://github.com/earendil-works/pi/issues/5990)).
## [0.79.10] - 2026-06-22
## [0.79.9] - 2026-06-20

View file

@ -313,7 +313,6 @@ export class TUI extends Container {
private clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1"; // Clear empty rows when content shrinks (default: off)
private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
private skippedOffscreenChangeRange: { first: number; last: number } | undefined;
private fullRedrawCount = 0;
private stopped = false;
private pendingOsc11BackgroundReplies = 0;
@ -719,7 +718,6 @@ export class TUI extends Container {
this.hardwareCursorRow = 0;
this.maxLinesRendered = 0;
this.previousViewportTop = 0;
this.skippedOffscreenChangeRange = undefined;
if (this.renderTimer) {
clearTimeout(this.renderTimer);
this.renderTimer = undefined;
@ -1174,30 +1172,6 @@ export class TUI extends Container {
return this.deleteKittyImages(ids);
}
private changedRangeContainsKittyImages(firstChanged: number, lastChanged: number, newLines: string[]): boolean {
for (let i = firstChanged; i <= lastChanged; i++) {
if (extractKittyImageIds(this.previousLines[i] ?? "").length > 0) return true;
if (extractKittyImageIds(newLines[i] ?? "").length > 0) return true;
}
return false;
}
private markSkippedOffscreenChange(first: number, last: number): void {
if (!this.skippedOffscreenChangeRange) {
this.skippedOffscreenChangeRange = { first, last };
return;
}
this.skippedOffscreenChangeRange = {
first: Math.min(this.skippedOffscreenChangeRange.first, first),
last: Math.max(this.skippedOffscreenChangeRange.last, last),
};
}
private skippedOffscreenChangesIntersect(first: number, last: number): boolean {
const range = this.skippedOffscreenChangeRange;
return range !== undefined && range.first <= last && range.last >= first;
}
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
private compositeLineAt(
baseLine: string,
@ -1348,7 +1322,6 @@ export class TUI extends Container {
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
this.previousWidth = width;
this.previousHeight = height;
this.skippedOffscreenChangeRange = undefined;
};
const debugRedraw = process.env.PI_DEBUG_REDRAW === "1";
@ -1376,18 +1349,7 @@ export class TUI extends Container {
// Height changes normally need a full re-render to keep the visible viewport aligned,
// but Termux changes height when the software keyboard shows or hides.
// In that environment, a full redraw causes the entire history to replay on every toggle.
// If the new Termux viewport would reveal rows skipped by the offscreen optimization,
// redraw once so stale scrollback does not become visible.
if (heightChanged && isTermuxSession()) {
const viewportBottom = prevViewportTop + height - 1;
if (this.skippedOffscreenChangesIntersect(prevViewportTop, viewportBottom)) {
logRedraw(
`terminal height changed revealing skipped offscreen changes (${this.previousHeight} -> ${height})`,
);
fullRender(true);
return;
}
} else if (heightChanged) {
if (heightChanged && !isTermuxSession()) {
logRedraw(`terminal height changed (${this.previousHeight} -> ${height})`);
fullRender(true);
return;
@ -1439,24 +1401,6 @@ export class TUI extends Container {
return;
}
// If all changed lines are above the visible viewport and the buffer length did not change,
// the on-screen content is already correct. Avoid clearing/replaying the viewport for
// offscreen animations such as tall dialogs with an updating header/status line.
if (
lastChanged < prevViewportTop &&
newLines.length === this.previousLines.length &&
!this.changedRangeContainsKittyImages(firstChanged, lastChanged, newLines)
) {
this.markSkippedOffscreenChange(firstChanged, lastChanged);
this.positionHardwareCursor(cursorPos, newLines.length);
this.previousLines = newLines;
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
this.previousWidth = width;
this.previousHeight = height;
this.previousViewportTop = prevViewportTop;
return;
}
// All changes are in deleted lines (nothing to render, just clear)
if (firstChanged >= newLines.length) {
if (this.previousLines.length > newLines.length) {

View file

@ -378,44 +378,6 @@ describe("TUI resize handling", () => {
});
});
it("full re-renders in Termux when height changes reveal skipped offscreen changes", async () => {
await withEnv({ TERMUX_VERSION: "1" }, async () => {
const terminal = new VirtualTerminal(20, 5);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = Array.from({ length: 11 }, (_, i) => `Line ${i}`);
tui.start();
await terminal.waitForRender();
component.lines = Array.from({ length: 11 }, (_, i) => (i === 5 ? "Changed 5" : `Line ${i}`));
tui.requestRender();
await terminal.waitForRender();
const redrawsAfterSkippedChange = tui.fullRedraws;
assert.deepStrictEqual(terminal.getViewport(), ["Line 6", "Line 7", "Line 8", "Line 9", "Line 10"]);
terminal.resize(20, 6);
await terminal.waitForRender();
assert.ok(
tui.fullRedraws > redrawsAfterSkippedChange,
"Height change should redraw before skipped offscreen content becomes visible",
);
assert.deepStrictEqual(terminal.getViewport(), [
"Changed 5",
"Line 6",
"Line 7",
"Line 8",
"Line 9",
"Line 10",
]);
tui.stop();
});
});
it("triggers full re-render when terminal width changes", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
@ -695,32 +657,6 @@ describe("TUI differential rendering", () => {
tui.stop();
});
it("does not full redraw for offscreen changes above the viewport", async () => {
const terminal = new VirtualTerminal(20, 5);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
const stableVisibleLines = Array.from({ length: 8 }, (_, i) => `Dialog ${i}`);
component.lines = ["Ticker 0", "Intro", "", ...stableVisibleLines];
tui.start();
await terminal.waitForRender();
const initialRedraws = tui.fullRedraws;
const initialViewport = terminal.getViewport();
for (let i = 1; i <= 3; i++) {
component.lines = [`Ticker ${i}`, "Intro", "", ...stableVisibleLines];
tui.requestRender();
await terminal.waitForRender();
}
assert.strictEqual(tui.fullRedraws, initialRedraws, "Offscreen changes should not force full redraws");
assert.deepStrictEqual(terminal.getViewport(), initialViewport, "Visible viewport should remain stable");
tui.stop();
});
it("full re-renders when deleted lines move the viewport upward", async () => {
const terminal = new VirtualTerminal(20, 5);
const tui = new TUI(terminal);