import type { Theme } from "@earendil-works/pi-coding-agent"; import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import { appendRightAlignedAdaptiveHint } from "./tui-layout.ts"; import { formatMetricValue } from "./metric-format.ts"; import type { ResearchState } from "../domain/research-state.ts"; import { buildResearchSummaryFromState, isBetterMetric, type ResearchRunSummary, } from "../domain/research-summary.ts"; // --------------------------------------------------------------------------- // Dashboard table renderer (pure function, no UI deps) // --------------------------------------------------------------------------- export function renderDashboardLines( st: ResearchState, width: number, th: Theme, maxRows: number = 6, headerHints: string[] = [] ): string[] { const lines: string[] = []; if (st.results.length === 0) { lines.push(` ${th.fg("dim", "No runs yet.")}`); return lines; } const summary = buildResearchSummaryFromState(st, { recentRunLimit: st.results.length }); const { statusCounts } = summary.currentExperiment; const kept = statusCounts.keep; const discarded = statusCounts.discard; const crashed = statusCounts.crash; const checksFailed = statusCounts.checks_failed; const baseline = summary.currentExperiment.baseline; const baselineSec = summary.currentExperiment.baselineSecondary; const best = summary.currentExperiment.best; // Runs summary const confSuffix = st.confidence !== null ? (() => { const confStr = st.confidence!.toFixed(1); const confColor: Parameters[0] = st.confidence! >= 2.0 ? "success" : st.confidence! >= 1.0 ? "warning" : "error"; return ` ${th.fg(confColor, `(conf: ${confStr}×)`)}`; })() : ""; lines.push( truncateToWidth( ` ${th.fg("muted", "Runs:")} ${th.fg("text", String(st.results.length))}` + ` ${th.fg("success", `${kept} kept`)}` + confSuffix + (discarded > 0 ? ` ${th.fg("warning", `${discarded} discarded`)}` : "") + (crashed > 0 ? ` ${th.fg("error", `${crashed} crashed`)}` : "") + (checksFailed > 0 ? ` ${th.fg("error", `${checksFailed} checks failed`)}` : ""), width ) ); // Baseline: first run's primary metric const baselineSuffix = baseline === null ? "" : ` #${baseline.runNumber}`; lines.push( truncateToWidth( ` ${th.fg("muted", "Baseline:")} ${th.fg("muted", `★ ${st.metricName}: ${formatMetricValue(baseline?.metric ?? null, st.metricUnit)}${baselineSuffix}`)}`, width ) ); // Progress: best primary metric with delta + run number if (best !== null) { let progressLine = ` ${th.fg("muted", "Progress:")} ${th.fg("warning", th.bold(`★ ${st.metricName}: ${formatMetricValue(best.metric, st.metricUnit)}`))}${th.fg("dim", ` #${best.runNumber}`)}`; if (best.deltaPercent !== null && baseline !== null) { const pct = best.deltaPercent; const sign = pct > 0 ? "+" : ""; const color = isBetterMetric(best.metric, baseline.metric, st.bestDirection) ? "success" : "error"; progressLine += th.fg(color, ` (${sign}${pct.toFixed(1)}%)`); } lines.push(truncateToWidth(progressLine, width)); // Progress secondary metrics — wrap into lines that fit width, indented if (st.secondaryMetrics.length > 0) { const indent = " "; // 12 chars to align under progress value const maxLineW = width - 2 - indent.length; // 2 for leading " " // Build individually-colored parts const secParts: string[] = []; for (const sm of st.secondaryMetrics) { const val = best.metrics[sm.name]; const bv = baselineSec[sm.name]; if (val !== undefined) { let part = th.fg("muted", `${sm.name}: ${formatMetricValue(val, sm.unit)}`); if (bv !== undefined && bv !== 0 && val !== bv) { const p = ((val - bv) / bv) * 100; const s = p > 0 ? "+" : ""; const c = val <= bv ? "success" : "error"; part += th.fg(c, ` ${s}${p.toFixed(1)}%`); } secParts.push(part); } } // Flow-wrap parts into lines if (secParts.length > 0) { let curLine = ""; let curVisW = 0; for (const part of secParts) { const partVisW = visibleWidth(part); const sep = curLine ? " " : ""; if (curLine && curVisW + sep.length + partVisW > maxLineW) { lines.push(truncateToWidth(` ${th.fg("dim", indent)}${curLine}`, width)); curLine = part; curVisW = partVisW; } else { curLine += sep + part; curVisW += sep.length + partVisW; } } if (curLine) { lines.push(truncateToWidth(` ${th.fg("dim", indent)}${curLine}`, width)); } } } } lines.push(""); // Determine visible rows once — used for both column sizing and rendering const effectiveMax = maxRows <= 0 ? st.results.length : maxRows; const startIdx = Math.max(0, st.results.length - effectiveMax); const rowsToRender = summary.recentRuns.slice(-effectiveMax); // Only show secondary metric columns that have at least one value in rendered rows const secMetrics = st.secondaryMetrics.filter((sm) => rowsToRender.some((r) => r.metrics[sm.name] !== undefined) ); // Column definitions // Primary column: "★ " prefix (2 visible) + metric name + 1 padding, clamped to 25% of width const primaryLabel = "★ " + (st.metricName || "metric"); const primaryW = Math.max(11, Math.min(Math.floor(width * 0.25), visibleWidth(primaryLabel) + 1)); const col = { idx: 3, commit: 8, primary: primaryW, status: 15 }; const minDescW = Math.max(10, Math.floor(width * 0.25)); const fixedW = col.idx + col.commit + col.primary + col.status + 6; // Compute each secondary column width from actual content: max(name, widest value) + 1 padding const secColWidths: number[] = secMetrics.map((sm) => { let maxW = visibleWidth(sm.name); for (const r of rowsToRender) { const val = (r.metrics ?? {})[sm.name]; if (val !== undefined) { maxW = Math.max(maxW, visibleWidth(formatMetricValue(val, sm.unit))); } } return maxW + 1; }); const totalSecWidth = () => secColWidths.slice(0, visibleSecMetrics.length).reduce((a, b) => a + b, 0); // Drop secondary columns from the right until they fit let visibleSecMetrics = secMetrics; while (visibleSecMetrics.length > 0 && totalSecWidth() > width - fixedW - minDescW) { visibleSecMetrics = visibleSecMetrics.slice(0, -1); } const descW = Math.max(minDescW, width - fixedW - totalSecWidth()); // Table header — primary metric name bolded with ★ let headerLine = ` ${th.fg("muted", "#".padEnd(col.idx))}` + `${th.fg("muted", "commit".padEnd(col.commit))}` + `${th.fg("warning", th.bold(truncateToWidth(primaryLabel, col.primary - 1).padEnd(col.primary)))}`; for (let si = 0; si < visibleSecMetrics.length; si++) { const sm = visibleSecMetrics[si]; headerLine += th.fg( "muted", sm.name.padEnd(secColWidths[si]) ); } headerLine += `${th.fg("muted", "status".padEnd(col.status))}` + `${th.fg("muted", "description")}`; lines.push( headerHints.length > 0 ? appendRightAlignedAdaptiveHint(headerLine, width, th, headerHints) : truncateToWidth(headerLine, width, "…", true) ); lines.push( truncateToWidth( ` ${th.fg("borderMuted", "─".repeat(Math.max(0, width - 4)))}`, width ) ); const baselinePrimary = baseline?.metric ?? null; const baselineSecondary = baselineSec; // Show max 6 recent runs, with a note about hidden earlier ones if (startIdx > 0) { lines.push( truncateToWidth( ` ${th.fg("dim", `… ${startIdx} earlier run${startIdx === 1 ? "" : "s"}`)}`, width ) ); } for (const r of rowsToRender) { const isOld = r.experimentIndex !== st.currentExperimentIndex; const isBaseline = !isOld && baseline?.runNumber === r.runNumber; const color = isOld ? "dim" : r.status === "keep" ? "success" : r.status === "crash" || r.status === "checks_failed" ? "error" : "warning"; // Primary metric with color coding const primaryStr = formatMetricValue(r.metric, st.metricUnit); let primaryColor: Parameters[0] = isOld ? "dim" : "text"; if (!isOld) { if (isBaseline) { primaryColor = "text"; // baseline row — normal text } else if ( baselinePrimary !== null && r.status === "keep" && r.metric > 0 ) { if (isBetterMetric(r.metric, baselinePrimary, st.bestDirection)) { primaryColor = "success"; } else if (r.metric !== baselinePrimary) { primaryColor = "error"; } } } const idxStr = th.fg("dim", String(r.runNumber).padEnd(col.idx)); const commitStr = isOld ? "(old)".padEnd(col.commit) : r.status !== "keep" ? "—".padStart(Math.ceil(col.commit / 2)).padEnd(col.commit) : r.commit.padEnd(col.commit); let rowLine = ` ${idxStr}` + `${th.fg(isOld ? "dim" : "accent", commitStr)}` + `${th.fg(primaryColor, isOld ? primaryStr.padEnd(col.primary) : th.bold(primaryStr.padEnd(col.primary)))}`; // Secondary metrics (only visible columns) const rowMetrics = r.metrics; for (let si = 0; si < visibleSecMetrics.length; si++) { const sm = visibleSecMetrics[si]; const colW = secColWidths[si]; const val = rowMetrics[sm.name]; if (val !== undefined) { const secStr = formatMetricValue(val, sm.unit); let secColor: Parameters[0] = "dim"; if (!isOld) { const bv = baselineSecondary[sm.name]; if (isBaseline) { secColor = "text"; } else if (bv !== undefined && bv !== 0) { secColor = val <= bv ? "success" : "error"; } } rowLine += th.fg(secColor, secStr.padEnd(colW)); } else { rowLine += th.fg("dim", "—".padEnd(colW)); } } rowLine += `${th.fg(color, r.status.padEnd(col.status))}` + `${th.fg("muted", r.description.slice(0, descW))}`; lines.push(truncateToWidth(rowLine, width)); } return lines; }