import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Key, matchesKey } from "@earendil-works/pi-tui"; import { SplitDialog } from "./dialog.js"; import { exportStatsCsv } from "./export.js"; import { renderGraphView } from "./graph-view.js"; import { renderMissesView } from "./misses-view.js"; import { collectCacheSessionMetrics } from "./session-data.js"; import { renderStatsView } from "./stats-view.js"; import { buildSummaryLines } from "./summary.js"; type CacheView = "distribution" | "misses" | "stats"; const VIEW_LABELS: Record = { distribution: "Turn distribution", misses: "Miss analysis", stats: "Per-message table", }; const VIEW_ORDER: CacheView[] = ["stats", "distribution", "misses"]; function initialViewFromArgs(args: string): CacheView { const sub = args.trim().toLowerCase(); if (sub === "graph") return "distribution"; return "stats"; // default: per-message table } export default function cacheInsightExtension(pi: ExtensionAPI): void { pi.registerCommand("cache", { description: "Session cache-prefix analysis: summary, turn distribution, miss analysis, or CSV export", getArgumentCompletions(prefix) { const items = [ { value: "graph", label: "graph", description: "Open cache viewer (turn-distribution chart)" }, { value: "export", label: "export", description: "Export stats data to a CSV in the OS temp dir" }, ]; const filtered = items.filter((item) => item.value.startsWith(prefix.toLowerCase())); return filtered.length > 0 ? filtered : items; }, handler: async (args, ctx) => { const sub = args.trim().toLowerCase(); if (sub === "export") { const metrics = collectCacheSessionMetrics(ctx.sessionManager, ctx.modelRegistry); const filePath = await exportStatsCsv(ctx.sessionManager, metrics); ctx.ui.notify(`Exported cache stats CSV to ${filePath}`, "info"); return; } if (!ctx.hasUI) { ctx.ui.notify( "/cache requires interactive TUI mode. Use /cache export in non-interactive mode.", "info", ); return; } let currentView: CacheView = initialViewFromArgs(args); const metrics = collectCacheSessionMetrics(ctx.sessionManager, ctx.modelRegistry); // Render in the editor slot (same mechanism as /model's selector panel): // the dialog appears right where the input box is, growing upward, so // it stays inside the user's visual focus area instead of floating // mid-screen as an overlay. await ctx.ui.custom( (_tui, theme, _keybindings, done) => new SplitDialog( theme, { title: "Cache Insight", getTitle: () => `Cache Insight — ${VIEW_LABELS[currentView]}`, helpText: "{Tab} cycle view • {j}/{k} scroll • {q} close", renderHeader: () => buildSummaryLines(theme, metrics, ctx.getContextUsage()), renderBody: (innerWidth) => { if (currentView === "stats") return renderStatsView(theme, metrics.allMessages, innerWidth); if (currentView === "misses") return renderMissesView(theme, metrics.allMessages, innerWidth); return renderGraphView(theme, metrics.allMessages, innerWidth); }, onKey: (data) => { if (matchesKey(data, Key.tab)) { const idx = VIEW_ORDER.indexOf(currentView); currentView = VIEW_ORDER[(idx + 1) % VIEW_ORDER.length]!; return true; } return false; }, }, () => done(undefined), ), ); }, }); }