// Pipelines tab - table, filters, detail panel, artifact viewer import { store } from "../core/state"; import { escapeHtml, formatDuration, formatMarkdown } from "../core/helpers"; import { renderPipelineSVG } from "../components/charts/pipeline-rail"; import { renderLogViewer } from "../components/terminal"; import { updateBulkToolbar } from "../components/modal"; import { icon } from "../design/icons"; import { STAGE_SHORT } from "../design/tokens"; import * as api from "../core/api"; import type { FleetState, View, PipelineDetail } from "../types/api"; let localPipelineDetail: PipelineDetail | null = null; function setupPipelineFilters(): void { const chips = document.querySelectorAll("#pipeline-filters .filter-chip"); chips.forEach((chip) => { chip.addEventListener("click", () => { const filter = chip.getAttribute("data-filter") || "all"; store.set("pipelineFilter", filter); const siblings = document.querySelectorAll( "#pipeline-filters .filter-chip", ); siblings.forEach((s) => s.classList.remove("active")); chip.classList.add("active"); const data = store.get("fleetState"); if (data) renderPipelinesTab(data); }); }); const closeBtn = document.getElementById("detail-panel-close"); if (closeBtn) closeBtn.addEventListener("click", closePipelineDetail); } function renderPipelinesTab(data: FleetState): void { const tbody = document.getElementById("pipeline-table-body"); if (!tbody) return; const pipelines = data.pipelines || []; const events = data.events || []; const pipelineFilter = store.get("pipelineFilter"); const selectedPipelineIssue = store.get("selectedPipelineIssue"); const selectedIssues = store.get("selectedIssues"); interface PipelineRow { issue: number; title: string; status: string; stage: string; elapsed_s: number | null; branch: string; } const rows: PipelineRow[] = []; for (const p of pipelines) { rows.push({ issue: p.issue, title: p.title || "", status: "active", stage: (STAGE_SHORT as Record)[p.stage] || p.stage || "\u2014", elapsed_s: p.elapsed_s, branch: p.worktree || "", }); } const seen: Record = {}; rows.forEach((r) => (seen[r.issue] = true)); for (let i = events.length - 1; i >= 0; i--) { const ev = events[i]; if (!ev.issue || seen[ev.issue]) continue; const typeRaw = String(ev.type || ""); if (typeRaw.includes("completed") || typeRaw.includes("failed")) { const st = typeRaw.includes("failed") ? "failed" : "completed"; rows.push({ issue: ev.issue, title: ev.issueTitle || ev.title || "", status: st, stage: st === "completed" ? "DONE" : "FAIL", elapsed_s: ev.duration_s ?? null, branch: "", }); seen[ev.issue] = true; } } let filtered = rows; if (pipelineFilter !== "all") { filtered = rows.filter((r) => r.status === pipelineFilter); } if (filtered.length === 0) { tbody.innerHTML = '

No pipelines match filter

