feat(tui): add overlay compositing for ctx.ui.custom() (#558)

Adds overlay rendering capability to the TUI, enabling floating modal
components that render on top of existing content without clearing the screen.

- Add showOverlay(), hideOverlay(), hasOverlay() methods to TUI
- Implement ANSI-aware line compositing via extractSegments()
- Support overlay stack (multiple overlays, later on top)
- Add { overlay: true } option to ctx.ui.custom()
- Add overlay-test.ts example extension

Also fixes pre-existing bug where bash tool output cached visual lines
at fixed terminal width, causing crashes on terminal resize.

Co-authored-by: Nico Bailon <nico.bailon@gmail.com>
This commit is contained in:
Mario Zechner 2026-01-08 22:40:42 +01:00
parent 121823c74d
commit f9064c2f69
8 changed files with 488 additions and 48 deletions

View file

@ -355,24 +355,29 @@ export class ToolExecutionComponent extends Container {
// Show all lines when expanded
this.contentBox.addChild(new Text(`\n${styledOutput}`, 0, 0));
} else {
// Use visual line truncation when collapsed
// Box has paddingX=1, so content width = terminal.columns - 2
const { visualLines, skippedCount } = truncateToVisualLines(
`\n${styledOutput}`,
BASH_PREVIEW_LINES,
this.ui.terminal.columns - 2,
);
// Use visual line truncation when collapsed with width-aware caching
const textContent = `\n${styledOutput}`;
let cachedWidth: number | undefined;
let cachedLines: string[] | undefined;
let cachedSkipped: number | undefined;
if (skippedCount > 0) {
this.contentBox.addChild(
new Text(theme.fg("toolOutput", `\n... (${skippedCount} earlier lines)`), 0, 0),
);
}
// Add pre-rendered visual lines as a raw component
this.contentBox.addChild({
render: () => visualLines,
invalidate: () => {},
render: (width: number) => {
if (cachedLines === undefined || cachedWidth !== width) {
const result = truncateToVisualLines(textContent, BASH_PREVIEW_LINES, width);
cachedLines = result.visualLines;
cachedSkipped = result.skippedCount;
cachedWidth = width;
}
return cachedSkipped && cachedSkipped > 0
? ["", theme.fg("toolOutput", `... (${cachedSkipped} earlier lines)`), ...cachedLines]
: cachedLines;
},
invalidate: () => {
cachedWidth = undefined;
cachedLines = undefined;
cachedSkipped = undefined;
},
});
}
}