import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent"; import type { AssistantUsageMetric } from "./types.js"; /** * Hit-rate buckets for the turn-distribution chart. Colors follow the global * tiering (<80 red, 80-95 yellow, >=95 green); within one color, solid vs * half-solid fill distinguishes the finer sub-ranges. */ const HIT_BUCKETS: { label: string; min: number; max: number; color: ThemeColor; fill: string }[] = [ { label: "0-50", min: 0, max: 50, color: "error", fill: "█" }, { label: "50-80", min: 50, max: 80, color: "error", fill: "▓" }, { label: "80-90", min: 80, max: 90, color: "warning", fill: "█" }, { label: "90-95", min: 90, max: 95, color: "warning", fill: "▓" }, { label: "95-98", min: 95, max: 98, color: "success", fill: "█" }, { label: ">98", min: 98, max: 101, color: "success", fill: "▓" }, ]; /** Label column width ("50-80" etc.). */ const LABEL_WIDTH = 5; /** Max bar length; wider terminals keep the chart compact instead of full-width. */ const MAX_BAR_WIDTH = 80; function countBuckets(messages: AssistantUsageMetric[]): number[] { return HIT_BUCKETS.map( (b) => messages.filter((m) => m.cacheHitPercent >= b.min && m.cacheHitPercent < b.max).length, ); } /** * Horizontal bar chart: one row per hit-rate bucket, bar length scaled * logarithmically so rare low-hit-rate turns stay visible next to a dominant * top bucket. The exact count is right-aligned at the end of each row. */ function renderDistributionChart(theme: Theme, messages: AssistantUsageMetric[], barWidth: number): string[] { const counts = countBuckets(messages); const maxCount = Math.max(...counts, 1); const maxLog = Math.log1p(maxCount); const lines: string[] = []; HIT_BUCKETS.forEach((b, i) => { const c = counts[i]!; const label = theme.fg("dim", b.label.padStart(LABEL_WIDTH)); if (c === 0) { lines.push(`${label} ${" ".repeat(barWidth)} ${theme.fg("dim", "0")}`); return; } const len = Math.max(1, Math.round((Math.log1p(c) / maxLog) * barWidth)); const bar = theme.fg(b.color, b.fill.repeat(len)); const pad = " ".repeat(barWidth - len); lines.push(`${label} ${bar}${pad} ${theme.fg(b.color, String(c))}`); }); lines.push(theme.fg("muted", " ".repeat(LABEL_WIDTH + 1) + "─".repeat(barWidth + 1))); lines.push( theme.fg("dim", " ".repeat(LABEL_WIDTH + 2) + "0") + theme.fg("dim", " ".repeat(Math.max(0, barWidth - 1)) + String(maxCount)), ); return lines; } // ─── Public entry point ─────────────────────────────────────────────────────── /** * Renders the turn-distribution bar chart plus the recent-turns list. * The session-wide summary is rendered once by the dialog header, not here. */ export function renderGraphView(theme: Theme, messages: AssistantUsageMetric[], width: number): string[] { const lines: string[] = []; if (messages.length === 0) { lines.push( theme.fg("warning", "No assistant messages with usage data are available yet in this session."), ); return lines; } lines.push(theme.fg("accent", theme.bold("▎ Turn distribution"))); lines.push(theme.fg("dim", "bar length = turns per bucket (log scale)")); lines.push(""); const barWidth = Math.min(Math.max(10, width - LABEL_WIDTH - 14), MAX_BAR_WIDTH); lines.push(...renderDistributionChart(theme, messages, barWidth)); return lines; }