mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 08:03:39 +00:00
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:
parent
419c07fb19
commit
0925fafe3b
8 changed files with 80 additions and 48 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Clear on shrink setting**: New `terminal.clearOnShrink` setting (and `/settings` toggle) controls whether empty rows are cleared when content shrinks. Disabled by default to reduce flicker. Enable via settings or `PI_CLEAR_ON_SHRINK=1` env var.
|
||||
|
||||
## [0.51.0] - 2026-02-01
|
||||
|
||||
### Breaking Changes
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export interface RetrySettings {
|
|||
|
||||
export interface TerminalSettings {
|
||||
showImages?: boolean; // default: true (only relevant if terminal supports images)
|
||||
clearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)
|
||||
}
|
||||
|
||||
export interface ImageSettings {
|
||||
|
|
@ -628,6 +629,23 @@ export class SettingsManager {
|
|||
this.save();
|
||||
}
|
||||
|
||||
getClearOnShrink(): boolean {
|
||||
// Settings takes precedence, then env var, then default false
|
||||
if (this.settings.terminal?.clearOnShrink !== undefined) {
|
||||
return this.settings.terminal.clearOnShrink;
|
||||
}
|
||||
return process.env.PI_CLEAR_ON_SHRINK === "1";
|
||||
}
|
||||
|
||||
setClearOnShrink(enabled: boolean): void {
|
||||
if (!this.globalSettings.terminal) {
|
||||
this.globalSettings.terminal = {};
|
||||
}
|
||||
this.globalSettings.terminal.clearOnShrink = enabled;
|
||||
this.markModified("terminal", "clearOnShrink");
|
||||
this.save();
|
||||
}
|
||||
|
||||
getImageAutoResize(): boolean {
|
||||
return this.settings.images?.autoResize ?? true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,14 +90,6 @@ export class CustomMessageComponent extends Container {
|
|||
.join("\n");
|
||||
}
|
||||
|
||||
// Limit lines when collapsed
|
||||
if (!this._expanded) {
|
||||
const lines = text.split("\n");
|
||||
if (lines.length > 5) {
|
||||
text = `${lines.slice(0, 5).join("\n")}\n...`;
|
||||
}
|
||||
}
|
||||
|
||||
this.box.addChild(
|
||||
new Markdown(text, 0, 0, this.markdownTheme, {
|
||||
color: (text: string) => theme.fg("customMessageText", text),
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export interface SettingsConfig {
|
|||
editorPaddingX: number;
|
||||
autocompleteMaxVisible: number;
|
||||
quietStartup: boolean;
|
||||
clearOnShrink: boolean;
|
||||
}
|
||||
|
||||
export interface SettingsCallbacks {
|
||||
|
|
@ -60,6 +61,7 @@ export interface SettingsCallbacks {
|
|||
onEditorPaddingXChange: (padding: number) => void;
|
||||
onAutocompleteMaxVisibleChange: (maxVisible: number) => void;
|
||||
onQuietStartupChange: (enabled: boolean) => void;
|
||||
onClearOnShrinkChange: (enabled: boolean) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -312,6 +314,16 @@ export class SettingsSelectorComponent extends Container {
|
|||
values: ["3", "5", "7", "10", "15", "20"],
|
||||
});
|
||||
|
||||
// Clear on shrink toggle (insert after autocomplete-max-visible)
|
||||
const autocompleteIndex = items.findIndex((item) => item.id === "autocomplete-max-visible");
|
||||
items.splice(autocompleteIndex + 1, 0, {
|
||||
id: "clear-on-shrink",
|
||||
label: "Clear on shrink",
|
||||
description: "Clear empty rows when content shrinks (may cause flicker)",
|
||||
currentValue: config.clearOnShrink ? "true" : "false",
|
||||
values: ["true", "false"],
|
||||
});
|
||||
|
||||
// Add borders
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
|
|
@ -363,6 +375,9 @@ export class SettingsSelectorComponent extends Container {
|
|||
case "autocomplete-max-visible":
|
||||
callbacks.onAutocompleteMaxVisibleChange(parseInt(newValue, 10));
|
||||
break;
|
||||
case "clear-on-shrink":
|
||||
callbacks.onClearOnShrinkChange(newValue === "true");
|
||||
break;
|
||||
}
|
||||
},
|
||||
callbacks.onCancel,
|
||||
|
|
|
|||
|
|
@ -258,6 +258,7 @@ export class InteractiveMode {
|
|||
this.session = session;
|
||||
this.version = VERSION;
|
||||
this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
|
||||
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
||||
this.headerContainer = new Container();
|
||||
this.chatContainer = new Container();
|
||||
this.pendingMessagesContainer = new Container();
|
||||
|
|
@ -2996,6 +2997,7 @@ export class InteractiveMode {
|
|||
editorPaddingX: this.settingsManager.getEditorPaddingX(),
|
||||
autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
|
||||
quietStartup: this.settingsManager.getQuietStartup(),
|
||||
clearOnShrink: this.settingsManager.getClearOnShrink(),
|
||||
},
|
||||
{
|
||||
onAutoCompactChange: (enabled) => {
|
||||
|
|
@ -3084,6 +3086,10 @@ export class InteractiveMode {
|
|||
this.editor.setAutocompleteMaxVisible(maxVisible);
|
||||
}
|
||||
},
|
||||
onClearOnShrinkChange: (enabled) => {
|
||||
this.settingsManager.setClearOnShrink(enabled);
|
||||
this.ui.setClearOnShrink(enabled);
|
||||
},
|
||||
onCancel: () => {
|
||||
done();
|
||||
this.ui.requestRender();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Terminal height changes no longer trigger full redraws, reducing flicker on resize
|
||||
- `clearOnShrink` now defaults to `false` (use `PI_CLEAR_ON_SHRINK=1` or `setClearOnShrink(true)` to enable)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed unnecessary full redraws when appending many lines after content had previously shrunk (viewport check now uses actual previous content size instead of stale maximum)
|
||||
|
||||
## [0.51.0] - 2026-02-01
|
||||
|
||||
## [0.50.9] - 2026-02-01
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -23,31 +23,6 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num
|
|||
}
|
||||
|
||||
describe("TUI resize handling", () => {
|
||||
it("triggers full re-render when terminal height changes", async () => {
|
||||
const terminal = new VirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
component.lines = ["Line 0", "Line 1", "Line 2"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
|
||||
// Resize height
|
||||
terminal.resize(40, 15);
|
||||
await terminal.flush();
|
||||
|
||||
// Should have triggered a full redraw
|
||||
assert.ok(tui.fullRedraws > initialRedraws, "Height change should trigger full redraw");
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
assert.ok(viewport[0]?.includes("Line 0"), "Content preserved after height change");
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
|
||||
it("triggers full re-render when terminal width changes", async () => {
|
||||
const terminal = new VirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
|
|
@ -75,6 +50,7 @@ describe("TUI content shrinkage", () => {
|
|||
it("clears empty rows when content shrinks significantly", async () => {
|
||||
const terminal = new VirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
tui.setClearOnShrink(true); // Explicitly enable (may be disabled via env var)
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
|
|
@ -106,6 +82,7 @@ describe("TUI content shrinkage", () => {
|
|||
it("handles shrink to single line", async () => {
|
||||
const terminal = new VirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
tui.setClearOnShrink(true); // Explicitly enable (may be disabled via env var)
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
|
|
@ -128,6 +105,7 @@ describe("TUI content shrinkage", () => {
|
|||
it("handles shrink to empty", async () => {
|
||||
const terminal = new VirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
tui.setClearOnShrink(true); // Explicitly enable (may be disabled via env var)
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue