/** * 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.' }
` + `${phaseRows}
TimePhaseSource
` ); } 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

    ${basedOnRows}
    Scaffold/sourceRole
    ` : ""; } 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

    ${artifactRows}
    FileBytes
    ` + (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))}
    ` ); }