mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(tui): Experimental support for limited repainting
This commit is contained in:
parent
34f3719a94
commit
1bf40a09c8
10 changed files with 229 additions and 9 deletions
|
|
@ -47,7 +47,7 @@ export const loadOpenRouterOAuth = async (): Promise<OAuthAuth> => {
|
|||
if (bundledLoaders) return bundledLoaders.openrouter();
|
||||
return ((await importOAuthModule("./openrouter.ts")) as { openRouterOAuth: OAuthAuth }).openRouterOAuth;
|
||||
};
|
||||
|
||||
|
||||
export const loadKimiCodingOAuth = async (): Promise<OAuthAuth> => {
|
||||
if (bundledLoaders) return bundledLoaders.kimiCoding();
|
||||
return ((await importOAuthModule("./kimi-coding.ts")) as { kimiCodingOAuth: OAuthAuth }).kimiCodingOAuth;
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ Keep `retry.provider.maxRetries` at `0` unless provider-level retries are explic
|
|||
| `terminal.showImages` | boolean | `true` | Show images in terminal (if supported) |
|
||||
| `terminal.imageWidthCells` | number | `60` | Preferred inline image width in terminal cells |
|
||||
| `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when content shrinks (can cause flicker) |
|
||||
| `terminal.limitedRepaint` | number | - | Maximum recent rows emitted during a full repaint. The visible viewport is always repainted, and incremental scrollback remains unbounded |
|
||||
| `images.autoResize` | boolean | `true` | Resize images to 2000x2000 max |
|
||||
| `images.blockImages` | boolean | `false` | Block all images from being sent to LLM |
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export async function createStartupTui(settingsManager: SettingsManager): Promis
|
|||
setKeybindings(KeybindingsManager.create());
|
||||
const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor(), getAgentDir());
|
||||
ui.setClearOnShrink(settingsManager.getClearOnShrink());
|
||||
ui.setLimitedRepaint(settingsManager.getLimitedRepaint());
|
||||
return ui;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export interface TerminalSettings {
|
|||
showImages?: boolean; // default: true (only relevant if terminal supports images)
|
||||
imageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)
|
||||
clearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)
|
||||
limitedRepaint?: number; // max rows emitted during full repaints; default: unlimited
|
||||
showTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators)
|
||||
}
|
||||
|
||||
|
|
@ -1107,6 +1108,24 @@ export class SettingsManager {
|
|||
this.save();
|
||||
}
|
||||
|
||||
getLimitedRepaint(): number | undefined {
|
||||
const maxLines = this.settings.terminal?.limitedRepaint;
|
||||
if (typeof maxLines !== "number" || !Number.isFinite(maxLines) || maxLines <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.floor(maxLines);
|
||||
}
|
||||
|
||||
setLimitedRepaint(maxLines: number | undefined): void {
|
||||
if (!this.globalSettings.terminal) {
|
||||
this.globalSettings.terminal = {};
|
||||
}
|
||||
this.globalSettings.terminal.limitedRepaint =
|
||||
maxLines === undefined || !Number.isFinite(maxLines) || maxLines <= 0 ? undefined : Math.floor(maxLines);
|
||||
this.markModified("terminal", "limitedRepaint");
|
||||
this.save();
|
||||
}
|
||||
|
||||
getShowTerminalProgress(): boolean {
|
||||
return this.settings.terminal?.showTerminalProgress ?? false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export interface SettingsConfig {
|
|||
quietStartup: boolean;
|
||||
defaultProjectTrust: DefaultProjectTrust;
|
||||
clearOnShrink: boolean;
|
||||
limitedRepaint: number | undefined;
|
||||
showTerminalProgress: boolean;
|
||||
warnings: WarningSettings;
|
||||
}
|
||||
|
|
@ -109,6 +110,7 @@ export interface SettingsCallbacks {
|
|||
onQuietStartupChange: (enabled: boolean) => void;
|
||||
onDefaultProjectTrustChange: (defaultProjectTrust: DefaultProjectTrust) => void;
|
||||
onClearOnShrinkChange: (enabled: boolean) => void;
|
||||
onLimitedRepaintChange: (maxLines: number | undefined) => void;
|
||||
onShowTerminalProgressChange: (enabled: boolean) => void;
|
||||
onWarningsChange: (warnings: WarningSettings) => void;
|
||||
onCancel: () => void;
|
||||
|
|
@ -718,9 +720,19 @@ export class SettingsSelectorComponent extends Container {
|
|||
values: ["true", "false"],
|
||||
});
|
||||
|
||||
// Terminal progress toggle (insert after clear-on-shrink)
|
||||
// Limited repaint setting (insert after clear-on-shrink)
|
||||
const clearOnShrinkIndex = items.findIndex((item) => item.id === "clear-on-shrink");
|
||||
items.splice(clearOnShrinkIndex + 1, 0, {
|
||||
id: "limited-repaint",
|
||||
label: "Limited repaint",
|
||||
description: "Limit full repaints to recent rows; incremental scrollback remains unbounded",
|
||||
currentValue: config.limitedRepaint === undefined ? "off" : String(config.limitedRepaint),
|
||||
values: ["off", "100", "500", "1000", "5000"],
|
||||
});
|
||||
|
||||
// Terminal progress toggle (insert after limited-repaint)
|
||||
const limitedRepaintIndex = items.findIndex((item) => item.id === "limited-repaint");
|
||||
items.splice(limitedRepaintIndex + 1, 0, {
|
||||
id: "terminal-progress",
|
||||
label: "Terminal progress",
|
||||
description: "Show OSC 9;4 progress indicators in the terminal tab bar",
|
||||
|
|
@ -816,6 +828,9 @@ export class SettingsSelectorComponent extends Container {
|
|||
case "clear-on-shrink":
|
||||
callbacks.onClearOnShrinkChange(newValue === "true");
|
||||
break;
|
||||
case "limited-repaint":
|
||||
callbacks.onLimitedRepaintChange(newValue === "off" ? undefined : parseInt(newValue, 10));
|
||||
break;
|
||||
case "terminal-progress":
|
||||
callbacks.onShowTerminalProgressChange(newValue === "true");
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -453,6 +453,7 @@ export class InteractiveMode {
|
|||
this.version = VERSION;
|
||||
this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor(), getAgentDir());
|
||||
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
||||
this.ui.setLimitedRepaint(this.settingsManager.getLimitedRepaint());
|
||||
this.headerContainer = new Container();
|
||||
this.loadedResourcesContainer = new Container();
|
||||
this.chatContainer = new Container();
|
||||
|
|
@ -1711,6 +1712,7 @@ export class InteractiveMode {
|
|||
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
||||
const clearOnShrink = this.settingsManager.getClearOnShrink();
|
||||
this.ui.setClearOnShrink(clearOnShrink);
|
||||
this.ui.setLimitedRepaint(this.settingsManager.getLimitedRepaint());
|
||||
if (!clearOnShrink && !this.activeStatusIndicator) {
|
||||
this.statusContainer.clear();
|
||||
}
|
||||
|
|
@ -4140,6 +4142,7 @@ export class InteractiveMode {
|
|||
autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
|
||||
quietStartup: this.settingsManager.getQuietStartup(),
|
||||
clearOnShrink: this.settingsManager.getClearOnShrink(),
|
||||
limitedRepaint: this.settingsManager.getLimitedRepaint(),
|
||||
showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
|
||||
warnings: this.settingsManager.getWarnings(),
|
||||
},
|
||||
|
|
@ -4274,6 +4277,10 @@ export class InteractiveMode {
|
|||
this.statusContainer.clear();
|
||||
}
|
||||
},
|
||||
onLimitedRepaintChange: (maxLines) => {
|
||||
this.settingsManager.setLimitedRepaint(maxLines);
|
||||
this.ui.setLimitedRepaint(maxLines);
|
||||
},
|
||||
onShowTerminalProgressChange: (enabled) => {
|
||||
this.settingsManager.setShowTerminalProgress(enabled);
|
||||
},
|
||||
|
|
@ -5348,6 +5355,7 @@ export class InteractiveMode {
|
|||
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
||||
const clearOnShrink = this.settingsManager.getClearOnShrink();
|
||||
this.ui.setClearOnShrink(clearOnShrink);
|
||||
this.ui.setLimitedRepaint(this.settingsManager.getLimitedRepaint());
|
||||
if (!clearOnShrink && !this.activeStatusIndicator) {
|
||||
this.statusContainer.clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -420,6 +420,29 @@ describe("SettingsManager", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("limitedRepaint", () => {
|
||||
it("defaults to unlimited and persists a positive row limit", async () => {
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
expect(manager.getLimitedRepaint()).toBeUndefined();
|
||||
|
||||
manager.setLimitedRepaint(500.9);
|
||||
await manager.flush();
|
||||
|
||||
expect(manager.getLimitedRepaint()).toBe(500);
|
||||
const savedSettings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
|
||||
expect(savedSettings.terminal.limitedRepaint).toBe(500);
|
||||
});
|
||||
|
||||
it("treats invalid row limits as unlimited", () => {
|
||||
expect(SettingsManager.inMemory({ terminal: { limitedRepaint: 0 } }).getLimitedRepaint()).toBeUndefined();
|
||||
expect(SettingsManager.inMemory({ terminal: { limitedRepaint: -10 } }).getLimitedRepaint()).toBeUndefined();
|
||||
expect(
|
||||
SettingsManager.inMemory({ terminal: { limitedRepaint: Number.POSITIVE_INFINITY } }).getLimitedRepaint(),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shellCommandPrefix", () => {
|
||||
it("should load shellCommandPrefix from settings", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ type ReloadCommandContext = {
|
|||
getAutocompleteMaxVisible: () => number;
|
||||
getShowHardwareCursor: () => boolean;
|
||||
getClearOnShrink: () => boolean;
|
||||
getLimitedRepaint: () => number | undefined;
|
||||
};
|
||||
keybindings: { reload: () => void };
|
||||
customHeader?: unknown;
|
||||
|
|
@ -112,6 +113,7 @@ type ReloadCommandContext = {
|
|||
requestRender: (force?: boolean) => void;
|
||||
setShowHardwareCursor: (enabled: boolean) => void;
|
||||
setClearOnShrink: (enabled: boolean) => void;
|
||||
setLimitedRepaint: (maxLines: number | undefined) => void;
|
||||
};
|
||||
editor: unknown;
|
||||
defaultEditor: { setPaddingX: (padding: number) => void; setAutocompleteMaxVisible: (maxVisible: number) => void };
|
||||
|
|
@ -174,6 +176,7 @@ function createReloadCommandContext(overrides: ReloadCommandContextOverrides = {
|
|||
getAutocompleteMaxVisible: () => 10,
|
||||
getShowHardwareCursor: () => false,
|
||||
getClearOnShrink: () => false,
|
||||
getLimitedRepaint: () => undefined,
|
||||
...overrides.settingsManager,
|
||||
},
|
||||
keybindings: { reload: () => {}, ...overrides.keybindings },
|
||||
|
|
@ -183,6 +186,7 @@ function createReloadCommandContext(overrides: ReloadCommandContextOverrides = {
|
|||
requestRender: () => {},
|
||||
setShowHardwareCursor: () => {},
|
||||
setClearOnShrink: () => {},
|
||||
setLimitedRepaint: () => {},
|
||||
...overrides.ui,
|
||||
},
|
||||
editor,
|
||||
|
|
|
|||
|
|
@ -311,6 +311,8 @@ export class TUI extends Container {
|
|||
private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
|
||||
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 limitedRepaint: number | undefined;
|
||||
private renderedBufferStart = 0; // First logical line retained after the last full repaint
|
||||
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;
|
||||
|
|
@ -365,6 +367,19 @@ export class TUI extends Container {
|
|||
this.clearOnShrink = enabled;
|
||||
}
|
||||
|
||||
getLimitedRepaint(): number | undefined {
|
||||
return this.limitedRepaint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit full repaints to the most recent number of rendered rows.
|
||||
* The visible viewport is always repainted in full, and differential appends remain unbounded.
|
||||
*/
|
||||
setLimitedRepaint(maxLines: number | undefined): void {
|
||||
this.limitedRepaint =
|
||||
maxLines === undefined || !Number.isFinite(maxLines) || maxLines <= 0 ? undefined : Math.floor(maxLines);
|
||||
}
|
||||
|
||||
setFocus(component: Component | null): void {
|
||||
this.setFocusInternal({ component, overlayFocusRestore: "clear" });
|
||||
}
|
||||
|
|
@ -1139,6 +1154,21 @@ export class TUI extends Container {
|
|||
return reservedRows;
|
||||
}
|
||||
|
||||
private getLimitedRepaintStart(lines: string[], height: number): number {
|
||||
if (this.limitedRepaint === undefined) return 0;
|
||||
|
||||
let start = Math.max(0, lines.length - Math.max(height, this.limitedRepaint));
|
||||
for (let i = 0; i < start; i++) {
|
||||
if (!isImageLine(lines[i] ?? "") || extractKittyImageRows(lines[i] ?? "") <= 1) continue;
|
||||
const blockEnd = i + this.getKittyImageReservedRows(lines, i) - 1;
|
||||
if (blockEnd >= start) {
|
||||
start = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
private expandChangedRangeForKittyImages(
|
||||
firstChanged: number,
|
||||
lastChanged: number,
|
||||
|
|
@ -1287,13 +1317,14 @@ export class TUI extends Container {
|
|||
// Helper to clear scrollback and viewport and render all new lines
|
||||
const fullRender = (clear: boolean): void => {
|
||||
this.fullRedrawCount += 1;
|
||||
const renderStart = clear ? this.getLimitedRepaintStart(newLines, height) : 0;
|
||||
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
||||
if (clear) {
|
||||
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
||||
buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback
|
||||
}
|
||||
for (let i = 0; i < newLines.length; i++) {
|
||||
if (i > 0) buffer += "\r\n";
|
||||
for (let i = renderStart; i < newLines.length; i++) {
|
||||
if (i > renderStart) buffer += "\r\n";
|
||||
const line = newLines[i];
|
||||
const isImage = isImageLine(line);
|
||||
const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i) : 1;
|
||||
|
|
@ -1323,7 +1354,8 @@ export class TUI extends Container {
|
|||
this.previousViewportTop = Math.max(0, bufferLength - height);
|
||||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines.slice(renderStart));
|
||||
this.renderedBufferStart = renderStart;
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
};
|
||||
|
|
@ -1353,8 +1385,16 @@ 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 (heightChanged && !isTermuxSession()) {
|
||||
// In that environment, only repaint if a larger viewport would expose history omitted
|
||||
// by a previous limited repaint.
|
||||
const retainedBufferLines = this.previousLines.length - this.renderedBufferStart;
|
||||
const termuxNeedsRepaint =
|
||||
isTermuxSession() &&
|
||||
heightChanged &&
|
||||
height > this.previousHeight &&
|
||||
this.renderedBufferStart > 0 &&
|
||||
height > retainedBufferLines;
|
||||
if (heightChanged && (!isTermuxSession() || termuxNeedsRepaint)) {
|
||||
logRedraw(`terminal height changed (${this.previousHeight} -> ${height})`);
|
||||
fullRender(true);
|
||||
return;
|
||||
|
|
@ -1448,7 +1488,7 @@ export class TUI extends Container {
|
|||
}
|
||||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines.slice(this.renderedBufferStart));
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
this.previousViewportTop = prevViewportTop;
|
||||
|
|
@ -1619,7 +1659,7 @@ export class TUI extends Container {
|
|||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines.slice(this.renderedBufferStart));
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -426,6 +426,115 @@ describe("TUI resize handling", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("TUI limited repaint", () => {
|
||||
it("limits full repaint output while leaving later appends unbounded", async () => {
|
||||
const terminal = new LoggingVirtualTerminal(40, 5);
|
||||
const tui = new TUI(terminal);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
component.lines = Array.from({ length: 20 }, (_, i) => `row-${String(i).padStart(3, "0")}|`);
|
||||
tui.start();
|
||||
await terminal.waitForRender();
|
||||
|
||||
tui.setLimitedRepaint(7);
|
||||
terminal.clearWrites();
|
||||
tui.requestRender(true);
|
||||
await terminal.waitForRender();
|
||||
|
||||
const repaintWrites = terminal.getWrites();
|
||||
assert.ok(repaintWrites.includes("\x1b[2J\x1b[H\x1b[3J"), "limited repaint should still clear scrollback");
|
||||
assert.ok(!repaintWrites.includes("row-012|"), "rows before the repaint limit should not be emitted");
|
||||
assert.ok(repaintWrites.includes("row-013|"), "the configured repaint tail should be emitted");
|
||||
assert.ok(repaintWrites.includes("row-019|"), "the latest row should be emitted");
|
||||
|
||||
const redrawsBeforeHiddenChange = tui.fullRedraws;
|
||||
terminal.clearWrites();
|
||||
component.lines[0] = "changed-row-000|";
|
||||
tui.requestRender();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const hiddenChangeWrites = terminal.getWrites();
|
||||
assert.ok(tui.fullRedraws > redrawsBeforeHiddenChange, "a hidden change should trigger a full repaint");
|
||||
assert.ok(!hiddenChangeWrites.includes("changed-row-000|"), "limited repaint should still omit old changed rows");
|
||||
assert.ok(hiddenChangeWrites.includes("row-013|"), "limited repaint should reconstruct the retained tail");
|
||||
|
||||
const redrawsAfterRepaint = tui.fullRedraws;
|
||||
terminal.clearWrites();
|
||||
component.lines.push("row-020|");
|
||||
tui.requestRender();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const appendWrites = terminal.getWrites();
|
||||
assert.strictEqual(tui.fullRedraws, redrawsAfterRepaint, "append should stay on the differential path");
|
||||
assert.ok(!appendWrites.includes("\x1b[2J"), "append should not clear the terminal");
|
||||
assert.ok(appendWrites.includes("row-020|"), "append should emit the new row");
|
||||
assert.deepStrictEqual(terminal.getViewport(), ["row-016|", "row-017|", "row-018|", "row-019|", "row-020|"]);
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
|
||||
it("repaints omitted rows when a Termux viewport grows beyond the retained buffer", async () => {
|
||||
await withEnv({ TERMUX_VERSION: "1" }, async () => {
|
||||
const terminal = new LoggingVirtualTerminal(40, 5);
|
||||
const tui = new TUI(terminal);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
component.lines = Array.from({ length: 20 }, (_, i) => `row-${String(i).padStart(3, "0")}|`);
|
||||
tui.start();
|
||||
await terminal.waitForRender();
|
||||
|
||||
tui.setLimitedRepaint(5);
|
||||
tui.requestRender(true);
|
||||
await terminal.waitForRender();
|
||||
const redrawsBeforeResize = tui.fullRedraws;
|
||||
terminal.clearWrites();
|
||||
|
||||
terminal.resize(40, 8);
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.ok(tui.fullRedraws > redrawsBeforeResize, "larger viewport should trigger a limited repaint");
|
||||
assert.ok(terminal.getWrites().includes("row-012|"), "newly visible rows should be reconstructed");
|
||||
assert.deepStrictEqual(terminal.getViewport(), [
|
||||
"row-012|",
|
||||
"row-013|",
|
||||
"row-014|",
|
||||
"row-015|",
|
||||
"row-016|",
|
||||
"row-017|",
|
||||
"row-018|",
|
||||
"row-019|",
|
||||
]);
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
});
|
||||
|
||||
it("always repaints at least the visible viewport", async () => {
|
||||
const terminal = new LoggingVirtualTerminal(40, 5);
|
||||
const tui = new TUI(terminal);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
component.lines = Array.from({ length: 20 }, (_, i) => `row-${String(i).padStart(3, "0")}|`);
|
||||
tui.start();
|
||||
await terminal.waitForRender();
|
||||
|
||||
tui.setLimitedRepaint(2);
|
||||
terminal.clearWrites();
|
||||
tui.requestRender(true);
|
||||
await terminal.waitForRender();
|
||||
|
||||
const writes = terminal.getWrites();
|
||||
assert.ok(!writes.includes("row-014|"), "rows above the viewport should not be emitted");
|
||||
assert.ok(writes.includes("row-015|"), "the complete viewport should be emitted");
|
||||
assert.deepStrictEqual(terminal.getViewport(), ["row-015|", "row-016|", "row-017|", "row-018|", "row-019|"]);
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TUI content shrinkage", () => {
|
||||
it("clears empty rows when content shrinks significantly", async () => {
|
||||
const terminal = new VirtualTerminal(40, 10);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue