a1f8751c0ec8668ad76c55b08ee6a6e6c7101a4a

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add better status footer to pi

Diff

This diff is truncated to protect this page.

  1diff --git a/dot_pi/agent/extensions/detailed-footer.ts b/dot_pi/agent/extensions/detailed-footer.ts
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..adee6394edec5fbf5d7f2a01ea7c52c1d995d797
  4--- /dev/null
  5+++ b/dot_pi/agent/extensions/detailed-footer.ts
  6@@ -0,0 +1,164 @@
  7+import { isAbsolute, relative, resolve, sep } from "node:path";
  8+import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
  9+import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
 10+
 11+type Usage = {
 12+	input?: number;
 13+	output?: number;
 14+	cacheRead?: number;
 15+	cacheWrite?: number;
 16+	cost?: { total?: number };
 17+};
 18+
 19+type UsageTotals = {
 20+	input: number;
 21+	output: number;
 22+	cacheRead: number;
 23+	cacheWrite: number;
 24+	cost: number;
 25+};
 26+
 27+const formatTokens = (count: number): string => {
 28+	if (count < 1_000) return count.toString();
 29+	if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`;
 30+	if (count < 1_000_000) return `${Math.round(count / 1_000)}k`;
 31+	if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
 32+	return `${Math.round(count / 1_000_000)}M`;
 33+};
 34+
 35+const addUsage = (totals: UsageTotals, usage: Usage | undefined): void => {
 36+	if (usage === undefined) return;
 37+	totals.input += usage.input ?? 0;
 38+	totals.output += usage.output ?? 0;
 39+	totals.cacheRead += usage.cacheRead ?? 0;
 40+	totals.cacheWrite += usage.cacheWrite ?? 0;
 41+	totals.cost += usage.cost?.total ?? 0;
 42+};
 43+
 44+const getUsageTotals = (ctx: ExtensionContext): UsageTotals => {
 45+	const totals: UsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
 46+
 47+	for (const entry of ctx.sessionManager.getEntries()) {
 48+		if (entry.type === "message") {
 49+			if (entry.message.role === "assistant" || entry.message.role === "toolResult") {
 50+				addUsage(totals, entry.message.usage);
 51+			}
 52+		} else if (entry.type === "branch_summary" || entry.type === "compaction") {
 53+			addUsage(totals, entry.usage);
 54+		}
 55+	}
 56+
 57+	return totals;
 58+};
 59+
 60+const formatCwd = (cwd: string): string => {
 61+	const home = process.env.HOME ?? process.env.USERPROFILE;
 62+	if (home === undefined) return cwd;
 63+
 64+	const relativeToHome = relative(resolve(home), resolve(cwd));
 65+	const isInsideHome =
 66+		relativeToHome === "" ||
 67+		(relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
 68+
 69+	return isInsideHome ? (relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`) : cwd;
 70+};
 71+
 72+const isSubscription = (ctx: ExtensionContext): boolean => {
 73+	const model = ctx.model;
 74+	if (model === undefined) return false;
 75+	if (model.provider === "kimi-coding") return true;
 76+
 77+	return ctx.modelRegistry.isUsingOAuth(model) && ctx.modelRegistry.getProvider(model.provider)?.auth.oauth?.isSubscription === true;
 78+};
 79+
 80+const sanitizeStatus = (status: string): string => status.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
 81+
 82+export default function detailedFooter(pi: ExtensionAPI): void {
 83+	pi.on("session_start", (_event, ctx) => {
 84+		if (ctx.mode !== "tui") return;
 85+
 86+		ctx.ui.setFooter((tui, theme, footerData) => ({
 87+			dispose: footerData.onBranchChange(() => tui.requestRender()),
 88+			invalidate() {},
 89+			render(width: number): string[] {
 90+				let location = formatCwd(ctx.cwd);
 91+				const branch = footerData.getGitBranch();
 92+				const sessionName = ctx.sessionManager.getSessionName();
 93+				if (branch) location += ` (${branch})`;
 94+				if (sessionName) location += ` • ${sessionName}`;
 95+
 96+				const totals = getUsageTotals(ctx);
 97+				const metrics: string[] = [];
 98+				const inputOutput = [
 99+					totals.input > 0 ? `${theme.fg("dim", "↑")}${theme.fg("text", formatTokens(totals.input))}` : undefined,
100+					totals.output > 0 ? `${theme.fg("dim", "↓")}${theme.fg("text", formatTokens(totals.output))}` : undefined,
101+				].filter((metric): metric is string => metric !== undefined);
102+				if (inputOutput.length > 0) metrics.push(inputOutput.join(" "));
103+
104+				let latestCacheHitRate: string | undefined;
105+				const latestAssistant = [...ctx.sessionManager.getEntries()]