@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.
Files changed (2) hide show
  1. package/index.ts +120 -10
  2. 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
- type ViewMode = "table" | "insights" | "history" | "graph";
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 VIEW_CYCLE: ViewMode[] = ["graph", "table", "insights", "history"];
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 usage = theme.fg("thinkingHigh", "Usage:") + " " + 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`) + contextStatus;
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) => { snapshotPrompt(ctx); refreshFooter(ctx); });
1231
- pi.on("message_end", (_event, ctx) => { snapshotPrompt(ctx); refreshFooter(ctx); });
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@po.dev/pi-usage-dashboard",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Local token and cost dashboard for pi sessions",
5
5
  "keywords": [
6
6
  "pi-package",