UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

107 lines (101 loc) • 3.92 kB
/** * Standalone driver for a KXM panel surface. * * `kxm dash` already owns its hub and SSE loop, so the kit does not duplicate * it. This is the small alternative for a local, file-backed surface — a config * or roster panel that repaints when an owner republishes and stops when the * operator leaves. * * A non-TTY caller gets one plain-text frame and exit code zero, which keeps * `--dry-run`, CI smoke, and piped output honest instead of pretending to be an * interactive session. */ import { ProcessTerminal, TuiAltScreen, stripTerminalSequences, type Terminal } from "@earendil-works/pi-tui"; import { KxmTuiPanelComponent } from "../tui/panelComponent.ts"; import { initialKxmTuiPanelState } from "../tui/panel.ts"; import { renderKxmTuiPanel } from "../tui/render.ts"; import type { KxmTuiRegistry } from "../services/registry.ts"; import { createDefaultKxmTuiTheme, createPlainKxmTuiTheme, type KxmTuiTheme } from "../tui/theme.ts"; export interface RunKxmTuiPanelInput { readonly registry: KxmTuiRegistry; readonly title: string; readonly breadcrumb?: string; readonly helpLines?: readonly string[]; readonly stdout: (text: string) => void; readonly terminal?: Terminal; readonly isTty?: boolean; readonly theme?: KxmTuiTheme; readonly abort?: AbortSignal; /** Repaint interval for surfaces whose owners can change outside this process. */ readonly pollMs?: number; readonly now?: () => number; } /** Draw one frame without a terminal. Used by `--dry-run` and CI smoke. */ export function renderKxmTuiPanelFrame(input: { registry: KxmTuiRegistry; title: string; theme?: KxmTuiTheme; width?: number; nowMs?: number; footer?: string; }): string { const theme = input.theme ?? createPlainKxmTuiTheme(); const lines = renderKxmTuiPanel( input.registry.getSections(), initialKxmTuiPanelState(input.registry.getSections()), theme, { title: input.title, footer: input.footer ?? "non-interactive frame", nowMs: input.nowMs ?? Date.now() }, input.width ?? 100, ); return `${lines.map((line) => stripTerminalSequences(line)).join("\n")}\n`; } /** Run a panel until it quits, the caller aborts, or one frame is enough. */ export async function runKxmTuiPanel(input: RunKxmTuiPanelInput): Promise<number> { const theme = input.theme ?? createDefaultKxmTuiTheme(); const tty = input.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY); if (!tty) { input.stdout(renderKxmTuiPanelFrame({ registry: input.registry, title: input.title, theme, ...(input.now === undefined ? {} : { nowMs: input.now() }), ...(input.breadcrumb ? { footer: input.breadcrumb } : {}), })); return 0; } const terminal = input.terminal ?? new ProcessTerminal(); const tui = new TuiAltScreen(terminal, false, undefined, { mouse: true }); let timer: ReturnType<typeof setInterval> | undefined; let stopped = false; let finished: () => void = () => undefined; const done = new Promise<void>((resolve) => { finished = resolve; }); const stop = (): void => { if (stopped) return; stopped = true; if (timer) clearInterval(timer); input.abort?.removeEventListener("abort", stop); panel.dispose(); tui.stop(); finished(); }; const panel = new KxmTuiPanelComponent({ registry: input.registry, theme, title: input.title, ...(input.breadcrumb ? { breadcrumb: input.breadcrumb } : {}), ...(input.helpLines ? { helpLines: input.helpLines } : {}), requestRender: () => tui.requestRender(), onQuit: stop, getWidth: () => terminal.columns, }); input.abort?.addEventListener("abort", stop, { once: true }); tui.setLayoutRoot(panel.root); tui.setFocus(panel); tui.start(); timer = setInterval(() => panel.refresh(), input.pollMs ?? 2_000); timer.unref?.(); await done; return stopped && input.abort?.aborted ? 130 : 0; }