/**
* Secciones de nivel reporte (tablas, callouts, diagrama) extraídas del orquestador html.ts.
*/
import type { RunReportModel } from "./html.js";
import type { ProgressSummary } from "./html-agents.js";
import { chip, prettyJsonOutput } from "./html-builders.js";
import { buildRunMermaidSource, MERMAID_CDN_INTEGRITY, MERMAID_CDN_URL } from "./html-mermaid.js";
import { artifactViewerHref, escapeHtml, safeRelativeHref } from "./safe-html.js";
export function renderCallouts(model: RunReportModel, autoRefreshSeconds: number | undefined): string {
const callouts: string[] = [];
if (model.error !== undefined) {
callouts.push(`
Run error: ${escapeHtml(model.error)}
`);
}
if (model.state === "running") {
callouts.push(
`Point-in-time snapshot as of ${escapeHtml(model.generatedAt)}` +
(model.liveness === "unverified"
? ` — running as of ${escapeHtml(model.updatedAt ?? model.generatedAt)}; liveness unverified (out-of-session run dir).`
: ". Outputs and metrics appear when the run completes.") +
`
`,
);
}
if (model.state === "stale") {
callouts.push(
`Stale: status.json says running but no active run owns this id in the generating session.
`,
);
}
if (model.codeDrift === "changed") {
callouts.push(
`Code drift: the workflow script changed since this run (hash mismatch); the structure shown may not match what executed.
`,
);
} else if (model.codeDrift === "missing") {
callouts.push(
`Structure unavailable: the workflow script was not found; rendering execution data only.
`,
);
}
if (model.missingFiles.length) {
callouts.push(
`Missing run files: ${escapeHtml(model.missingFiles.join(", "))}.
`,
);
}
for (const note of model.clampNotes) {
callouts.push(`Clamp: ${escapeHtml(note)}
`);
}
if (autoRefreshSeconds !== undefined) {
callouts.push(
`Auto-refresh: this watched report reloads every ${autoRefreshSeconds}s while the run is running. The final regenerated report removes this refresh tag.
`,
);
}
return callouts.join("\n");
}
export function renderSchemasSection(schemas?: { name: string; json: string }[]): string {
if (!schemas || schemas.length === 0) return "";
return (
`Schemas (${schemas.length})
` +
schemas
.map(
(schema) =>
`${escapeHtml(schema.name)}
` +
`${escapeHtml(schema.json)} `,
)
.join("\n")
);
}
export function renderHeaderChips(model: RunReportModel, summary: ProgressSummary): string {
const failedAgents = summary.failed;
return [
chip("run", model.runId),
chip("scope", model.scope),
chip("agents", model.agents.length),
chip("preview", model.previewMode),
model.basedOn?.length ? chip("based on", model.basedOn.length) : "",
failedAgents ? chip("failed", failedAgents) : "",
model.integrity?.emptyOutputAgents ? chip("empty-output", model.integrity.emptyOutputAgents) : "",
model.integrity?.outputTruncatedAgents ? chip("output:truncated", model.integrity.outputTruncatedAgents) : "",
model.integrity?.stdoutTruncatedAgents ? chip("stdout:truncated", model.integrity.stdoutTruncatedAgents) : "",
chip("concurrency", model.agentConcurrency),
chip("maxAgents", model.maxAgents),
chip("peak parallel", model.peakParallelAgents),
chip("elapsed", model.elapsedMs !== undefined ? `${Math.round(model.elapsedMs / 1000)}s` : undefined),
chip("generated", model.generatedAt),
].join("");
}
export function renderPhaseSection(model: RunReportModel): string {
const phaseRows = model.phases
.map(
(p) =>
`| ${escapeHtml(p.time)} | ${escapeHtml(p.label)} | ${escapeHtml(p.source ?? "log")} |
`,
)
.join("");
if (!model.phases.length) return "";
const hasStructuredPhases = model.phases.some((p) => p.source === "event");
return (
`Phases
${
hasStructuredPhases
? "Structured phase events from the run dir; legacy log-derived phases are marked as log."
: 'Derived from the "phase: …" log convention.'
}
` +
`| Time | Phase | Source |
${phaseRows}
`
);
}
export function renderLogSection(logs: RunReportModel["logs"]): string {
return logs.length
? `Timeline (${logs.length} log entries)
${renderTimeline(logs)}
`
: "";
}
function renderTimelineDetails(details: string | undefined): string {
if (!details) return "";
const pretty = prettyJsonOutput(details);
const body = pretty
? `${escapeHtml(pretty)}`
: `${escapeHtml(details)}
`;
return `${body}
`;
}
function renderTimeline(logs: RunReportModel["logs"]): string {
const items = logs
.map(
(log) =>
`${escapeHtml(log.time)}` +
`${escapeHtml(log.message)}
${renderTimelineDetails(log.details)}`,
)
.join("");
return `${items}
`;
}
export function renderIntegritySection(integrity: RunReportModel["integrity"]): string {
if (!integrity) return "";
return (
`Result integrity
` +
[
chip("agent results", integrity.agentResults),
chip("failed", integrity.failedAgents),
chip("empty-output", integrity.emptyOutputAgents),
chip("output:truncated", integrity.outputTruncatedAgents),
chip("stdout:truncated", integrity.stdoutTruncatedAgents),
chip("timed out", integrity.timedOutAgents),
chip("schema failed", integrity.schemaFailedAgents),
].join("") +
`
`
);
}
export function renderMetricsSection(t: RunReportModel["metricsTotals"]): string {
if (!t) return "";
return (
`Run metrics
` +
[
chip("measured agents", t.measuredAgents),
chip("ok", t.okAgents),
chip("failed", t.failedAgents),
chip("output tokens", t.outputTokensTotal),
chip("cost", t.costTotal),
chip("tool calls", t.toolCalls),
chip("tool errors", t.toolErrors),
chip("retries", t.autoRetries),
].join("") +
`
`
);
}
export function renderBasedOnSection(basedOn: RunReportModel["basedOn"]): string {
const basedOnRows = (basedOn ?? [])
.map((item) => {
const detail = [item.role, item.desc].filter(Boolean).join(" · ");
return `| ${escapeHtml(item.name)} | ${escapeHtml(detail)} |
`;
})
.join("");
return basedOnRows
? `Based on
| Scaffold/source | Role |
${basedOnRows}
`
: "";
}
export function renderArtifactSection(
artifacts: RunReportModel["artifacts"],
artifactsOmitted: number | undefined,
): string {
if (!artifacts.length) return "";
const artifactRows = artifacts
.map((a) => {
const href = artifactViewerHref(a.path) ?? safeRelativeHref(a.path);
const label = escapeHtml(a.path);
const cell = href ? `${label}` : label;
return `| ${cell} | ${a.bytes !== undefined ? escapeHtml(String(a.bytes)) : ""} |
`;
})
.join("");
return (
`Artifacts
` +
(artifactsOmitted
? `Clamp: ${artifactsOmitted} more files not listed.
`
: "")
);
}
export function renderMermaidSection(model: RunReportModel): string {
if (!model.agents.length) return "";
return (
`Run diagram
${escapeHtml(buildRunMermaidSource(model))}
` +
`` +
// theme:"base" + themeVariables sigue prefers-color-scheme (matchMedia fijo, sin
// datos del modelo). Los hex de abajo son un subset intencionalmente duplicado de
// PANDI_TOKENS_CSS (--paper/--link/--info-bg en light, --raised en dark para que el
// cluster se despegue del fondo general en vez de casi fundirse con él): el diagrama sandbox
// renderiza en un iframe aislado que NO hereda los custom properties del documento
// padre, así que no hay forma de leerlos vs var(...) — hay que repetirlos literales.
// Fondo de cluster + líneas con un toque del accent "link" (alpha bajo) en vez de
// gris puro: un poco de color, sutil, sin competir con los estados pastel del nodo.
// background matchea --paper (pandi) en vez de transparent: probamos transparente y
// se volvió atrás a propósito (se veía peor que un fondo sólido consistente).
// fontFamily matchea el stack del body (LAYOUT_CSS): el iframe sandbox NO hereda el
// CSS de la página padre, así que sin esto mermaid cae al font default del browser.
`` +
`Run diagram (Mermaid source text)
` +
`
Si el diagrama de arriba no renderiza (JS deshabilitado o CDN bloqueada), pegá este texto en un visor Mermaid (mermaid.live u otro).
` +
`
${escapeHtml(buildRunMermaidSource(model))} `
);
}