'; return; } let html = ""; for (const r of filtered) { const selectedClass = selectedPipelineIssue == r.issue ? " row-selected" : ""; const isChecked = selectedIssues[r.issue] ? " checked" : ""; html += `` + `` + `#${r.issue}` + `${escapeHtml(r.title)}` + `${r.status.toUpperCase()}` + `${escapeHtml(r.stage)}` + `${formatDuration(r.elapsed_s)}` + `${escapeHtml(r.branch)}`; } tbody.innerHTML = html; // Checkbox handlers tbody.querySelectorAll(".pipeline-checkbox").forEach((cb) => { cb.addEventListener("change", (e) => { e.stopPropagation(); const el = e.target as HTMLInputElement; const iss = el.getAttribute("data-issue") || ""; const issues = { ...store.get("selectedIssues") }; if (el.checked) issues[iss] = true; else delete issues[iss]; store.set("selectedIssues", issues); updateBulkToolbar(); }); cb.addEventListener("click", (e) => e.stopPropagation()); }); // Select-all const selectAll = document.getElementById( "pipeline-select-all", ) as HTMLInputElement; if (selectAll) { selectAll.addEventListener("change", () => { const cbs = tbody.querySelectorAll( ".pipeline-checkbox", ) as NodeListOf; const issues: Record = {}; cbs.forEach((cb) => { cb.checked = selectAll.checked; const iss = cb.getAttribute("data-issue") || ""; if (selectAll.checked) issues[iss] = true; }); store.set("selectedIssues", selectAll.checked ? issues : {}); updateBulkToolbar(); }); } // Row click handlers tbody.querySelectorAll(".pipeline-row").forEach((tr) => { tr.addEventListener("click", () => { const issue = tr.getAttribute("data-issue"); if (!issue) return; if (store.get("selectedPipelineIssue") == Number(issue)) { closePipelineDetail(); } else { fetchPipelineDetail(Number(issue)); } }); }); } export function fetchPipelineDetail(issue: number): void { store.set("selectedPipelineIssue", issue); document .querySelectorAll("#pipeline-table-body .pipeline-row") .forEach((tr) => { if (tr.getAttribute("data-issue") == String(issue)) tr.classList.add("row-selected"); else tr.classList.remove("row-selected"); }); const panel = document.getElementById("pipeline-detail-panel"); const title = document.getElementById("detail-panel-title"); const body = document.getElementById("detail-panel-body"); if (title) title.textContent = "Pipeline #" + issue; if (body) body.innerHTML = '

Loading...

'; if (panel) panel.classList.add("open"); api .fetchPipelineDetail(issue) .then((detail) => { localPipelineDetail = detail; store.set("pipelineDetail", detail); renderPipelineDetail(detail); }) .catch((err) => { if (body) body.innerHTML = `

Failed to load: ${escapeHtml(String(err))}

