a1f8751c0ec8668ad76c55b08ee6a6e6c7101a4a
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
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()]