// ABOUTME: Footer widget displaying model name, context percentage + window size, and working directory. // ABOUTME: Shows context usage warnings; core pi framework handles actual auto-compaction. /** * Footer — Dark status bar with model · context % / window · directory. * * Context compaction is handled by pi's core _runAutoCompaction which properly * emits auto_compaction_start/end events. The interactive-mode handles these * events by calling rebuildChatFromMessages() to clear and re-render the UI. * * Previously, this extension called ctx.compact() directly which bypassed * the auto_compaction events, leaving stale UI components that caused * doubled/artifact rendering after compaction. */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; import { basename, dirname } from "node:path"; import { applyExtensionDefaults } from "./lib/themeMap.ts"; import { shouldWarnForCompaction, getProactiveCompactionPhase } from "./lib/context-gate.ts"; /** Turn a model name like "Claude 4 Opus" into "opus 4" */ function shortModelName(name: string | undefined): string { if (!name) return "no model"; const cleaned = name.replace(/^claude\s*/i, "").trim(); const tokens = cleaned.split(/\s+/); const versions: string[] = []; const words: string[] = []; for (const token of tokens) { if (/^[\d.]+$/.test(token)) versions.push(token); else words.push(token.toLowerCase()); } const parts = [...words, ...versions]; return parts.join(" ") || name.toLowerCase(); } /** Format a token count into compact K/M notation: 200K, 1.2M */ export function formatTokens(n: number): string { if (n < 1000) return String(Math.round(n)); if (n < 1_000_000) { const k = n / 1000; return k % 1 === 0 ? `${k}K` : `${parseFloat(k.toFixed(1))}K`; } const m = n / 1_000_000; return m % 1 === 0 ? `${m}M` : `${parseFloat(m.toFixed(1))}M`; } /** Thinking level → labeled indicator */ function thinkingIndicator(level: string | undefined, theme: any): string { const label = level || "off"; const color = label === "off" ? "dim" : label === "high" || label === "xhigh" ? "warning" : "accent"; return theme.fg("dim", "thinking: ") + theme.fg(color, theme.bold(label)); } /** Last two path components: "Github-Work/pi-vs-claude-code" */ function shortDir(cwd: string): string { const child = basename(cwd); const parent = basename(dirname(cwd)); return parent ? `${parent}/${child}` : child; } function setupFooter(pi: ExtensionAPI, ctx: any, onUnsub: (unsub: () => void) => void) { ctx.ui.setFooter((tui: any, theme: any, footerData: any) => { const unsub = footerData.onBranchChange(() => tui.requestRender()); onUnsub(unsub); return { dispose: unsub, invalidate() {}, render(width: number): string[] { const model = shortModelName(ctx.model?.name); const usage = ctx.getContextUsage(); const contextWindow = ctx.model?.contextWindow || 0; let usageStr = "–"; if (usage?.percent != null) { const pct = `${Math.round(usage.percent)}%`; if (contextWindow > 0) { usageStr = `${pct} / ${formatTokens(contextWindow)}`; } else { usageStr = pct; } } const dir = shortDir(ctx.cwd); const thinking = thinkingIndicator(pi.getThinkingLevel?.(), theme); const sep = theme.fg("dim", " | "); const modelStr = theme.fg("accent", theme.bold(model)); const leftContent = ` ` + modelStr + sep + theme.fg("dim", usageStr) + sep + theme.fg("dim", dir); const rightContent = thinking + ` `; const leftWidth = visibleWidth(leftContent); const rightWidth = visibleWidth(rightContent); const gap = Math.max(1, width - leftWidth - rightWidth); const line = leftContent + " ".repeat(gap) + rightContent; return [truncateToWidth(line, width, "")]; }, }; }); } export default function (pi: ExtensionAPI) { let branchUnsub: (() => void) | null = null; pi.on("session_start", async (_event, ctx) => { applyExtensionDefaults(import.meta.url, ctx); setupFooter(pi, ctx, (unsub) => { branchUnsub = unsub; }); }); // No tool_call blocking — core auto-compaction handles compaction properly // via auto_compaction_start/end events which trigger UI rebuild. // Footer no longer shows context warnings — memory-cycle.ts handles // proactive compaction with two-phase inject (70% prep, 80% hard stop). // The footer just renders the percentage in the status bar. pi.on("session_shutdown", async () => { if (branchUnsub) { branchUnsub(); branchUnsub = null; } }); }