/**
* 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),
];
},
};
});
});
}