Add footer renderer for TUI status display.

Displays git branch status, dirty state, extension statuses, model info,
cost, and context usage percentage in the footer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ace-pm 2026-04-15 16:17:24 +02:00
parent c18f67d278
commit ec39512960

View file

@ -0,0 +1,78 @@
import type { ExtensionContext, Theme, ReadonlyFooterDataProvider } from "@sf-run/pi-coding-agent";
import { truncateToWidth } from "@sf-run/pi-tui";
import { refreshGitStatus } from "./git.js";
import { rightAlign } from "./shared.js";
function getSessionStats(ctx: ExtensionContext) {
let cost = 0;
let cxPct = 0;
let cxColor: "success" | "warning" | "error" | "dim" = "dim";
try {
for (const entry of ctx.sessionManager.getEntries()) {
if (entry.type === "message") {
const msg = (entry as any).message;
if (msg?.role === "assistant" && msg.usage) {
cost += msg.usage.cost?.total || 0;
}
}
}
const cx = ctx.getContextUsage?.();
if (cx?.percent != null) {
cxPct = cx.percent;
cxColor = cxPct >= 85 ? "error" : cxPct >= 60 ? "warning" : "success";
}
} catch {
/* ignore */
}
return { cost, cxPct, cxColor };
}
export function renderFooter(
theme: Theme,
footerData: ReadonlyFooterDataProvider,
ctx: ExtensionContext,
width: number,
): string[] {
const git = refreshGitStatus(process.cwd());
const parts: string[] = [];
if (git.branch) {
const dirty = git.dirty
? theme.fg("error", "✗")
: git.untracked
? theme.fg("warning", "?")
: theme.fg("success", "✓");
const aheadBehind =
git.ahead || git.behind
? theme.fg("dim", `${git.ahead ? `${git.ahead}` : ""}${git.behind ? `${git.behind}` : ""}`)
: "";
parts.push(`${theme.fg("dim", "⎇")} ${git.branch} ${dirty}${aheadBehind ? ` ${aheadBehind}` : ""}`);
}
const statuses = Array.from(footerData.getExtensionStatuses().entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([, text]) => text)
.filter(Boolean)
.join(" ");
if (statuses) {
parts.push(theme.fg("dim", statuses));
}
const left = parts.join(theme.fg("dim", " │ "));
const rightParts: string[] = [];
if (ctx.model) {
rightParts.push(theme.fg("muted", `${ctx.model.provider}/${ctx.model.id}`));
}
const { cost, cxPct, cxColor } = getSessionStats(ctx);
if (cost > 0) rightParts.push(theme.fg("warning", `$${cost.toFixed(2)}`));
rightParts.push(theme.fg(cxColor, `${cxPct.toFixed(0)}%ctx`));
const right = rightParts.join(theme.fg("dim", " "));
const line = rightAlign(left, right, width);
return [truncateToWidth(line, width, theme.fg("dim", "…"))];
}