fix(tui): reduce unnecessary full redraws for better performance

- Remove height change detection (only width changes trigger full redraw)
- Change clearOnShrink default to false (use PI_CLEAR_ON_SHRINK=1 to enable)
- Fix viewport check to use previousLines.length instead of maxLinesRendered
  (prevents false positive redraws when appending lines after content shrunk)
- Add clearOnShrink setting to /settings in coding-agent
- Remove line truncation in custom message component (always show full content)
This commit is contained in:
Mario Zechner 2026-02-02 08:10:08 +01:00
parent 419c07fb19
commit 0925fafe3b
8 changed files with 80 additions and 48 deletions

View file

@ -199,7 +199,6 @@ export class TUI extends Container {
public terminal: Terminal;
private previousLines: string[] = [];
private previousWidth = 0;
private previousHeight = 0;
private focusedComponent: Component | null = null;
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
@ -210,6 +209,7 @@ export class TUI extends Container {
private inputBuffer = ""; // Buffer for parsing terminal responses
private cellSizeQueryPending = false;
private showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1";
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 fullRedrawCount = 0;
@ -248,6 +248,19 @@ export class TUI extends Container {
this.requestRender();
}
getClearOnShrink(): boolean {
return this.clearOnShrink;
}
/**
* Set whether to trigger full re-render when content shrinks.
* When true (default), empty rows are cleared when content shrinks.
* When false, empty rows remain (reduces redraws on slower terminals).
*/
setClearOnShrink(enabled: boolean): void {
this.clearOnShrink = enabled;
}
setFocus(component: Component | null): void {
// Clear focused flag on old component
if (isFocusable(this.focusedComponent)) {
@ -397,7 +410,6 @@ export class TUI extends Container {
if (force) {
this.previousLines = [];
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
this.cursorRow = 0;
this.hardwareCursorRow = 0;
this.maxLinesRendered = 0;
@ -827,9 +839,8 @@ export class TUI extends Container {
newLines = this.applyLineResets(newLines);
// Width or height changed - need full re-render
// Width changed - need full re-render (line wrapping changes)
const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
const heightChanged = this.previousHeight !== 0 && this.previousHeight !== height;
// Helper to clear scrollback and viewport and render all new lines
const fullRender = (clear: boolean): void => {
@ -854,24 +865,24 @@ export class TUI extends Container {
this.positionHardwareCursor(cursorPos, newLines.length);
this.previousLines = newLines;
this.previousWidth = width;
this.previousHeight = height;
};
// First render - just output everything without clearing (assumes clean screen)
if (this.previousLines.length === 0 && !widthChanged && !heightChanged) {
if (this.previousLines.length === 0 && !widthChanged) {
fullRender(false);
return;
}
// Width or height changed - full re-render
if (widthChanged || heightChanged) {
// Width changed - full re-render (line wrapping changes)
if (widthChanged) {
fullRender(true);
return;
}
// Content shrunk below the working area and no overlays - re-render to clear empty rows
// (overlays need the padding, so only do this when no overlays are active)
if (newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
// Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
if (this.clearOnShrink && newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
fullRender(true);
return;
}
@ -941,15 +952,15 @@ export class TUI extends Container {
this.positionHardwareCursor(cursorPos, newLines.length);
this.previousLines = newLines;
this.previousWidth = width;
this.previousHeight = height;
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
return;
}
// Check if firstChanged is outside the viewport
// Viewport is based on max lines ever rendered (terminal's working area)
if (firstChanged < viewportTop) {
// First change is above viewport - need full re-render
// Check if firstChanged is above what was previously visible
// Use previousLines.length (not maxLinesRendered) to avoid false positives after content shrinks
const previousContentViewportTop = Math.max(0, this.previousLines.length - height);
if (firstChanged < previousContentViewportTop) {
// First change is above previous viewport - need full re-render
fullRender(true);
return;
}
@ -1088,7 +1099,6 @@ export class TUI extends Container {
this.previousLines = newLines;
this.previousWidth = width;
this.previousHeight = height;
}
/**