feat(tui): overlay positioning API with CSS-like values

Add OverlayOptions for configurable positioning (anchor, margins, offsets,
percentages). Add OverlayHandle for programmatic visibility control with
hide/setHidden/isHidden. Add visible callback for responsive overlays.

Extension API: ctx.ui.custom() now accepts overlayOptions and onHandle callback.

Examples: overlay-qa-tests.ts (10 test commands), doom-overlay (DOOM at 35 FPS).
This commit is contained in:
Nico Bailon 2026-01-12 22:12:56 -08:00
parent d29f268f46
commit a4ccff382c
22 changed files with 1344 additions and 103 deletions

View file

@ -15,7 +15,15 @@ import type {
ThinkingLevel,
} from "@mariozechner/pi-agent-core";
import type { ImageContent, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
import type { Component, EditorComponent, EditorTheme, KeyId, OverlayOptions, TUI } from "@mariozechner/pi-tui";
import type {
Component,
EditorComponent,
EditorTheme,
KeyId,
OverlayHandle,
OverlayOptions,
TUI,
} from "@mariozechner/pi-tui";
import type { Static, TSchema } from "@sinclair/typebox";
import type { Theme } from "../../modes/interactive/theme/theme.js";
import type { BashResult } from "../bash-executor.js";
@ -116,6 +124,8 @@ export interface ExtensionUIContext {
overlay?: boolean;
/** Overlay positioning/sizing options. Can be static or a function for dynamic updates. */
overlayOptions?: OverlayOptions | (() => OverlayOptions);
/** Called with the overlay handle after the overlay is shown. Use to control visibility. */
onHandle?: (handle: OverlayHandle) => void;
},
): Promise<T>;

View file

@ -21,6 +21,7 @@ import type {
EditorComponent,
EditorTheme,
KeyId,
OverlayHandle,
OverlayOptions,
SlashCommand,
} from "@mariozechner/pi-tui";
@ -1260,7 +1261,11 @@ export class InteractiveMode {
keybindings: KeybindingsManager,
done: (result: T) => void,
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
options?: { overlay?: boolean; overlayOptions?: OverlayOptions | (() => OverlayOptions) },
options?: {
overlay?: boolean;
overlayOptions?: OverlayOptions | (() => OverlayOptions);
onHandle?: (handle: OverlayHandle) => void;
},
): Promise<T> {
const savedText = this.editor.getText();
const isOverlay = options?.overlay ?? false;
@ -1309,7 +1314,9 @@ export class InteractiveMode {
const w = (component as { width?: number }).width;
return w ? { width: w } : undefined;
};
this.ui.showOverlay(component, resolveOptions());
const handle = this.ui.showOverlay(component, resolveOptions());
// Expose handle to caller for visibility control
options?.onHandle?.(handle);
} else {
this.editorContainer.clear();
this.editorContainer.addChild(component);