pi-extensions 0.1.23 → 0.1.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensions",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "keywords": [
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5 - 2026-04-09
4
+ - Keep recursive subagent session scanning in `/usage`
5
+ - Remove the deduped/raw mode toggle and keep the deduped view as the default behavior
6
+
3
7
  ## 0.1.4 - 2026-04-09
4
8
  - Scan session files recursively so nested subagent runs are included in `/usage`
5
9
  - Add deduped vs raw counting modes to compare copied branch history against raw file totals
@@ -65,15 +65,6 @@ In Pi, run:
65
65
 
66
66
  Use `Tab` or `←`/`→` to switch between periods.
67
67
 
68
- ### Count Modes
69
-
70
- | Mode | Definition |
71
- |------|------------|
72
- | **Deduped** | Default. Deduplicates copied assistant history across branched session files |
73
- | **Raw** | Counts every assistant message found in every session file |
74
-
75
- Both modes scan nested session files recursively, so subagent runs are included.
76
-
77
68
  ### Timezone
78
69
 
79
70
  Time periods are calculated in the local timezone where Pi runs. If you want to override it, set the `TZ` environment variable (IANA timezone, e.g. `TZ=UTC` or `TZ=America/New_York`) before launching Pi.
@@ -96,9 +87,6 @@ Time periods are calculated in the local timezone where Pi runs. If you want to
96
87
  | Key | Action |
97
88
  |-----|--------|
98
89
  | `Tab` / `←` `→` | Switch time period |
99
- | `m` | Cycle count mode |
100
- | `d` | Switch to deduped mode |
101
- | `r` | Switch to raw mode |
102
90
  | `↑` `↓` | Select provider |
103
91
  | `Enter` / `Space` | Expand/collapse provider to show models |
104
92
  | `q` / `Esc` | Close |
@@ -125,9 +113,7 @@ The "Cache" column combines both read and write tokens.
125
113
 
126
114
  Statistics are parsed recursively from session files in `~/.pi/agent/sessions/`, including nested subagent runs such as `run-0/` directories. Each session is a JSONL file containing message entries with usage data.
127
115
 
