@po.dev/pi-usage-dashboard 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +120 -10
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -39,15 +39,54 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
39
39
|
import { homedir, tmpdir } from "node:os";
|
|
40
40
|
import { join } from "node:path";
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Footer Settings
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
interface FooterSettings {
|
|
47
|
+
showSession: boolean;
|
|
48
|
+
showToday: boolean;
|
|
49
|
+
showContext: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const DEFAULT_FOOTER_SETTINGS: FooterSettings = {
|
|
53
|
+
showSession: true,
|
|
54
|
+
showToday: false,
|
|
55
|
+
showContext: true,
|
|
56
|
+
};
|
|
43
57
|
|
|
44
|
-
const
|
|
58
|
+
const FOOTER_SETTING_ITEMS: { key: keyof FooterSettings; label: string; description: string }[] = [
|
|
59
|
+
{ key: "showSession", label: "Show session usage", description: "Cost & tokens for the current session" },
|
|
60
|
+
{ key: "showToday", label: "Show today's usage", description: "Cost & tokens aggregated for today" },
|
|
61
|
+
{ key: "showContext", label: "Show context %", description: "Context window usage percentage" },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function footerSettingsPath(): string {
|
|
65
|
+
return join(getAgentDir(), "token-dashboard", "footer-settings.json");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function loadFooterSettings(): FooterSettings {
|
|
69
|
+
try {
|
|
70
|
+
const raw: unknown = JSON.parse(readFileSync(footerSettingsPath(), "utf8"));
|
|
71
|
+
return typeof raw === "object" && raw !== null ? { ...DEFAULT_FOOTER_SETTINGS, ...(raw as Partial<FooterSettings>) } : { ...DEFAULT_FOOTER_SETTINGS };
|
|
72
|
+
} catch { return { ...DEFAULT_FOOTER_SETTINGS }; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function saveFooterSettings(settings: FooterSettings): void {
|
|
76
|
+
mkdirSync(join(getAgentDir(), "token-dashboard"), { recursive: true });
|
|
77
|
+
writeFileSync(footerSettingsPath(), JSON.stringify(settings, null, 2));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ViewMode = "table" | "insights" | "history" | "graph" | "settings";
|
|
81
|
+
|
|
82
|
+
const VIEW_CYCLE: ViewMode[] = ["graph", "table", "insights", "history", "settings"];
|
|
45
83
|
|
|
46
84
|
const VIEW_LABELS: Record<ViewMode, string> = {
|
|
47
85
|
graph: "Overview",
|
|
48
86
|
table: "Usage",
|
|
49
87
|
insights: "Insights",
|
|
50
88
|
history: "History",
|
|
89
|
+
settings: "Settings",
|
|
51
90
|
};
|
|
52
91
|
|
|
53
92
|
type PromptItem = { label: string; chars: number; text?: string };
|
|
@@ -388,8 +427,11 @@ class UsageComponent {
|
|
|
388
427
|
private historySelected: string | null = null;
|
|
389
428
|
private currentSessionId: string;
|
|
390
429
|
private currentPrompt: string;
|
|
430
|
+
private footerSettings: FooterSettings;
|
|
431
|
+
private settingsIndex = 0;
|
|
432
|
+
private onSettingsChange: (settings: FooterSettings) => void;
|
|
391
433
|
|
|
392
|
-
constructor(theme: Theme, data: UsageData, prompt: string, currentSessionId: string, requestRender: () => void, done: () => void) {
|
|
434
|
+
constructor(theme: Theme, data: UsageData, prompt: string, currentSessionId: string, requestRender: () => void, done: () => void, footerSettings: FooterSettings, onSettingsChange: (settings: FooterSettings) => void) {
|
|
393
435
|
this.theme = theme;
|
|
394
436
|
this.requestRender = requestRender;
|
|
395
437
|
this.done = done;
|
|
@@ -397,6 +439,8 @@ class UsageComponent {
|
|
|
397
439
|
this.currentSessionId = currentSessionId;
|
|
398
440
|
this.currentPrompt = prompt;
|
|
399
441
|
this.promptSections = promptSections(prompt);
|
|
442
|
+
this.footerSettings = { ...footerSettings };
|
|
443
|
+
this.onSettingsChange = onSettingsChange;
|
|
400
444
|
this.updateProviderOrder();
|
|
401
445
|
}
|
|
402
446
|
|
|
@@ -524,6 +568,7 @@ class UsageComponent {
|
|
|
524
568
|
}
|
|
525
569
|
if (this.viewMode === "insights" && this.handleInsightInput(data)) return;
|
|
526
570
|
if (this.viewMode === "history" && this.handleHistoryInput(data)) return;
|
|
571
|
+
if (this.viewMode === "settings" && this.handleSettingsInput(data)) return;
|
|
527
572
|
|
|
528
573
|
if (matchesKey(data, "right")) {
|
|
529
574
|
const idx = TAB_ORDER.indexOf(this.activeTab);
|
|
@@ -636,6 +681,25 @@ class UsageComponent {
|
|
|
636
681
|
return true;
|
|
637
682
|
}
|
|
638
683
|
|
|
684
|
+
private handleSettingsInput(data: string): boolean {
|
|
685
|
+
if (matchesKey(data, "up")) {
|
|
686
|
+
this.settingsIndex = Math.max(0, this.settingsIndex - 1);
|
|
687
|
+
} else if (matchesKey(data, "down")) {
|
|
688
|
+
this.settingsIndex = Math.min(FOOTER_SETTING_ITEMS.length - 1, this.settingsIndex + 1);
|
|
689
|
+
} else if (matchesKey(data, "enter") || matchesKey(data, "space")) {
|
|
690
|
+
const item = FOOTER_SETTING_ITEMS[this.settingsIndex];
|
|
691
|
+
if (item) {
|
|
692
|
+
this.footerSettings = { ...this.footerSettings, [item.key]: !this.footerSettings[item.key] };
|
|
693
|
+
saveFooterSettings(this.footerSettings);
|
|
694
|
+
this.onSettingsChange(this.footerSettings);
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
this.requestRender();
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
|
|
639
703
|
private handleHistoryInput(data: string): boolean {
|
|
640
704
|
if (this.historySelected && (matchesKey(data, "up") || matchesKey(data, "down") || matchesKey(data, "enter"))) return this.handleInsightInput(data);
|
|
641
705
|
const sessions = Array.from(this.data.sessions.values()).sort((a, b) => b.timestamp - a.timestamp);
|
|
@@ -723,6 +787,10 @@ class UsageComponent {
|
|
|
723
787
|
return clampLines([...this.renderTitle(width), ...this.renderHistory(width)], width);
|
|
724
788
|
}
|
|
725
789
|
|
|
790
|
+
if (this.viewMode === "settings") {
|
|
791
|
+
return clampLines([...this.renderTitle(width), ...this.renderSettings(width)], width);
|
|
792
|
+
}
|
|
793
|
+
|
|
726
794
|
const layout = getTableLayout(width);
|
|
727
795
|
return clampLines(
|
|
728
796
|
[
|
|
@@ -1123,6 +1191,21 @@ class UsageComponent {
|
|
|
1123
1191
|
return [th.fg("border", "─".repeat(layout.tableWidth)), totalRow, ""];
|
|
1124
1192
|
}
|
|
1125
1193
|
|
|
1194
|
+
private renderSettings(width: number): string[] {
|
|
1195
|
+
const th = this.theme;
|
|
1196
|
+
const lines = [th.bold("Footer display settings"), th.fg("dim", "Choose what appears in the usage footer · changes apply immediately"), ""];
|
|
1197
|
+
for (let i = 0; i < FOOTER_SETTING_ITEMS.length; i++) {
|
|
1198
|
+
const item = FOOTER_SETTING_ITEMS[i]!;
|
|
1199
|
+
const selected = i === this.settingsIndex;
|
|
1200
|
+
const checked = this.footerSettings[item.key] ? th.fg("success", "☑") : th.fg("dim", "☐");
|
|
1201
|
+
const marker = selected ? th.fg("accent", "▸ ") : " ";
|
|
1202
|
+
const label = selected ? th.fg("accent", item.label) : item.label;
|
|
1203
|
+
lines.push(`${marker}${checked} ${label} ${th.fg("dim", item.description)}`);
|
|
1204
|
+
}
|
|
1205
|
+
lines.push("", th.fg("dim", "[↑↓] select [Enter/Space] toggle [Tab] view [q] close"));
|
|
1206
|
+
return lines;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1126
1209
|
private renderFormulaNote(width: number): string[] {
|
|
1127
1210
|
const line = pickFittingText(width, [
|
|
1128
1211
|
"Tokens = Input + Output + CacheWrite · ↑In = Input + CacheWrite (as of 0.2.0)",
|
|
@@ -1192,17 +1275,24 @@ function footerModelLabel(ctx: Pick<ExtensionCommandContext, "model" | "thinking
|
|
|
1192
1275
|
return model.reasoning && ctx.thinkingLevel ? `${label} • ${ctx.thinkingLevel}` : label;
|
|
1193
1276
|
}
|
|
1194
1277
|
|
|
1195
|
-
function setUsageFooter(ctx: Pick<ExtensionCommandContext, "ui" | "model" | "thinkingLevel" | "getContextUsage">, totals: TotalStats): void {
|
|
1278
|
+
function setUsageFooter(ctx: Pick<ExtensionCommandContext, "ui" | "model" | "thinkingLevel" | "getContextUsage">, totals: TotalStats, sessionTotals: TotalStats | null, settings: FooterSettings): void {
|
|
1196
1279
|
ctx.ui.setFooter((_tui, theme) => ({
|
|
1197
1280
|
invalidate() {},
|
|
1198
1281
|
render(width: number): string[] {
|
|
1199
1282
|
const context = ctx.getContextUsage();
|
|
1200
1283
|
const compactAt = context ? context.contextWindow - 16_384 : 0;
|
|
1201
1284
|
const left = context?.tokens === null ? "?" : context ? formatTokens(Math.max(0, compactAt - context.tokens)) : "?";
|
|
1202
|
-
const contextStatus = context
|
|
1285
|
+
const contextStatus = settings.showContext && context
|
|
1203
1286
|
? " · " + theme.fg("warning", `${context.percent?.toFixed(1) ?? "?"}%`) + theme.fg("dim", "/") + theme.fg("success", `${left} left`) + " " + theme.fg("accent", "(auto)")
|
|
1204
1287
|
: "";
|
|
1205
|
-
const
|
|
1288
|
+
const bodyParts: string[] = [];
|
|
1289
|
+
if (settings.showSession && sessionTotals) {
|
|
1290
|
+
bodyParts.push(theme.fg("success", "(Session)") + " · " + theme.fg("accent", formatCost(sessionTotals.cost)) + " · " + theme.fg("text", `${formatTokens(sessionTotals.tokens.total)} tokens`) + " · " + theme.fg("success", `↑${formatTokens(sessionTotals.tokens.input + sessionTotals.tokens.cacheWrite)}`) + " · " + theme.fg("warning", `↓${formatTokens(sessionTotals.tokens.output)}`));
|
|
1291
|
+
}
|
|
1292
|
+
if (settings.showToday) {
|
|
1293
|
+
bodyParts.push(theme.fg("accent", "(Today)") + " · " + theme.fg("accent", formatCost(totals.cost)) + " · " + theme.fg("text", `${formatTokens(totals.tokens.total)} tokens`) + " · " + theme.fg("success", `↑${formatTokens(totals.tokens.input + totals.tokens.cacheWrite)}`) + " · " + theme.fg("warning", `↓${formatTokens(totals.tokens.output)}`) + " · " + theme.fg("thinkingHigh", `${formatTokens(totals.tokens.cacheRead + totals.tokens.cacheWrite)} cache`));
|
|
1294
|
+
}
|
|
1295
|
+
const usage = theme.fg("thinkingHigh", "Usage:") + (bodyParts.length ? " " + bodyParts.join(" ") : "") + contextStatus;
|
|
1206
1296
|
const model = theme.fg("accent", footerModelLabel(ctx));
|
|
1207
1297
|
const gap = width - visibleWidth(usage) - visibleWidth(model);
|
|
1208
1298
|
return [gap >= 2 ? usage + " ".repeat(gap) + model : truncateToWidth(usage, width)];
|
|
@@ -1211,6 +1301,9 @@ function setUsageFooter(ctx: Pick<ExtensionCommandContext, "ui" | "model" | "thi
|
|
|
1211
1301
|
}
|
|
1212
1302
|
|
|
1213
1303
|
export default function (pi: ExtensionAPI) {
|
|
1304
|
+
let sessionTotals: TotalStats = { sessions: 0, messages: 0, cost: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
1305
|
+
let footerSettings = loadFooterSettings();
|
|
1306
|
+
|
|
1214
1307
|
const refreshFooter = (ctx: Pick<ExtensionCommandContext, "hasUI" | "ui" | "model" | "thinkingLevel" | "getContextUsage">) => {
|
|
1215
1308
|
if (!ctx.hasUI) return;
|
|
1216
1309
|
void collectUsageData().then((data) => {
|
|
@@ -1221,14 +1314,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
1221
1314
|
totals.messages += stats.messages; totals.cost += stats.cost;
|
|
1222
1315
|
totals.tokens.total += stats.tokens.total; totals.tokens.input += stats.tokens.input; totals.tokens.output += stats.tokens.output; totals.tokens.cacheRead += stats.tokens.cacheRead; totals.tokens.cacheWrite += stats.tokens.cacheWrite;
|
|
1223
1316
|
}
|
|
1224
|
-
setUsageFooter(ctx, totals);
|
|
1317
|
+
setUsageFooter(ctx, totals, sessionTotals, footerSettings);
|
|
1225
1318
|
});
|
|
1226
1319
|
};
|
|
1227
1320
|
const snapshotPrompt = (ctx: { sessionManager: { getSessionId(): string }; getSystemPrompt(): string }) => {
|
|
1228
1321
|
try { savePromptSnapshot(ctx.sessionManager.getSessionId(), ctx.getSystemPrompt()); } catch { /* usage must never block Pi */ }
|
|
1229
1322
|
};
|
|
1230
|
-
pi.on("session_start", (_event, ctx) => {
|
|
1231
|
-
|
|
1323
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1324
|
+
sessionTotals = { sessions: 0, messages: 0, cost: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
1325
|
+
snapshotPrompt(ctx);
|
|
1326
|
+
refreshFooter(ctx);
|
|
1327
|
+
});
|
|
1328
|
+
pi.on("message_end", (event, ctx) => {
|
|
1329
|
+
if (event.message.role === "assistant") {
|
|
1330
|
+
const u = event.message.usage;
|
|
1331
|
+
sessionTotals.messages++;
|
|
1332
|
+
sessionTotals.cost += u.cost.total;
|
|
1333
|
+
sessionTotals.tokens.total += u.totalTokens;
|
|
1334
|
+
sessionTotals.tokens.input += u.input;
|
|
1335
|
+
sessionTotals.tokens.output += u.output;
|
|
1336
|
+
sessionTotals.tokens.cacheRead += u.cacheRead;
|
|
1337
|
+
sessionTotals.tokens.cacheWrite += u.cacheWrite;
|
|
1338
|
+
}
|
|
1339
|
+
snapshotPrompt(ctx);
|
|
1340
|
+
refreshFooter(ctx);
|
|
1341
|
+
});
|
|
1232
1342
|
pi.on("model_select", (_event, ctx) => { refreshFooter(ctx); });
|
|
1233
1343
|
pi.registerCommand("usage", {
|
|
1234
1344
|
description: "Show usage statistics dashboard",
|
|
@@ -1287,7 +1397,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1287
1397
|
container.addChild(new DynamicBorder((s: string) => theme.fg("border", s)));
|
|
1288
1398
|
container.addChild(new Spacer(1));
|
|
1289
1399
|
|
|
1290
|
-
const usage = new UsageComponent(theme, data, ctx.getSystemPrompt(), ctx.sessionManager.getSessionId(), () => tui.requestRender(), () => done());
|
|
1400
|
+
const usage = new UsageComponent(theme, data, ctx.getSystemPrompt(), ctx.sessionManager.getSessionId(), () => tui.requestRender(), () => done(), footerSettings, (newSettings) => { footerSettings = newSettings; refreshFooter(ctx); });
|
|
1291
1401
|
|
|
1292
1402
|
return {
|
|
1293
1403
|
render: (w: number) => {
|