import type { Theme } from "@earendil-works/pi-coding-agent"; import type { CacheSessionMetrics, CacheUsageTotals } from "./types.js"; import { formatCompact, formatPercent, hitRateColor, summarizeHitPercent } from "./format-utils.js"; /** Width of the label column so the two summary rows align. "Active branch" = 13. */ const LABEL_WIDTH = 13; /** One metric cell: dim name + plain value, e.g. "prompt 60.0k". */ function metric(theme: Theme, name: string, value: string): string { return `${theme.fg("dim", name)} ${value}`; } /** * One aligned summary row: * Active branch 3 turns · prompt 60.0k · recv 1.5k · hit 33.0k · rate 66.7% * Label is accent, metric names dim, values plain, hit rate tier-colored. */ function totalsLine( theme: Theme, label: string, totals: CacheUsageTotals, hideWrite: boolean, ): string { const rate = summarizeHitPercent(totals); const parts = [ theme.fg("accent", label.padEnd(LABEL_WIDTH)), theme.fg("dim", `${formatCompact(totals.assistantMessages)} turns`), metric(theme, "prompt", formatCompact(totals.input + totals.cacheRead + totals.cacheWrite)), metric(theme, "recv", formatCompact(totals.output)), metric(theme, "hit", formatCompact(totals.cacheRead)), ]; if (!hideWrite) parts.push(metric(theme, "write", formatCompact(totals.cacheWrite))); parts.push(metric(theme, "rate", hitRateColor(theme, rate, theme.bold(formatPercent(rate))))); return parts.join(" · "); } /** Block title shared by the summary and view sections. */ function blockTitle(theme: Theme, text: string): string { return theme.fg("accent", theme.bold(`▎ ${text}`)); } /** One line: current context occupancy, e.g. "context: 45.2% · 57.8k / 128k". */ function contextLine( theme: Theme, contextUsage?: { tokens: number | null; contextWindow: number; percent: number | null }, ): string[] { if (!contextUsage) return []; const { tokens, contextWindow, percent } = contextUsage; const pct = percent === null ? "?" : percent > 90 ? theme.fg("error", `${percent.toFixed(1)}%`) : percent > 70 ? theme.fg("warning", `${percent.toFixed(1)}%`) : `${percent.toFixed(1)}%`; const tokensText = tokens === null ? "?" : formatCompact(tokens); return [ theme.fg("dim", "context:") + ` ${pct}` + theme.fg("dim", " · ") + `${tokensText} / ${formatCompact(contextWindow)}`, ]; } /** * Session-wide summary, rendered once in the dialog's fixed header. * The delta row appears only when the session tree has side branches. */ export function buildSummaryLines( theme: Theme, metrics: CacheSessionMetrics, contextUsage?: { tokens: number | null; contextWindow: number; percent: number | null }, ): string[] { const tree = metrics.treeTotals; const branch = metrics.activeBranchTotals; const treeHitRate = summarizeHitPercent(tree); const branchHitRate = summarizeHitPercent(branch); // Providers without a cache-write bucket (DeepSeek/OpenAI/Gemini) always // report 0 here; hide the noise term instead of printing "write 0". const hideWrite = tree.cacheWrite === 0 && branch.cacheWrite === 0; const deltaPrompt = tree.input + tree.cacheRead + tree.cacheWrite - (branch.input + branch.cacheRead + branch.cacheWrite); const deltaReceived = tree.output - branch.output; const deltaHit = tree.cacheRead - branch.cacheRead; const deltaWrite = tree.cacheWrite - branch.cacheWrite; const deltaSpread = treeHitRate - branchHitRate; // Sessions without forks have tree == branch; hide the all-zero delta row // instead of printing zeros (it reappears once the session forks). const hasSideBranches = tree.assistantMessages !== branch.assistantMessages || deltaPrompt !== 0 || deltaReceived !== 0 || deltaHit !== 0 || deltaWrite !== 0; const lines = [ blockTitle(theme, "Session summary"), ...contextLine(theme, contextUsage), totalsLine(theme, "Active branch", branch, hideWrite), totalsLine(theme, "Whole tree", tree, hideWrite), ]; if (hasSideBranches) { const deltaParts = [ theme.fg("accent", "Delta".padEnd(LABEL_WIDTH)), metric(theme, "prompt", formatCompact(deltaPrompt)), metric(theme, "recv", formatCompact(deltaReceived)), metric(theme, "hit", formatCompact(deltaHit)), ]; if (!hideWrite) deltaParts.push(metric(theme, "write", formatCompact(deltaWrite))); deltaParts.push(metric(theme, "spread", formatPercent(deltaSpread))); lines.push(deltaParts.join(" · ")); } return lines; }