`; }); } function renderPipelineDetail(detail: PipelineDetail): void { const body = document.getElementById("detail-panel-body"); if (!body) return; const issue = detail.issue || store.get("selectedPipelineIssue"); let html = ""; html += `
`; const stagesDone = detail.stageHistory?.map((h) => h.stage) || []; html += `
${renderPipelineSVG({ stagesDone, stage: detail.stage, status: "", issue: detail.issue, title: detail.title, elapsed_s: detail.elapsed_s, iteration: 0, maxIterations: 0, })}
`; const history = detail.stageHistory || []; if (history.length > 0) { html += '
'; for (const sh of history) { const isActive = sh.stage === detail.stage; const dotCls = isActive ? "active" : "done"; html += `
` + `
` + `${escapeHtml(sh.stage)}` + `${formatDuration(sh.duration_s)}
`; } html += "
"; } html += '
'; if (detail.branch) html += `
Branch: ${escapeHtml(detail.branch)}
`; if (detail.elapsed_s != null) html += `
Elapsed: ${formatDuration(detail.elapsed_s)}
`; if (detail.prLink) html += ``; html += "
"; // Quality gate display html += ``; // Approval gate placeholder html += ``; html += renderArtifactViewer(issue!, detail); body.innerHTML = html; if (issue) { renderGitHubStatus(issue); setupArtifactTabs(issue); checkApprovalGate(issue, detail.stage); loadQualityGates(issue); } } function renderArtifactViewer(issue: number, detail: PipelineDetail): string { const tabs = [ { key: "plan", label: "Plan", content: detail.plan }, { key: "design", label: "Design", content: detail.design }, { key: "dod", label: "DoD", content: detail.dod }, { key: "reasoning", label: "Reasoning", content: null }, { key: "failures", label: "Failures", content: null }, { key: "tests", label: "Tests", content: null }, { key: "review", label: "Review", content: null }, { key: "logs", label: "Logs", content: null }, ]; let html = '
'; for (let i = 0; i < tabs.length; i++) { const activeClass = i === 0 ? " active" : ""; html += ``; } html += "
"; html += `
`; if (detail.plan) { html += `
${formatMarkdown(detail.plan)}
`; } else { html += '

No plan data

'; } html += "
"; return html; } function setupArtifactTabs(issue: number): void { const btns = document.querySelectorAll( `.artifact-tab-btn[data-issue="${issue}"]`, ); btns.forEach((btn) => { btn.addEventListener("click", () => { const artifact = btn.getAttribute("data-artifact"); const iss = btn.getAttribute("data-issue"); const siblings = document.querySelectorAll( `.artifact-tab-btn[data-issue="${iss}"]`, ); siblings.forEach((s) => s.classList.remove("active")); btn.classList.add("active"); if (iss && artifact) fetchArtifact(Number(iss), artifact); }); }); } function fetchArtifact(issue: number, type: string): void { const container = document.getElementById("artifact-content-" + issue); if (!container) return; container.innerHTML = '

Loading...

'; if (localPipelineDetail) { if (type === "plan" && localPipelineDetail.plan) { container.innerHTML = `
${formatMarkdown(localPipelineDetail.plan)}
`; return; } if (type === "design" && localPipelineDetail.design) { container.innerHTML = `
${formatMarkdown(localPipelineDetail.design)}
`; return; } if (type === "dod" && localPipelineDetail.dod) { container.innerHTML = `
${formatMarkdown(localPipelineDetail.dod)}
`; return; } } // Handle special artifact types if (type === "reasoning") { api .fetchPipelineReasoning(issue) .then((data) => { const reasoning = data.reasoning || []; if (reasoning.length === 0) { container.innerHTML = '

No reasoning data available. Agent reasoning will appear here as stages complete.

'; return; } let html = '
'; for (const r of reasoning) { const stage = String(r.stage || "general"); const content = String(r.content || r.summary || r.description || ""); html += `
` + `
${escapeHtml(stage)}
` + `
${r.type === "markdown" ? formatMarkdown(content) : escapeHtml(content)}
`; } html += "
"; container.innerHTML = html; }) .catch(() => { container.innerHTML = '

Could not load reasoning data

'; }); return; } if (type === "failures") { api .fetchPipelineFailures(issue) .then((data) => { const failures = data.failures || []; if (failures.length === 0) { container.innerHTML = '

No failures recorded for this pipeline.

'; return; } let html = '
'; for (const f of failures) { const fType = String(f.type || f.stage || "unknown"); const desc = String(f.description || f.error || f.message || ""); const rootCause = f.root_cause ? `
Root cause: ${escapeHtml(String(f.root_cause))}
` : ""; const fix = f.fix || f.suggested_fix; const fixHtml = fix ? `
Fix: ${escapeHtml(String(fix))}
` : ""; html += `
` + `
${escapeHtml(fType)}
` + `
${escapeHtml(desc)}
` + rootCause + fixHtml + "
"; } html += "
"; container.innerHTML = html; }) .catch(() => { container.innerHTML = '

Could not load failure data

'; }); return; } api .fetchArtifact(issue, type) .then((data) => { if (type === "logs") { container.innerHTML = renderLogViewer(data.content || ""); } else { container.innerHTML = `
${formatMarkdown(data.content || "")}
`; } }) .catch((err) => { container.innerHTML = `

Not available: ${escapeHtml(String(err))}

