mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 14:03:49 +00:00
Extension UI dialogs (select, confirm, input) now support a timeout option that auto-dismisses with a live countdown display. Simpler alternative to manually managing AbortSignal for timed dialogs. Also adds ExtensionUIDialogOptions type export and updates RPC mode to forward timeout to clients.
96 lines
2.8 KiB
TypeScript
96 lines
2.8 KiB
TypeScript
/**
|
|
* Generic selector component for extensions.
|
|
* Displays a list of string options with keyboard navigation.
|
|
*/
|
|
|
|
import { Container, getEditorKeybindings, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
|
import { theme } from "../theme/theme.js";
|
|
import { CountdownTimer } from "./countdown-timer.js";
|
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
|
|
export interface ExtensionSelectorOptions {
|
|
tui?: TUI;
|
|
timeout?: number;
|
|
}
|
|
|
|
export class ExtensionSelectorComponent extends Container {
|
|
private options: string[];
|
|
private selectedIndex = 0;
|
|
private listContainer: Container;
|
|
private onSelectCallback: (option: string) => void;
|
|
private onCancelCallback: () => void;
|
|
private titleText: Text;
|
|
private baseTitle: string;
|
|
private countdown: CountdownTimer | undefined;
|
|
|
|
constructor(
|
|
title: string,
|
|
options: string[],
|
|
onSelect: (option: string) => void,
|
|
onCancel: () => void,
|
|
opts?: ExtensionSelectorOptions,
|
|
) {
|
|
super();
|
|
|
|
this.options = options;
|
|
this.onSelectCallback = onSelect;
|
|
this.onCancelCallback = onCancel;
|
|
this.baseTitle = title;
|
|
|
|
this.addChild(new DynamicBorder());
|
|
this.addChild(new Spacer(1));
|
|
|
|
this.titleText = new Text(theme.fg("accent", title), 1, 0);
|
|
this.addChild(this.titleText);
|
|
this.addChild(new Spacer(1));
|
|
|
|
if (opts?.timeout && opts.timeout > 0 && opts.tui) {
|
|
this.countdown = new CountdownTimer(
|
|
opts.timeout,
|
|
opts.tui,
|
|
(s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)),
|
|
() => this.onCancelCallback(),
|
|
);
|
|
}
|
|
|
|
this.listContainer = new Container();
|
|
this.addChild(this.listContainer);
|
|
this.addChild(new Spacer(1));
|
|
this.addChild(new Text(theme.fg("dim", "↑↓ navigate enter select esc cancel"), 1, 0));
|
|
this.addChild(new Spacer(1));
|
|
this.addChild(new DynamicBorder());
|
|
|
|
this.updateList();
|
|
}
|
|
|
|
private updateList(): void {
|
|
this.listContainer.clear();
|
|
for (let i = 0; i < this.options.length; i++) {
|
|
const isSelected = i === this.selectedIndex;
|
|
const text = isSelected
|
|
? theme.fg("accent", "→ ") + theme.fg("accent", this.options[i])
|
|
: ` ${theme.fg("text", this.options[i])}`;
|
|
this.listContainer.addChild(new Text(text, 1, 0));
|
|
}
|
|
}
|
|
|
|
handleInput(keyData: string): void {
|
|
const kb = getEditorKeybindings();
|
|
if (kb.matches(keyData, "selectUp") || keyData === "k") {
|
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
this.updateList();
|
|
} else if (kb.matches(keyData, "selectDown") || keyData === "j") {
|
|
this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);
|
|
this.updateList();
|
|
} else if (kb.matches(keyData, "selectConfirm") || keyData === "\n") {
|
|
const selected = this.options[this.selectedIndex];
|
|
if (selected) this.onSelectCallback(selected);
|
|
} else if (kb.matches(keyData, "selectCancel")) {
|
|
this.onCancelCallback();
|
|
}
|
|
}
|
|
|
|
dispose(): void {
|
|
this.countdown?.dispose();
|
|
}
|
|
}
|