@po.dev/pi-usage-dashboard 0.1.8 → 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 +99 -11
  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
+ };
57
+
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
+ }
43
67
 
44
- const VIEW_CYCLE: ViewMode[] = ["graph", "table", "insights", "history"];
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,20 +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, sessionTotals: TotalStats | null): 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 sessionPart = sessionTotals
1206
- ? 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)}`) + " "
1207
- : "";
1208
- const usage = theme.fg("thinkingHigh", "Usage:") + " " + sessionPart + 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;
1209
1296
  const model = theme.fg("accent", footerModelLabel(ctx));
1210
1297
  const gap = width - visibleWidth(usage) - visibleWidth(model);
1211
1298
  return [gap >= 2 ? usage + " ".repeat(gap) + model : truncateToWidth(usage, width)];
@@ -1215,6 +1302,7 @@ function setUsageFooter(ctx: Pick<ExtensionCommandContext, "ui" | "model" | "thi
1215
1302
 
1216
1303
  export default function (pi: ExtensionAPI) {
1217
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();
1218
1306
 
1219
1307
  const refreshFooter = (ctx: Pick<ExtensionCommandContext, "hasUI" | "ui" | "model" | "thinkingLevel" | "getContextUsage">) => {
1220
1308
  if (!ctx.hasUI) return;
@@ -1226,7 +1314,7 @@ export default function (pi: ExtensionAPI) {
1226
1314
  totals.messages += stats.messages; totals.cost += stats.cost;
1227
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;
1228
1316
  }
1229
- setUsageFooter(ctx, totals, sessionTotals);
1317
+ setUsageFooter(ctx, totals, sessionTotals, footerSettings);
1230
1318
  });
1231
1319
  };
1232
1320
  const snapshotPrompt = (ctx: { sessionManager: { getSessionId(): string }; getSystemPrompt(): string }) => {
@@ -1309,7 +1397,7 @@ export default function (pi: ExtensionAPI) {
1309
1397
  container.addChild(new DynamicBorder((s: string) => theme.fg("border", s)));
1310
1398
  container.addChild(new Spacer(1));
1311
1399
 
1312
- 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); });
1313
1401
 
1314
1402
  return {
1315
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.8",
3
+ "version": "0.1.9",
4
4
  "description": "Local token and cost dashboard for pi sessions",
5
5
  "keywords": [
6
6
  "pi-package",