import type { RunState, NodeInstanceState } from "./types.ts"; export interface LoopWidgetOptions { /** Number of columns used for node progress. Defaults to 2. */ columns?: number; /** Animation frame for running-node loaders. Defaults to Date.now-derived frame. */ frame?: number; /** Whether to show attempt count when a node has retried. Defaults to true. */ showAttempts?: boolean; } /** * Build the standard extension-level loop progress widget. * * The renderer is intentionally generic: it uses node labels from the loop * definition and never hardcodes workflow-specific step names. Running nodes use * a 4-dot loader where 3 highlighted dots move through the 4 positions. */ export function renderLoopWidget(run: RunState, options: LoopWidgetOptions = {}): string[] { const columns = Math.max(1, options.columns ?? 2); const frame = options.frame ?? Math.floor(Date.now() / 350); const showAttempts = options.showAttempts ?? true; const completed = run.nodes.filter((node) => node.status === "completed").length; const header = `Pi Loop: ${run.loopId} · ${run.status} · ${completed}/${run.nodes.length}`; const nodeLines = run.nodes.map((node, index) => renderNodeCell(node, index, frame, showAttempts)); const rows: string[] = [header, ""]; for (let i = 0; i < nodeLines.length; i += columns) { rows.push(nodeLines.slice(i, i + columns).map((line) => line.padEnd(36)).join(" ").trimEnd()); } return rows; } function renderNodeCell(node: NodeInstanceState, index: number, frame: number, showAttempts: boolean): string { const marker = renderNodeMarker(node, frame); const attempts = showAttempts && node.attempts.length > 1 ? ` (${node.attempts.length})` : ""; return `${String(index + 1).padStart(2, " ")} ${marker} ${node.nodeDef.label}${attempts}`; } function renderNodeMarker(node: NodeInstanceState, frame: number): string { switch (node.status) { case "completed": return "✓"; case "failed": return "✗"; case "skipped": return "-"; case "running": return renderFourDotLoader(frame); case "pending": default: return "○"; } } function renderFourDotLoader(frame: number): string { const emptyIndex = (3 - (((frame % 4) + 4) % 4) + 4) % 4; let out = ""; for (let i = 0; i < 4; i++) { out += i === emptyIndex ? "·" : "●"; } return out; }