/** * Clean UI: big pixel "PI" header + two-line footer. * All colors are derived from the active theme (no theme changes). */ import type { AssistantMessage } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent"; import { mixColors, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; // ── Logo ──────────────────────────────────────────────────────────────── // Each cell is drawn 2 chars wide. "#" = filled. const LOGO = [ "##### ##", "## ## ##", "## ## ##", "##### ##", "## ##", "## ##", ]; function cwdLabel(cwd: string): string { const home = process.env.HOME; return home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd; } function center(line: string, width: number): string { const pad = Math.max(0, Math.floor((width - visibleWidth(line)) / 2)); return truncateToWidth(" ".repeat(pad) + line, width); } function renderLogo(theme: Theme): string[] { const rows = LOGO.length; const cols = LOGO[0].length * 2; // char width const filled = (r: number, c: number) => r >= 0 && r < rows && c >= 0 && c < cols && LOGO[r][Math.floor(c / 2)] === "#"; // Gradient from the theme's accent: lighter top-left → deeper bottom-right. let paint: (t: number, s: string) => string; let shadow: (s: string) => string; try { const { accent, text, background } = theme.colors as any; const light = mixColors(accent, text, 0.45); const dark = mixColors(accent, background, 0.35); const shade = mixColors(accent, background, 0.6); paint = (t, s) => theme.style(s, { fg: mixColors(light, dark, Math.min(1, Math.max(0, t))) } as any); shadow = (s) => theme.style(s, { fg: shade } as any); } catch { paint = (_t, s) => theme.fg("accent", s); shadow = (s) => theme.fg("borderAccent", s); } const out: string[] = []; // +1 row / +1 col for the drop shadow for (let r = 0; r <= rows; r++) { let line = ""; for (let c = 0; c <= cols; c++) { if (filled(r, c)) { line += paint((c / cols) * 0.6 + (r / rows) * 0.4, "█"); } else if (filled(r - 1, c - 1)) { line += shadow("░"); } else { line += " "; } } out.push(line); } return out; } // ── Footer helpers ────────────────────────────────────────────────────── function row(left: string, right: string, width: number): string { const gap = width - visibleWidth(left) - visibleWidth(right); if (gap < 1) return truncateToWidth(`${left} ${right}`, width); return left + " ".repeat(gap) + right; } function fmtK(n: number): string { return n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`; } export default function (pi: ExtensionAPI) { let changedFiles = 0; let requestRender: (() => void) | undefined; const refreshGit = async (ctx: ExtensionContext) => { const res = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd }).catch(() => undefined); changedFiles = res && res.code === 0 ? res.stdout.split("\n").filter((l) => l.trim()).length : 0; requestRender?.(); }; pi.on("message_end", () => requestRender?.()); pi.on("agent_settled", (_e, ctx) => void refreshGit(ctx)); pi.on("session_start", (_event, ctx) => { if (ctx.mode !== "tui") return; // Header: logo + cwd, centered ctx.ui.setHeader((_tui, theme) => ({ render(width: number): string[] { return [ "", ...renderLogo(theme).map((l) => center(l, width)), center(theme.fg("accent", cwdLabel(ctx.cwd)), width), "", ]; }, invalidate() {}, })); // Footer: two lines, left/right aligned ctx.ui.setFooter((tui, theme, footerData) => { requestRender = () => tui.requestRender(); const unsub = footerData.onBranchChange(() => { void refreshGit(ctx); tui.requestRender(); }); void refreshGit(ctx); return { dispose() { unsub(); requestRender = undefined; }, invalidate() {}, render(width: number): string[] { let cost = 0; for (const e of ctx.sessionManager.getBranch()) { if (e.type === "message" && e.message.role === "assistant") { cost += (e.message as AssistantMessage).usage?.cost?.total ?? 0; } } const usage = ctx.getContextUsage(); const window = usage?.contextWindow ?? ctx.model?.contextWindow; const pct = usage?.percent != null ? Math.round(usage.percent) : 0; const ctxStr = window ? `${pct}%/${fmtK(window)}` : "ctx ?"; const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "no model"; const thinking = pi.getThinkingLevel(); const branch = footerData.getGitBranch(); const git = branch ? `${branch}${changedFiles ? ` · ${changedFiles} file${changedFiles === 1 ? "" : "s"} changed` : ""}` : ""; return [ row(theme.fg("text", cwdLabel(ctx.cwd)), theme.fg("muted", `${model} · ${thinking}`), width), row(theme.fg("dim", `${ctxStr} · $${cost.toFixed(2)}`), theme.fg("dim", git), width), ]; }, }; }); }); }