import { formatUsd } from "./status-widget.js"; import { isTerminal } from "../controller/state-machine.js"; import type { UltraController } from "../controller/controller.js"; import type { ControllerState } from "../controller/state-machine.js"; import type { ChecklistItem, ProviderHealth } from "../types.js"; export const PANEL_KEY = "ultrapi"; /** Pi truncates a widget past this, so the panel budgets its own lines rather than being cut. */ export const PANEL_MAX_LINES = 10; const MARK: Record = { done: "✔", active: "▸", blocked: "✗", pending: " " }; function healthLine(health: ProviderHealth, spentUsd: number): string { const faults = [health.recent429 ? `429×${health.recent429}` : "", health.recent5xx ? `5xx×${health.recent5xx}` : ""].filter(Boolean); const cooling = health.cooldownUntil ? ` · cooling until ${health.cooldownUntil.slice(11, 19)}` : ""; return `${health.state} · ${health.concurrency} parallel${faults.length ? ` · ${faults.join(" ")}` : ""}${cooling} · est. ${formatUsd(spentUsd)}`; } /** * The live fleet as a panel: which step of the run is in progress, who owns it, and what the * delegated agents under it are doing. * * The step names are already in the ledger — `plannedChecklist` writes four to six titled, owned * steps per topology — and until now the whole list was collapsed to a `2/5` counter. Provider * health is the same story: latency, throttle counts and concurrency are all computed and then * discarded at the display boundary. * * Returns undefined when nothing is running, which is what tells the caller to clear the widget. */ export function panelLines(controller: UltraController, maxLines = PANEL_MAX_LINES): string[] | undefined { const status = controller.status(); const live = status.activeRuns.filter((run) => !isTerminal(run.state as ControllerState)); const run = live.at(-1); if (!run) return undefined; const queued = live.length > 1 ? ` +${live.length - 1} more` : ""; const header = `UltraPi · ${run.topology} · ${run.runId.slice(0, 8)}${queued}`; const footer = healthLine(status.health, status.spentUsd); const checklist = run.checklist; const active = checklist.findIndex((item) => item.state === "active"); const nodes = run.nodes.filter((node) => node.state !== "done"); // The step rows and the node rows compete for the same ten lines. Steps win, because a run with // no visible stage is unreadable; nodes are shown for as much room as is left. const steps = checklist.length ? checklist : [{ id: run.state, title: run.state, owner: "root", state: "active" as const }]; const room = Math.max(0, maxLines - 2 - steps.length); const width = Math.max(0, ...steps.map((item) => item.title.length)); const nodeWidth = Math.max(0, ...nodes.map((node) => node.nodeId.length)); const roleWidth = Math.max(0, ...nodes.map((node) => node.role.length)); const nodeLines = nodes.map((node) => ` ${node.nodeId.padEnd(nodeWidth)} ${node.role.padEnd(roleWidth)} ${node.model ?? "model unselected"} ${node.credits.toFixed(2)} cr ${node.state}`); // The overflow note costs a line of the same budget, so it is counted rather than added on top. const kept = nodeLines.length > room ? Math.max(0, room - 1) : nodeLines.length; const body = (nodeLines.length > kept ? [...nodeLines.slice(0, kept), ` +${nodeLines.length - kept} more agents`] : nodeLines).slice(0, room); return [ header, ...steps.map((item, index) => { const progress = index === active && run.progress.total ? ` ${run.progress.done}/${run.progress.total}` : ""; return `${MARK[item.state]} ${item.title.padEnd(width)} ${item.owner}${progress}`; }), ...body, footer, ]; } export interface PanelSurface { setWidget?: (key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }) => void } /** * Paints the panel, and clears it when the last run settles. * * Like the status line, this can never throw. A panel exists to say a run is alive; one that can * end the run it reports on is worse than none, so a host without `setWidget`, a controller that is * not ready, and a renderer that rejects the content are all swallowed. It also only writes when * the content changes, so an idle session never touches the surface at all. */ export function createPanelPainter(controller: UltraController): (ui: PanelSurface | undefined) => void { let painted: string | undefined; return (ui) => { try { if (typeof ui?.setWidget !== "function") return; const lines = panelLines(controller); const key = lines?.join("\n"); if (key === painted) return; painted = key; ui.setWidget(PANEL_KEY, lines, { placement: "aboveEditor" }); } catch {} }; } export function clearPanel(ui: PanelSurface | undefined): void { try { ui?.setWidget?.(PANEL_KEY, undefined); } catch {} }