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 (
`
();
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 agentNo 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
? ``
: `No agents recorded for this run.
`) +
``
);
}