/** * Dashboard TUI component for /workflows command. * * Provides an interactive overlay to: * - View running and completed workflow runs * - Inspect individual step progress and output * - Cancel running workflows * - Toggle auto-refresh */ import type { Component } from "@earendil-works/pi-tui"; import type { Theme } from "@earendil-works/pi-coding-agent"; import { truncateToWidth, } from "@earendil-works/pi-tui"; import type { WorkflowProgress, StepProgress, DashboardState } from "./types.ts"; import { listRuns, loadProgress } from "./artifacts.ts"; import { getActiveRuns, type ActiveRun } from "./cleanup.ts"; // ── Rendering Helpers ─────────────────────────────────────────────────────────── function formatTime(ts: number | null): string { if (!ts) return "--"; return new Date(ts).toLocaleTimeString(); } function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60000)}m${Math.round((ms % 60000) / 1000)}s`; } function statusIcon(status: string, theme: any): string { const fg = theme.fg || ((_: string, t: string) => t); switch (status) { case "running": return fg("warning", "⟳"); case "completed": return fg("success", "✓"); case "failed": return fg("error", "✗"); case "cancelled": return fg("dim", "✕"); default: return fg("dim", "○"); } } function stepStatusIcon(status: string, theme: any): string { const fg = theme.fg || ((_: string, t: string) => t); switch (status) { case "running": return fg("warning", "▸"); case "completed": return fg("success", "✓"); case "failed": return fg("error", "✗"); case "cancelled": return fg("dim", "✕"); default: return fg("dim", "·"); } } function formatTokens(n: number): string { if (n < 1000) return String(n); if (n < 10000) return `${(n / 1000).toFixed(1)}k`; return `${Math.round(n / 1000)}k`; } // ── Dashboard Component ───────────────────────────────────────────────────────── export class DashboardComponent implements Component { private state: DashboardState; private theme: any; private width = 80; private needsRefresh = true; constructor(state: DashboardState, theme: any) { this.state = state; this.theme = theme; if (state.autoRefresh) { this.state.refreshInterval = setInterval(() => { this.refreshRuns(); }, 1_500); } } private refreshRuns(): void { const activeMap = new Map(); for (const run of getActiveRuns()) { activeMap.set(run.runId, run.progress); } const saved = listRuns() .filter((id: string) => !activeMap.has(id)) .map((id: string) => ({ runId: id, progress: loadProgress(id) })); const active = Array.from(activeMap.entries()).map(([runId, progress]: [string, WorkflowProgress]) => ({ runId, progress, })); this.state.runs = [...active, ...saved]; if (this.state.selectedIndex >= this.state.runs.length) { this.state.selectedIndex = Math.max(0, this.state.runs.length - 1); } this.needsRefresh = true; } render(width: number, _height?: number): string[] { this.width = width; if (this.state.autoRefresh) { this.refreshRuns(); } const lines: string[] = []; const theme = this.theme; const fg = theme.fg || ((_: string, t: string) => t); const bold = theme.bold || ((t: string) => t); const innerW = Math.max(1, width - 4); // Header lines.push(fg("accent", bold("Workflow Dashboard"))); lines.push(fg("dim", `${"─".repeat(Math.max(0, innerW - 2))}`)); if (this.state.runs.length === 0) { lines.push(""); lines.push(fg("dim", " No workflow runs found.")); lines.push(fg("dim", " Use the workflow tool or /workflows run