`; }); } function renderGitHubStatus(issue: number): void { const container = document.getElementById("github-status-" + issue); if (!container) return; api .fetchGitHubStatus(issue) .then((data) => { if (!(data as any).configured) { container.innerHTML = ""; return; } let html = '
'; if ((data as any).issue_state) html += `${escapeHtml((data as any).issue_state)}`; if ((data as any).pr_number) html += `PR #${(data as any).pr_number}`; if ((data as any).checks?.length > 0) { html += ''; for (const check of (data as any).checks) { const ci = check.status === "success" ? "\u2713" : check.status === "failure" ? "\u2717" : "\u25CF"; const cls = check.status === "success" ? "github-badge success" : check.status === "failure" ? "github-badge failure" : "github-badge pending"; html += `${ci}`; } html += ""; } html += "
"; container.innerHTML = html; }) .catch(() => { container.innerHTML = ""; }); } function renderErrorHighlight(issue: number): void { const container = document.getElementById("error-highlight-" + issue); if (!container) return; api .fetchLogs(issue) .then((data) => { const content = data.content || ""; const lines = content.split("\n"); const errorLines = lines.filter((l) => { const lower = l.toLowerCase(); return lower.indexOf("error") !== -1 || lower.indexOf("fail") !== -1; }); if (errorLines.length === 0) { container.innerHTML = ""; return; } const lastError = errorLines[errorLines.length - 1]; container.innerHTML = `
LAST ERROR` + `
${escapeHtml(lastError)}
`; }) .catch(() => { container.innerHTML = ""; }); } function loadQualityGates(issue: number): void { const panel = document.getElementById("quality-gate-" + issue); if (!panel) return; api .fetchPipelineQuality(issue) .then((data) => { const results = data.results || []; if (results.length === 0) { panel.style.display = "none"; return; } panel.style.display = ""; const allPassed = results.every((r) => r.passed); let html = `
${allPassed ? "\u2705" : "\u26A0\uFE0F"}`; html += `Quality Gates${allPassed ? " - All Passing" : ""}
`; html += '
'; for (const r of results) { const statusCls = r.passed ? "gate-pass" : "gate-fail"; const statusIcon = r.passed ? "\u2713" : "\u2717"; const valueStr = r.value !== null && r.value !== undefined ? String(r.value) : "N/A"; html += `
` + `${statusIcon}` + `${escapeHtml(r.name.replace(/_/g, " "))}` + `${r.operator} ${r.threshold}` + `Current: ${escapeHtml(valueStr)}
`; } html += "
"; panel.innerHTML = html; }) .catch(() => { panel.style.display = "none"; }); } function checkApprovalGate(issue: number, currentStage: string): void { const banner = document.getElementById("approval-gate-" + issue); if (!banner) return; api .fetchApprovalGates() .then((config) => { if (!config.enabled) { banner.style.display = "none"; return; } // Check if current stage requires approval const gatedStages = config.stages || []; const pending = (config.pending || []).find((p) => p.issue === issue); if (pending) { banner.style.display = ""; banner.innerHTML = `
` + `\u{1F6D1}` + `Awaiting approval to proceed to ${escapeHtml(pending.stage)}` + `
` + `` + `
`; document .getElementById("approve-" + issue) ?.addEventListener("click", () => { api.approveGate(issue, pending.stage).then(() => { banner.innerHTML = '
Approved. Agent will proceed.
'; setTimeout(() => { banner.style.display = "none"; }, 3000); }); }); document .getElementById("reject-" + issue) ?.addEventListener("click", () => { const reason = prompt("Reason for rejection (optional):"); api .rejectGate(issue, pending.stage, reason || undefined) .then(() => { banner.innerHTML = '
Rejected. Agent will stop.
'; setTimeout(() => { banner.style.display = "none"; }, 3000); }); }); } else if (gatedStages.includes(currentStage)) { banner.style.display = ""; banner.innerHTML = `
` + `\u{1F512}` + `Approval gates are enabled for: ${gatedStages.map((s) => escapeHtml(s)).join(", ")}
`; } else { banner.style.display = "none"; } }) .catch(() => { banner.style.display = "none"; }); } function closePipelineDetail(): void { store.set("selectedPipelineIssue", null); store.set("pipelineDetail", null); localPipelineDetail = null; const panel = document.getElementById("pipeline-detail-panel"); if (panel) panel.classList.remove("open"); document .querySelectorAll("#pipeline-table-body .pipeline-row") .forEach((tr) => tr.classList.remove("row-selected")); } export const pipelinesView: View = { init() { setupPipelineFilters(); }, render(data: FleetState) { renderPipelinesTab(data); }, destroy() {}, };