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,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
|
-
|
|
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
|
|
package/usage-extension/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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<
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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 [
|
|
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<
|
|
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:
|
|
596
|
+
const finish = (value: UsageData | null) => {
|
|
654
597
|
if (finished) return;
|
|
655
598
|
finished = true;
|
|
656
599
|
loader.dispose();
|