// Agent Cockpit - full-screen agent view with live terminal, CPU/memory sparklines, self-healing ring import { store } from "../core/state"; import { escapeHtml, formatDuration, fmtNum } from "../core/helpers"; import { icon } from "../design/icons"; import { colors } from "../design/tokens"; import { renderSparkline } from "../components/charts/sparkline"; import { renderSVGDonut } from "../components/charts/donut"; import { LiveTerminal } from "../components/terminal"; import { SSEClient } from "../core/sse"; import * as api from "../core/api"; import type { FleetState, View, PipelineInfo } from "../types/api"; let terminal: LiveTerminal | null = null; let sseClient: SSEClient | null = null; let selectedAgent: number | null = null; let cpuHistory: number[] = []; let memHistory: number[] = []; function renderCockpit(data: FleetState): void { const container = document.getElementById("panel-agent-cockpit"); if (!container) return; const pipelines = data.pipelines || []; if (pipelines.length === 0 && !selectedAgent) { container.innerHTML = `
${icon("cpu", 48)}

No active agents

`; return; } // Agent selector bar let html = '
'; html += '
'; for (const p of pipelines) { const isSelected = selectedAgent === p.issue; const statusDot = p.status === "failed" ? "offline" : "online"; html += ``; } html += "
"; if (selectedAgent) { const pipeline = pipelines.find((p) => p.issue === selectedAgent); if (pipeline) { html += '
'; // Top metrics row html += '
'; // CPU sparkline html += '
'; html += `
${icon("cpu", 16)} CPU
`; html += `
${cpuHistory.length > 1 ? renderSparkline(cpuHistory, colors.accent.cyan, 160, 40) : '\u2014'}
`; html += "
"; // Memory sparkline html += '
'; html += `
${icon("memory-stick", 16)} Memory
`; html += `
${memHistory.length > 1 ? renderSparkline(memHistory, colors.accent.purple, 160, 40) : '\u2014'}
`; html += "
"; // Self-healing ring const healthPct = pipeline.status === "failed" ? 0 : Math.min( 100, ((pipeline.iteration || 0) / (pipeline.maxIterations || 20)) * 100, ); html += '
'; html += `
${icon("shield-alert", 16)} Health
`; html += `
${renderSVGDonut(100 - healthPct)}
`; html += "
"; // Stage + status html += '
'; html += `
${icon("activity", 16)} Status
`; html += '
'; html += `
Stage: ${escapeHtml(pipeline.stage)}
`; html += `
Iteration: ${pipeline.iteration || 0}/${pipeline.maxIterations || 20}
`; html += `
Elapsed: ${formatDuration(pipeline.elapsed_s)}
`; if (pipeline.linesWritten != null) html += `
Lines: ${fmtNum(pipeline.linesWritten)}
`; html += "
"; html += "
"; // cockpit-metrics // Live Changes panel html += '
'; html += `
${icon("file-diff", 16)} Files Changed
`; html += '
'; html += "
"; // Live terminal html += '
'; html += "
"; // cockpit-main } else { html += `

Agent #${selectedAgent} no longer active

`; } } else { html += `
${icon("terminal", 32)}

Select an agent to monitor

`; } html += "
"; container.innerHTML = html; // Wire up agent selector container.querySelectorAll(".cockpit-agent-btn").forEach((btn) => { btn.addEventListener("click", () => { const issue = parseInt(btn.getAttribute("data-issue") || "0", 10); if (issue) selectAgent(issue, data); }); }); // Initialize terminal if (selectedAgent) { const termContainer = document.getElementById("cockpit-terminal-container"); if (termContainer) { terminal = new LiveTerminal(termContainer); connectAgentStream(selectedAgent); } // Load live changes loadCockpitChanges(selectedAgent); const refreshBtn = document.getElementById("cockpit-refresh-diff"); if (refreshBtn) { refreshBtn.addEventListener("click", () => { if (selectedAgent) loadCockpitChanges(selectedAgent); }); } // Update resource histories from agent heartbeat data const agents = data.agents || []; const agentInfo = agents.find((a) => a.issue === selectedAgent); if (agentInfo) { if (agentInfo.cpu_pct != null) { cpuHistory.push(agentInfo.cpu_pct); if (cpuHistory.length > 60) cpuHistory.shift(); } if (agentInfo.memory_mb != null) { memHistory.push(agentInfo.memory_mb); if (memHistory.length > 60) memHistory.shift(); } } } } function selectAgent(issue: number, data: FleetState): void { if (sseClient) sseClient.close(); if (terminal) terminal.destroy(); selectedAgent = issue; cpuHistory = []; memHistory = []; renderCockpit(data); } function loadCockpitChanges(issue: number): void { const body = document.getElementById("cockpit-changes-body"); if (!body) return; body.innerHTML = '

Loading...

'; api .fetchPipelineFiles(issue) .then((data) => { const files = data.files || []; if (files.length === 0) { body.innerHTML = '

No changes yet

'; return; } let html = ""; for (const f of files) { const statusCls = f.status === "added" ? "file-added" : f.status === "deleted" ? "file-deleted" : "file-modified"; const statusChar = f.status === "added" ? "A" : f.status === "deleted" ? "D" : "M"; html += `
${statusChar}${escapeHtml(f.path)}
`; } body.innerHTML = html; }) .catch(() => { body.innerHTML = '

Could not load changes

'; }); } function connectAgentStream(issue: number): void { if (sseClient) sseClient.close(); sseClient = new SSEClient( `/api/logs/${issue}/stream`, (data) => { if (terminal) terminal.append(data); }, () => { api .fetchLogs(issue) .then((data) => { if (terminal) terminal.append(data.content || "No logs available"); }) .catch(() => { if (terminal) terminal.append("Failed to load logs"); }); }, ); sseClient.connect(); } export const agentCockpitView: View = { init() {}, render(data: FleetState) { renderCockpit(data); }, destroy() { if (sseClient) { sseClient.close(); sseClient = null; } if (terminal) { terminal.destroy(); terminal = null; } selectedAgent = null; cpuHistory = []; memHistory = []; }, };