// Pipeline Theater - live terminal + animated stage rail + token burn meter + file accumulator import { store } from "../core/state"; import { escapeHtml, formatDuration, fmtNum } from "../core/helpers"; import { icon } from "../design/icons"; import { colors, STAGES, STAGE_HEX, STAGE_SHORT } from "../design/tokens"; import { LiveTerminal } from "../components/terminal"; import { SSEClient } from "../core/sse"; import { renderPipelineSVG } from "../components/charts/pipeline-rail"; 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 selectedIssue: number | null = null; function renderTheater(data: FleetState): void { const container = document.getElementById("panel-pipeline-theater"); if (!container) return; const pipelines = data.pipelines || []; if (pipelines.length === 0 && !selectedIssue) { container.innerHTML = `
${icon("eye", 48)}

No active pipelines to observe

`; return; } // Pipeline selector let html = '
'; html += '
'; html += '
Active Pipelines
'; for (const p of pipelines) { const isSelected = selectedIssue === p.issue; html += `
` + `#${p.issue}` + `${escapeHtml(p.stage)}` + `${formatDuration(p.elapsed_s)}
`; } html += "
"; // Main theater area html += '
'; if (selectedIssue) { const pipeline = pipelines.find((p) => p.issue === selectedIssue); if (pipeline) { // Stage rail html += `
${renderPipelineSVG(pipeline)}
`; // Token burn + file accumulator html += '
'; html += `
${icon("zap", 14)} Iteration` + `${pipeline.iteration || 0}/${pipeline.maxIterations || 20}
`; if (pipeline.linesWritten != null) { html += `
${icon("file-diff", 14)} Lines` + `${fmtNum(pipeline.linesWritten)}
`; } if (pipeline.cost != null) { html += `
${icon("dollar-sign", 14)} Cost` + `$${pipeline.cost.toFixed(2)}
`; } html += "
"; // Live Changes panel (diff + files) html += '
'; html += `
${icon("file-diff", 16)} Live Changes
`; html += '

Loading changes...

'; html += "
"; // Live terminal html += '
'; } else { html += `

Pipeline #${selectedIssue} no longer active

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

Select a pipeline to observe

`; } html += "
"; container.innerHTML = html; // Wire up pipeline selection container.querySelectorAll(".theater-pipeline-item").forEach((item) => { item.addEventListener("click", () => { const issue = parseInt(item.getAttribute("data-issue") || "0", 10); if (issue) selectPipeline(issue, data); }); }); // Initialize terminal and live changes if pipeline is selected if (selectedIssue) { const termContainer = document.getElementById("theater-terminal-container"); if (termContainer) { terminal = new LiveTerminal(termContainer); connectLogStream(selectedIssue); } loadLiveChanges(selectedIssue); const refreshBtn = document.getElementById("theater-refresh-diff"); if (refreshBtn) { refreshBtn.addEventListener("click", () => { if (selectedIssue) loadLiveChanges(selectedIssue); }); } } } function selectPipeline(issue: number, data: FleetState): void { if (sseClient) sseClient.close(); if (terminal) terminal.destroy(); selectedIssue = issue; renderTheater(data); } function loadLiveChanges(issue: number): void { const body = document.getElementById("theater-changes-body"); if (!body) return; Promise.all([ api.fetchPipelineFiles(issue).catch(() => ({ files: [] })), api.fetchPipelineDiff(issue).catch(() => ({ diff: "", stats: { files_changed: 0, insertions: 0, deletions: 0 }, worktree: "", })), ]).then(([filesData, diffData]) => { const files = filesData.files || []; const stats = diffData.stats; let html = ""; // Stats summary if (stats.files_changed > 0) { html += `
` + `${stats.files_changed} file${stats.files_changed !== 1 ? "s" : ""}` + `+${stats.insertions}` + `-${stats.deletions}
`; } // File list if (files.length > 0) { 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)}
`; } html += "
"; } // Diff preview (truncated) if (diffData.diff) { const truncatedDiff = diffData.diff.length > 5000 ? diffData.diff.substring(0, 5000) + "\n... (truncated)" : diffData.diff; html += `
Show Diff
${escapeHtml(truncatedDiff)}
`; } if (!html) { html = '

No changes detected (worktree may not exist yet)

'; } body.innerHTML = html; }); } function connectLogStream(issue: number): void { if (sseClient) sseClient.close(); // Try SSE endpoint first, fall back to static logs sseClient = new SSEClient( `/api/logs/${issue}/stream`, (data) => { if (terminal) terminal.append(data); }, () => { // SSE not available, load static logs 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 pipelineTheaterView: View = { init() {}, render(data: FleetState) { renderTheater(data); }, destroy() { if (sseClient) { sseClient.close(); sseClient = null; } if (terminal) { terminal.destroy(); terminal = null; } selectedIssue = null; }, };