import { agentRowMetaChipTone, buildAgentRowMetaChips } from "../lib/agent-row-meta.js"; import { formatElapsedMs } from "../lib/presentation.js"; import { formatAgentPhase } from "./event-parser.js"; import type { RunReportAgent, RunReportModel } from "./html.js"; import { artifactViewerHref, compactInlineForHtml, escapeHtml, safeRelativeHref } from "./safe-html.js"; export function pillClass(state: string, ok?: boolean): string { if (state === "completed" && ok !== false) return "ok"; if (state === "running") return "run"; if (state === "cached") return "ok"; if (state === "interrupted") return "fail"; // "planned" es la vista pre-launch (nada corrió todavía) — advertencia neutral, nunca "fail". if (state === "stale" || state === "cancelled" || state === "unknown" || state === "planned") return "warn"; return "fail"; } export function plural(count: number, singular: string, pluralForm = `${singular}s`): string { return count === 1 ? singular : pluralForm; } type ProgressTone = "ok" | "fail" | "run" | "warn"; function meter(fraction: number, tone: ProgressTone = "ok"): string { const pct = Math.max(0, Math.min(1, Number.isFinite(fraction) ? fraction : 0)); return ``; } function metricCard(label: string, value: string | number, detail = ""): string { return ( `
${escapeHtml(label)}
` + `
${escapeHtml(String(value))}
` + (detail ? `
${detail}
` : "") + `
` ); } export function agentFailed(agent: RunReportAgent): boolean { return agent.ok === false || agent.state === "failed" || agent.state === "interrupted"; } function agentSucceeded(agent: RunReportAgent): boolean { return (agent.state === "completed" || agent.state === "cached") && agent.ok !== false; } function agentDone(agent: RunReportAgent): boolean { return agent.state !== "running" && agent.state !== "planned"; } export interface ProgressSummary { observed: number; total: number; done: number; running: number; failed: number; unknown: number; fraction: number; tone: ProgressTone; openEnded: boolean; } function plannedAgentTotal(agents: RunReportAgent[]): number { const phaseTotals = new Map(); let standalone = 0; for (const agent of agents) { if (agent.phaseTotal !== undefined && agent.phaseTotal > 0) { const key = agent.phaseId !== undefined ? `phase:${agent.phaseId}` : `agent:${agent.name}`; phaseTotals.set(key, Math.max(phaseTotals.get(key) ?? 0, agent.phaseTotal)); } else { standalone += 1; } } let planned = standalone; for (const total of phaseTotals.values()) planned += total; return Math.max(agents.length, planned); } export function summarizeProgress(model: RunReportModel): ProgressSummary { const observed = model.agents.length; const done = model.agents.filter(agentDone).length; const running = model.agents.filter((agent) => agent.state === "running").length; const failed = model.agents.filter(agentFailed).length; const unknown = model.agents.filter( (agent) => agentDone(agent) && !agentFailed(agent) && !agentSucceeded(agent), ).length; const total = plannedAgentTotal(model.agents); const openEnded = model.state === "running" && running === 0 && done >= total && total > 0; const fraction = total > 0 ? (openEnded ? Math.min(done / total, 0.95) : done / total) : 0; const tone: ProgressTone = failed > 0 || model.state === "failed" ? "fail" : model.state === "running" || running > 0 || openEnded ? "run" : unknown > 0 || model.state === "cancelled" || model.state === "stale" || model.state === "unknown" || model.state === "planned" ? "warn" : "ok"; return { observed, total, done, running, failed, unknown, fraction, tone, openEnded }; } function progressValue(summary: ProgressSummary): string { return `${summary.done}/${summary.total}${summary.openEnded ? "+" : ""}`; } function reportAgentPhaseDetail(agent: RunReportAgent): string { const phase = formatAgentPhase(agent); if (phase && agent.phaseLabel) return `${phase} • ${agent.phaseLabel}`; return phase ?? agent.phaseLabel ?? ""; } function agentStateText(agent: RunReportAgent): string { if (agentFailed(agent)) return agent.state === "interrupted" ? "✗ interrupted" : "✗ failed"; if (agent.state === "completed") return "✓ done"; if (agent.state === "running") return "▶ running"; if (agent.state === "cached") return "♻ cached"; if (agent.state === "planned") return "◦ planned"; return "? unknown"; } function skillsText(agent: RunReportAgent): string { if (agent.skills) return `${agent.skills}${agent.includeSkills ? " + discovery" : " (explicit only)"}`; return agent.includeSkills === false ? "disabled" : "default discovery"; } function extensionsText(agent: RunReportAgent): string { if (agent.extensions) return `${agent.extensions}${agent.includeExtensions ? " + discovery" : " (explicit only)"}`; return agent.includeExtensions ? "default discovery" : "disabled"; } function keysText(agent: RunReportAgent): string { return agent.keys ? agent.keys : agent.isolatedEnv ? "none selected" : "default inherited environment"; } export function agentAccessMeta(agent: RunReportAgent): string { return [ agent.promptAvailable ? "prompt✓" : agent.promptAvailable === false ? "prompt?" : "", agent.schemaOk !== undefined ? `schema ${agent.schemaOk ? "ok" : "bad"}` : "", agent.model ? `model ${agent.model}` : "", agent.thinking ? `effort ${agent.thinking}` : "", agent.outputEmpty ? "output empty" : "", agent.outputTruncated ? "output truncated" : "", agent.outputChars !== undefined ? `output chars: ${agent.outputChars}` : "", agent.tools ? `tools: ${agent.tools}` : "tools: default", agent.excludeTools ? `exclude: ${agent.excludeTools}` : "", `skills: ${skillsText(agent)}`, `extensions: ${extensionsText(agent)}`, `keys: ${keysText(agent)}`, agent.missingKeys ? `missing: ${agent.missingKeys}` : "", agent.isolatedEnv ? "isolated env" : "", ] .filter(Boolean) .join(" · "); } function agentRowMeta(agent: RunReportAgent): string[] { return buildAgentRowMetaChips(agent); } function miniChipClass(label: string): string { return agentRowMetaChipTone(label); } function renderMiniChips(chips: string[]): string { return `
${chips .map( (label) => `${escapeHtml(label)}`, ) .join("")}
`; } function renderMonitorAgentLine(agent: RunReportAgent): string { const phase = formatAgentPhase(agent); const elapsed = agent.elapsedMs === undefined ? "elapsed:…" : `elapsed:${formatElapsedMs(agent.elapsedMs)}`; return ( `
` + `${escapeHtml(agentStateText(agent))}` + `#${escapeHtml(String(agent.id))}` + (phase ? `${escapeHtml(phase)}` : "") + `${escapeHtml(agent.name)}` + `${escapeHtml(elapsed)}` + (agent.code === undefined ? "" : `code:${escapeHtml(String(agent.code))}`) + renderMiniChips(agentRowMeta(agent)) + `
` ); } function detailLine(label: string, valueHtml: string): string { return `
${escapeHtml(label)}: ${valueHtml}
`; } export function link(href: string | undefined, label: string): string { const safe = artifactViewerHref(href) ?? safeRelativeHref(href); if (!safe) return ""; return `${escapeHtml(label)}`; } function renderMonitorSelectedAgent(agent: RunReportAgent, failed: boolean): string { const artifact = link(agent.artifactHref, "artifact.md"); const promptStatus = agent.promptAvailable ? "available" : "not available"; const phaseToken = formatAgentPhase(agent); const phaseDetail = reportAgentPhaseDetail(agent); const phase = phaseDetail ? detailLine("phase", escapeHtml(phaseDetail)) : ""; const config = [ detailLine( "model", `${escapeHtml(agent.model ?? "default")} effort: ${escapeHtml(agent.thinking ?? "default")}`, ), detailLine( "tools", `${escapeHtml(agent.tools ?? "default")}${agent.excludeTools ? ` exclude: ${escapeHtml(agent.excludeTools)}` : ""}`, ), detailLine("skills", escapeHtml(skillsText(agent))), detailLine("extensions", escapeHtml(extensionsText(agent))), detailLine( "keys", `${escapeHtml(keysText(agent))}${agent.missingKeys ? ` missing: ${escapeHtml(agent.missingKeys)}` : ""}`, ), agent.isolatedEnv ? detailLine("env", "isolated") : "", ] .filter(Boolean) .join(""); const outputState = [ agent.outputEmpty ? "empty" : "", agent.outputTruncated ? "truncated" : "", agent.outputChars !== undefined ? `${agent.outputChars} chars` : "", ] .filter(Boolean) .join(" • "); const io = [ agent.promptPreview ? detailLine("prompt preview", escapeHtml(compactInlineForHtml(agent.promptPreview, 220))) : "", outputState ? detailLine("output state", escapeHtml(outputState)) : "", agent.output !== undefined ? detailLine("output", escapeHtml(compactInlineForHtml(agent.output.text, 220))) : "", agent.outputEmpty ? detailLine("integrity", "empty-output") : "", agent.outputTruncated ? detailLine( "integrity", `output:truncated${agent.outputChars === undefined ? "" : ` (${escapeHtml(String(agent.outputChars))} chars)`}`, ) : "", agent.stdoutTruncated ? detailLine( "integrity", `stdout:truncated${agent.stdoutChars === undefined ? "" : ` (${escapeHtml(String(agent.stdoutChars))} chars)`}`, ) : "", ] .filter(Boolean) .join(""); return ( `
Selected agent` + detailLine( "agent", `#${escapeHtml(String(agent.id))} ${phaseToken ? `${escapeHtml(phaseToken)} ` : ""}${escapeHtml(agent.name)}`, ) + detailLine( "state", `${escapeHtml(agent.state)}${agent.elapsedMs !== undefined ? ` ${escapeHtml(formatElapsedMs(agent.elapsedMs))}` : ""}${agent.code !== undefined ? ` code ${escapeHtml(String(agent.code))}` : ""}`, ) + phase + detailLine( "prompt", `${escapeHtml(promptStatus)}${artifact ? ` ${artifact}` : ""}`, ) + `
config
${config}` + (io ? `
i/o
${io}` : "") + `
` ); } export function renderWorkflowMonitor(model: RunReportModel, summary: ProgressSummary): string { const running = summary.running; const frac = summary.fraction; const last = model.logs.slice(-1)[0]; const featured = model.agents.find(agentFailed) ?? model.agents.find((agent) => agent.state === "running") ?? model.agents[0]; const row = (agent: RunReportAgent): string => { const isFeatured = featured && agent.id === featured.id; return `${renderMonitorAgentLine(agent)}`; }; const agentRows = model.agents.map(row).join(""); const parallel = model.agentConcurrency !== undefined ? `${running}/${model.agentConcurrency}` : String(running); const agentHeader = `
Agents (${model.agents.length})` + `• parallel ${escapeHtml(parallel)}${model.peakParallelAgents === undefined ? "" : ` • peak ${escapeHtml(String(model.peakParallelAgents))}`}
`; const featuredHint = featured ? renderMonitorSelectedAgent(featured, summary.failed > 0) : `
Selected agent
No agents recorded yet.
`; return ( `

Workflow monitor

` + `${escapeHtml(model.state)}
` + `
` + metricCard( "Progress", progressValue(summary), `${meter(frac, summary.tone)} ${Math.round(frac * 100)}%`, ) + metricCard( "parallel", model.agentConcurrency !== undefined ? `${running}/${model.agentConcurrency}` : running, model.peakParallelAgents !== undefined ? `peak ${escapeHtml(String(model.peakParallelAgents))}` : "running now", ) + metricCard("failed", summary.failed, summary.failed ? "review failed cards" : "no failed agents") + metricCard( "artifacts", model.artifacts.length, model.artifacts[0] ? escapeHtml(model.artifacts[0].path) : "none listed", ) + metricCard("logs", model.logs.length, "timeline entries") + metricCard( "Latest activity", last ? `${String(last.time).slice(11, 19)} ${last.message}` : "—", "last log event", ) + `
` + featuredHint + `

Agent monitor

` + agentHeader + (agentRows ? `${agentRows}
Monitor row
` : `
No agents recorded for this run.
`) + `
` ); }