128
- In **Deduped** mode, assistant messages duplicated across branched session files are deduplicated by timestamp + total tokens (matching the extension's previous behavior and keeping totals comparable with earlier releases).
129
-
130
- In **Raw** mode, every assistant message found in every session file is counted.
116
+ Assistant messages duplicated across branched session files are deduplicated by timestamp + total tokens, matching the extension's previous behavior while still including recursive subagent sessions.
131
117
 
132
118
  Respects the `PI_CODING_AGENT_DIR` environment variable if set.
133
119
 
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Shows an inline view with usage stats grouped by provider.
5
5
  * - Tab cycles: Today → This Week → All Time
6
- * - D toggles deduped view, R toggles raw view, M cycles both
7
6
  * - Arrow keys navigate providers
8
7
  * - Enter expands/collapses to show models
9
8
  */
@@ -57,8 +56,6 @@ interface UsageData {
57
56
  }
58
57
 
59
58
  type TabName = "today" | "thisWeek" | "allTime";
60
- type UsageCountMode = "deduped" | "raw";
61
- type UsageDataByMode = Record<UsageCountMode, UsageData>;
62
59
 
63
60
  // =============================================================================
64
61
  // Column Configuration
@@ -136,8 +133,7 @@ interface SessionMessage {
136
133
 
137
134
  interface ParsedSessionFile {
138
135
  sessionId: string;
139
- rawMessages: SessionMessage[];
140
- dedupedMessages: SessionMessage[];
136
+ messages: SessionMessage[];
141
137
  }
142
138
 
143
139
  async function parseSessionFile(
@@ -149,8 +145,7 @@ async function parseSessionFile(
149
145
  const content = await readFile(filePath, "utf8");
150
146
  if (signal?.aborted) return null;
151
147
  const lines = content.trim().split("\n");
152
- const rawMessages: SessionMessage[] = [];
153
- const dedupedMessages: SessionMessage[] = [];
148
+ const messages: SessionMessage[] = [];
154
149
  let sessionId = "";
155
150
 
156
151
  for (let i = 0; i < lines.length; i++) {
@@ -175,7 +170,14 @@ async function parseSessionFile(
175
170
  const fallbackTs = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
176
171
  const timestamp = msg.timestamp || (Number.isNaN(fallbackTs) ? 0 : fallbackTs);
177
172
 
178
- const sessionMessage: SessionMessage = {
173
+ // Deduplicate copied history across branched session files.
174
+ // Keep the existing ccusage-style hash so current totals remain comparable.
175
+ const totalTokens = input + output + cacheRead + cacheWrite;
176
+ const hash = `${timestamp}:${totalTokens}`;
177
+ if (seenHashes.has(hash)) continue;
178
+ seenHashes.add(hash);
179
+
180
+ messages.push({
179
181
  provider: msg.provider,
180
182
  model: msg.model,
181
183
  cost: msg.usage.cost?.total || 0,
@@ -184,16 +186,7 @@ async function parseSessionFile(
184
186
  cacheRead,
185
187
  cacheWrite,
186
188
  timestamp,
187
- };
188
- rawMessages.push(sessionMessage);
189
-
190
- // Deduplicate copied history across branched session files.
191
- // Keep the existing ccusage-style hash so current totals remain comparable.
192
- const totalTokens = input + output + cacheRead + cacheWrite;
193
- const hash = `${timestamp}:${totalTokens}`;
194
- if (seenHashes.has(hash)) continue;
195
- seenHashes.add(hash);
196
- dedupedMessages.push(sessionMessage);
189
+ });
197
190
  }
198
191
  }
199
192
  } catch {
@@ -201,7 +194,7 @@ async function parseSessionFile(
201
194
  }
202
195
  }
203
196
 
204
- return sessionId ? { sessionId, rawMessages, dedupedMessages } : null;
197
+ return sessionId ? { sessionId, messages } : null;
205
198
  } catch {
206
199
  return null;
207
200
  }
@@ -307,7 +300,7 @@ function addMessagesToUsageData(
307
300
  if (sessionContributed.allTime) data.allTime.totals.sessions++;
308
301
  }
309
302
 
310
- async function collectUsageData(signal?: AbortSignal): Promise<UsageDataByMode | null> {
303
+ async function collectUsageData(signal?: AbortSignal): Promise<UsageData | null> {
311
304
  const startOfToday = new Date();
312
305
  startOfToday.setHours(0, 0, 0, 0);
313
306
  const todayMs = startOfToday.getTime();
@@ -320,10 +313,7 @@ async function collectUsageData(signal?: AbortSignal): Promise<UsageDataByMode |
320
313
  startOfWeek.setHours(0, 0, 0, 0);
321
314
  const weekStartMs = startOfWeek.getTime();
322
315
 
323
- const data: UsageDataByMode = {
324
- deduped: emptyUsageData(),
325
- raw: emptyUsageData(),
326
- };
316
+ const data = emptyUsageData();
327
317
 
328
318
  const sessionFiles = await getAllSessionFiles(signal);
329
319
  if (signal?.aborted) return null;
@@ -335,8 +325,7 @@ async function collectUsageData(signal?: AbortSignal): Promise<UsageDataByMode |
335
325
  if (signal?.aborted) return null;
336
326
  if (!parsed) continue;
337
327
 
338
- addMessagesToUsageData(data.raw, parsed.sessionId, parsed.rawMessages, todayMs, weekStartMs);
339
- addMessagesToUsageData(data.deduped, parsed.sessionId, parsed.dedupedMessages, todayMs, weekStartMs);
328
+ addMessagesToUsageData(data, parsed.sessionId, parsed.messages, todayMs, weekStartMs);
340
329
 
341
330
  await new Promise<void>((resolve) => setImmediate(resolve));
342
331
  }
@@ -395,17 +384,9 @@ const TAB_LABELS: Record<TabName, string> = {
395
384
 
396
385
  const TAB_ORDER: TabName[] = ["today", "thisWeek", "allTime"];
397
386
 
398
- const MODE_LABELS: Record<UsageCountMode, string> = {
399
- deduped: "Deduped",
400
- raw: "Raw",
401
- };
402
-
403
- const MODE_ORDER: UsageCountMode[] = ["deduped", "raw"];
404
-
405
387
  class UsageComponent {
406
388
  private activeTab: TabName = "allTime";
407
- private activeMode: UsageCountMode = "deduped";
408
- private data: UsageDataByMode;
389
+ private data: UsageData;
409
390
  private selectedIndex = 0;
410
391
  private expanded = new Set<string>();
411
392
  private providerOrder: string[] = [];
@@ -413,7 +394,7 @@ class UsageComponent {
413
394
  private requestRender: () => void;
414
395
  private done: () => void;
415
396
 
416
- constructor(theme: Theme, data: UsageDataByMode, requestRender: () => void, done: () => void) {
397
+ constructor(theme: Theme, data: UsageData, requestRender: () => void, done: () => void) {
417
398
  this.theme = theme;
418
399
  this.requestRender = requestRender;
419
400
  this.done = done;
@@ -421,25 +402,14 @@ class UsageComponent {
421
402
  this.updateProviderOrder();
422
403
  }
423
404
 
424
- private getActiveStats(): TimeFilteredStats {
425
- return this.data[this.activeMode][this.activeTab];
426
- }
427
-
428
405
  private updateProviderOrder(): void {
429
- const stats = this.getActiveStats();
406
+ const stats = this.data[this.activeTab];
430
407
  this.providerOrder = Array.from(stats.providers.entries())
431
408
  .sort((a, b) => b[1].cost - a[1].cost)
432
409
  .map(([name]) => name);
433
410
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.providerOrder.length - 1));
434
411
  }
435
412
 
436
- private cycleMode(step: 1 | -1): void {
437
- const idx = MODE_ORDER.indexOf(this.activeMode);
438
- this.activeMode = MODE_ORDER[(idx + step + MODE_ORDER.length) % MODE_ORDER.length]!;
439
- this.updateProviderOrder();
440
- this.requestRender();
441
- }
442
-
443
413
  handleInput(data: string): void {
444
414
  if (matchesKey(data, "escape") || matchesKey(data, "q")) {
445
415
  this.done();
@@ -456,20 +426,6 @@ class UsageComponent {
456
426
  this.activeTab = TAB_ORDER[(idx - 1 + TAB_ORDER.length) % TAB_ORDER.length]!;
457
427
  this.updateProviderOrder();
458
428
  this.requestRender();
459
- } else if (matchesKey(data, "m")) {
460
- this.cycleMode(1);
461
- } else if (matchesKey(data, "d")) {
462
- if (this.activeMode !== "deduped") {
463
- this.activeMode = "deduped";
464
- this.updateProviderOrder();
465
- this.requestRender();
466
- }
467
- } else if (matchesKey(data, "r")) {
468
- if (this.activeMode !== "raw") {
469
- this.activeMode = "raw";
470
- this.updateProviderOrder();
471
- this.requestRender();
472
- }
473
429
  } else if (matchesKey(data, "up")) {
474
430
  if (this.selectedIndex > 0) {
475
431
  this.selectedIndex--;
@@ -501,7 +457,6 @@ class UsageComponent {
501
457
  return [
502
458
  ...this.renderTitle(),
503
459
  ...this.renderTabs(),
504
- ...this.renderModes(),
505
460
  ...this.renderHeader(),
506
461
  ...this.renderRows(),
507
462
  ...this.renderTotals(),
@@ -520,19 +475,7 @@ class UsageComponent {
520
475
  const label = TAB_LABELS[tab];
521
476
  return tab === this.activeTab ? th.fg("accent", `[${label}]`) : th.fg("dim", ` ${label} `);
522
477
  }).join(" ");
523
- return [tabs];
524
- }
525
-
526
- private renderModes(): string[] {
527
- const th = this.theme;
528
- const modes = MODE_ORDER.map((mode) => {
529
- const label = MODE_LABELS[mode];
530
- return mode === this.activeMode ? th.fg("accent", `[${label}]`) : th.fg("dim", ` ${label} `);
531
- }).join(" ");
532
- const note = this.activeMode === "deduped"
533
- ? "Dedupes copied branched-history messages. Recursive subagent sessions included."
534
- : "Counts raw message totals from all session files. Recursive subagent sessions included.";
535
- return [modes, th.fg("dim", note), ""];
478
+ return [tabs, th.fg("dim", "Dedupes copied branched-history messages. Recursive subagent sessions included."), ""];
536
479
  }
537
480
 
538
481
  private renderHeader(): string[] {
@@ -573,7 +516,7 @@ class UsageComponent {
573
516
 
574
517
  private renderRows(): string[] {
575
518
  const th = this.theme;
576
- const stats = this.getActiveStats();
519
+ const stats = this.data[this.activeTab];
577
520
  const lines: string[] = [];
578
521
 
579
522
  if (this.providerOrder.length === 0) {
@@ -611,7 +554,7 @@ class UsageComponent {
611
554
 
612
555
  private renderTotals(): string[] {
613
556
  const th = this.theme;
614
- const stats = this.getActiveStats();
557
+ const stats = this.data[this.activeTab];
615
558
 
616
559
  let totalRow = padRight(th.bold("Total"), NAME_COL_WIDTH);
617
560
  for (const col of DATA_COLUMNS) {
@@ -623,7 +566,7 @@ class UsageComponent {
623
566
  }
624
567
 
625
568
  private renderHelp(): string[] {
626
- return [this.theme.fg("dim", "[Tab/←→] period [m/d/r] count mode [↑↓] select [Enter] expand [q] close")];
569
+ return [this.theme.fg("dim", "[Tab/←→] period [↑↓] select [Enter] expand [q] close")];
627
570
  }
628
571
 
629
572
  invalidate(): void {}
@@ -642,7 +585,7 @@ export default function (pi: ExtensionAPI) {
642
585
  return;
643
586
  }
644
587
 
645
- const data = await ctx.ui.custom<UsageDataByMode | null>((tui, theme, _kb, done) => {
588
+ const data = await ctx.ui.custom<UsageData | null>((tui, theme, _kb, done) => {
646
589
  const loader = new CancellableLoader(
647
590
  tui,
648
591
  (s: string) => theme.fg("accent", s),
@@ -650,7 +593,7 @@ export default function (pi: ExtensionAPI) {
650
593
  "Loading Usage..."
651
594
  );
652
595
  let finished = false;
653
- const finish = (value: UsageDataByMode | null) => {
596
+ const finish = (value: UsageData | null) => {
654
597
  if (finished) return;
655
598
  finished = true;
656
599
  loader.dispose();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-usage-extension",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Usage statistics dashboard for Pi sessions.",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Mustier",