pi-extensions 0.1.24 β 0.1.28
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/README.md +1 -1
- package/files-widget/index.ts +2 -2
- package/package.json +1 -1
- package/usage-extension/CHANGELOG.md +18 -0
- package/usage-extension/README.md +11 -4
- package/usage-extension/index.ts +245 -81
- package/usage-extension/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Personal extensions for the [Pi coding agent](https://github.com/badlogic/pi-mon
|
|
|
10
10
|
| [tab-status](tab-status/) | Manage as many parallel sessions as your mind can handle. Terminal tab indicators for <br>β
done / π§ stuck / π timed out |
|
|
11
11
|
| [ralph-wiggum](ralph-wiggum/) | Run arbitrarily-long tasks without diluting model attention. Flat version without subagents like [ralph-loop](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/ralph-loop) |
|
|
12
12
|
| [agent-guidance](agent-guidance/) | Switch between Claude/Codex/Gemini with model-specific guidance (CLAUDE.md, CODEX.md, GEMINI.md) |
|
|
13
|
-
| [/usage](usage-extension/) | π Usage statistics dashboard. See cost, tokens, and messages by provider/model across Today, This Week, All Time |
|
|
13
|
+
| [/usage](usage-extension/) | π Usage statistics dashboard. See cost, tokens, and messages by provider/model across Today, This Week, Last Week, and All Time β with a compact view for narrow terminals |
|
|
14
14
|
| [/paste](raw-paste/) | Paste editable text, not [paste #1 +21 lines]. Running `/paste` with optional keybinding |
|
|
15
15
|
| [/code](code-actions/) | Pick code blocks or inline snippets from assistant messages to copy, insert, or run with `/code` |
|
|
16
16
|
| [arcade](arcade/) | Play minigames while your tests run: πΎ sPIce-invaders, π» picman, π ping, π§© tetris, π mario-not |
|
package/files-widget/index.ts
CHANGED
|
@@ -66,7 +66,7 @@ export default function editorExtension(pi: ExtensionAPI): void {
|
|
|
66
66
|
},
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
pi.registerCommand("review", {
|
|
69
|
+
pi.registerCommand("readfiles-review", {
|
|
70
70
|
description: "Open tuicr to review changes and send feedback to agent",
|
|
71
71
|
handler: async (_args, ctx) => {
|
|
72
72
|
if (!hasCommand("tuicr")) {
|
|
@@ -101,7 +101,7 @@ export default function editorExtension(pi: ExtensionAPI): void {
|
|
|
101
101
|
},
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
pi.registerCommand("diff", {
|
|
104
|
+
pi.registerCommand("readfiles-diff", {
|
|
105
105
|
description: "Open critique to view diffs",
|
|
106
106
|
handler: async (args, ctx) => {
|
|
107
107
|
if (!hasCommand("bun")) {
|
package/package.json
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 - 2026-04-17
|
|
4
|
+
- Add a one-line formula footer to the `/usage` dashboard (`Tokens = Input + Output + CacheWrite Β· βIn = Input + CacheWrite`)
|
|
5
|
+
- README now calls out the 0.2.0 formula change explicitly under the columns table
|
|
6
|
+
|
|
7
|
+
## 0.2.0 - 2026-04-17
|
|
8
|
+
- Include `cacheWrite` in the main `Tokens` total and in the `βIn` column so providers like Anthropic that report fresh prompt work under `cacheWrite` are no longer undercounted
|
|
9
|
+
- Keep `cacheRead` out of `Tokens` so repeated cache hits do not swamp the dashboard
|
|
10
|
+
- Keep the `Cache` column as combined cache read + write tokens for reference
|
|
11
|
+
- Minor semver bump: the numbers shown under `Tokens` and `βIn` are now higher for Anthropic usage. Cost, `βOut`, and `Cache` are unchanged.
|
|
12
|
+
|
|
13
|
+
## 0.1.7 - 2026-04-09
|
|
14
|
+
- Prevent `/usage` from crashing in narrow terminals by switching to a compact responsive table and truncating every rendered line to the terminal width
|
|
15
|
+
- Thanks @markokocic
|
|
16
|
+
|
|
17
|
+
## 0.1.6 - 2026-04-09
|
|
18
|
+
- Add a "Last Week" time period tab
|
|
19
|
+
- Thanks @ttttmr
|
|
20
|
+
|
|
3
21
|
## 0.1.5 - 2026-04-09
|
|
4
22
|
- Keep recursive subagent session scanning in `/usage`
|
|
5
23
|
- Remove the deduped/raw mode toggle and keep the deduped view as the default behavior
|
|
@@ -7,7 +7,7 @@ A Pi extension that displays aggregated usage statistics across all sessions.
|
|
|
7
7
|
## Compatibility
|
|
8
8
|
|
|
9
9
|
- **Pi version:** 0.42.4+
|
|
10
|
-
- **Last updated:** 2026-04-
|
|
10
|
+
- **Last updated:** 2026-04-17 (0.2.1)
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
@@ -61,6 +61,7 @@ In Pi, run:
|
|
|
61
61
|
|--------|------------|
|
|
62
62
|
| **Today** | From midnight (00:00) today |
|
|
63
63
|
| **This Week** | From Monday 00:00 of the current week |
|
|
64
|
+
| **Last Week** | Previous week (Monday 00:00 β this Monday 00:00) |
|
|
64
65
|
| **All Time** | All recorded sessions |
|
|
65
66
|
|
|
66
67
|
Use `Tab` or `β`/`β` to switch between periods.
|
|
@@ -77,10 +78,14 @@ Time periods are calculated in the local timezone where Pi runs. If you want to
|
|
|
77
78
|
| **Sessions** | Number of unique sessions |
|
|
78
79
|
| **Msgs** | Number of assistant messages |
|
|
79
80
|
| **Cost** | Total cost in USD (from API response) |
|
|
80
|
-
| **Tokens** |
|
|
81
|
-
| **βIn** |
|
|
81
|
+
| **Tokens** | Fresh tokens for the turn: input + output + cache write |
|
|
82
|
+
| **βIn** | Fresh input tokens: input + cache write *(dimmed)* |
|
|
82
83
|
| **βOut** | Output tokens *(dimmed)* |
|
|
83
|
-
| **Cache** | Cache read + write tokens *(dimmed)* |
|
|
84
|
+
| **Cache** | Cache read + write tokens *(dimmed; informational)* |
|
|
85
|
+
|
|
86
|
+
> **As of 0.2.0:** `Tokens = Input + Output + CacheWrite` and `βIn = Input + CacheWrite`. `CacheRead` stays out of `Tokens` so repeated cache hits don't swamp the dashboard. The dashboard itself shows a one-line footer reminder.
|
|
87
|
+
|
|
88
|
+
On narrow terminals, `/usage` automatically switches to a compact table instead of overflowing the terminal. Hidden columns reappear as soon as you widen the terminal.
|
|
84
89
|
|
|
85
90
|
### Navigation
|
|
86
91
|
|
|
@@ -109,6 +114,8 @@ Cache token support varies by provider:
|
|
|
109
114
|
|
|
110
115
|
The "Cache" column combines both read and write tokens.
|
|
111
116
|
|
|
117
|
+
`Tokens` and `βIn` include cache writes but intentionally exclude cache reads. That keeps totals aligned with fresh/billed prompt work without letting repeated cache hits swamp the dashboard.
|
|
118
|
+
|
|
112
119
|
## Data Source
|
|
113
120
|
|
|
114
121
|
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.
|
package/usage-extension/index.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* /usage - Usage statistics dashboard
|
|
3
3
|
*
|
|
4
4
|
* Shows an inline view with usage stats grouped by provider.
|
|
5
|
-
* - Tab cycles: Today β This Week β All Time
|
|
5
|
+
* - Tab cycles: Today β This Week β Last Week β All Time
|
|
6
6
|
* - Arrow keys navigate providers
|
|
7
7
|
* - Enter expands/collapses to show models
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
11
11
|
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
12
|
-
import { CancellableLoader, Container, Spacer, matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
12
|
+
import { CancellableLoader, Container, Spacer, matchesKey, visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
13
13
|
import { readdir, readFile } from "node:fs/promises";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { homedir } from "node:os";
|
|
@@ -22,7 +22,8 @@ interface TokenStats {
|
|
|
22
22
|
total: number;
|
|
23
23
|
input: number;
|
|
24
24
|
output: number;
|
|
25
|
-
|
|
25
|
+
cacheRead: number;
|
|
26
|
+
cacheWrite: number;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
interface BaseStats {
|
|
@@ -52,10 +53,11 @@ interface TimeFilteredStats {
|
|
|
52
53
|
interface UsageData {
|
|
53
54
|
today: TimeFilteredStats;
|
|
54
55
|
thisWeek: TimeFilteredStats;
|
|
56
|
+
lastWeek: TimeFilteredStats;
|
|
55
57
|
allTime: TimeFilteredStats;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
type TabName = "today" | "thisWeek" | "allTime";
|
|
60
|
+
type TabName = "today" | "thisWeek" | "lastWeek" | "allTime";
|
|
59
61
|
|
|
60
62
|
// =============================================================================
|
|
61
63
|
// Column Configuration
|
|
@@ -68,23 +70,86 @@ interface DataColumn {
|
|
|
68
70
|
getValue: (stats: BaseStats & { sessions: Set<string> | number }) => string;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
interface TableLayoutCandidate {
|
|
74
|
+
columns: DataColumn[];
|
|
75
|
+
minNameWidth: number;
|
|
76
|
+
compact?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface TableLayout {
|
|
80
|
+
columns: DataColumn[];
|
|
81
|
+
nameWidth: number;
|
|
82
|
+
tableWidth: number;
|
|
83
|
+
compact: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const MAX_NAME_COL_WIDTH = 26;
|
|
87
|
+
|
|
88
|
+
const SESSIONS_COLUMN: DataColumn = {
|
|
89
|
+
label: "Sessions",
|
|
90
|
+
width: 9,
|
|
91
|
+
getValue: (s) => formatNumber(typeof s.sessions === "number" ? s.sessions : s.sessions.size),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const MSGS_COLUMN: DataColumn = {
|
|
95
|
+
label: "Msgs",
|
|
96
|
+
width: 9,
|
|
97
|
+
getValue: (s) => formatNumber(s.messages),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const COST_COLUMN: DataColumn = {
|
|
101
|
+
label: "Cost",
|
|
102
|
+
width: 9,
|
|
103
|
+
getValue: (s) => formatCost(s.cost),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const TOKENS_COLUMN: DataColumn = {
|
|
107
|
+
label: "Tokens",
|
|
108
|
+
width: 9,
|
|
109
|
+
getValue: (s) => formatTokens(s.tokens.total),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const INPUT_COLUMN: DataColumn = {
|
|
113
|
+
label: "βIn",
|
|
114
|
+
width: 8,
|
|
115
|
+
dimmed: true,
|
|
116
|
+
// Include cacheWrite so this reflects fresh input tokens sent this turn,
|
|
117
|
+
// even for providers like Anthropic that split cached prompt creation out
|
|
118
|
+
// from the regular input token count.
|
|
119
|
+
getValue: (s) => formatTokens(s.tokens.input + s.tokens.cacheWrite),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const OUTPUT_COLUMN: DataColumn = {
|
|
123
|
+
label: "βOut",
|
|
124
|
+
width: 8,
|
|
125
|
+
dimmed: true,
|
|
126
|
+
getValue: (s) => formatTokens(s.tokens.output),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const CACHE_COLUMN: DataColumn = {
|
|
130
|
+
label: "Cache",
|
|
131
|
+
width: 8,
|
|
132
|
+
dimmed: true,
|
|
133
|
+
getValue: (s) => formatTokens(s.tokens.cacheRead + s.tokens.cacheWrite),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const FULL_DATA_COLUMNS: DataColumn[] = [
|
|
137
|
+
SESSIONS_COLUMN,
|
|
138
|
+
MSGS_COLUMN,
|
|
139
|
+
COST_COLUMN,
|
|
140
|
+
TOKENS_COLUMN,
|
|
141
|
+
INPUT_COLUMN,
|
|
142
|
+
OUTPUT_COLUMN,
|
|
143
|
+
CACHE_COLUMN,
|
|
85
144
|
];
|
|
86
145
|
|
|
87
|
-
const
|
|
146
|
+
const TABLE_LAYOUTS: TableLayoutCandidate[] = [
|
|
147
|
+
{ columns: FULL_DATA_COLUMNS, minNameWidth: MAX_NAME_COL_WIDTH },
|
|
148
|
+
{ columns: [SESSIONS_COLUMN, MSGS_COLUMN, COST_COLUMN, TOKENS_COLUMN], minNameWidth: 14, compact: true },
|
|
149
|
+
{ columns: [SESSIONS_COLUMN, COST_COLUMN, TOKENS_COLUMN], minNameWidth: 12, compact: true },
|
|
150
|
+
{ columns: [COST_COLUMN, TOKENS_COLUMN], minNameWidth: 10, compact: true },
|
|
151
|
+
{ columns: [COST_COLUMN], minNameWidth: 8, compact: true },
|
|
152
|
+
];
|
|
88
153
|
|
|
89
154
|
// =============================================================================
|
|
90
155
|
// Data Collection
|
|
@@ -204,18 +269,19 @@ async function parseSessionFile(
|
|
|
204
269
|
function accumulateStats(
|
|
205
270
|
target: BaseStats,
|
|
206
271
|
cost: number,
|
|
207
|
-
tokens: { total: number; input: number; output: number;
|
|
272
|
+
tokens: { total: number; input: number; output: number; cacheRead: number; cacheWrite: number }
|
|
208
273
|
): void {
|
|
209
274
|
target.messages++;
|
|
210
275
|
target.cost += cost;
|
|
211
276
|
target.tokens.total += tokens.total;
|
|
212
277
|
target.tokens.input += tokens.input;
|
|
213
278
|
target.tokens.output += tokens.output;
|
|
214
|
-
target.tokens.
|
|
279
|
+
target.tokens.cacheRead += tokens.cacheRead;
|
|
280
|
+
target.tokens.cacheWrite += tokens.cacheWrite;
|
|
215
281
|
}
|
|
216
282
|
|
|
217
283
|
function emptyTokens(): TokenStats {
|
|
218
|
-
return { total: 0, input: 0, output: 0,
|
|
284
|
+
return { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
219
285
|
}
|
|
220
286
|
|
|
221
287
|
function emptyModelStats(): ModelStats {
|
|
@@ -237,14 +303,19 @@ function emptyUsageData(): UsageData {
|
|
|
237
303
|
return {
|
|
238
304
|
today: emptyTimeFilteredStats(),
|
|
239
305
|
thisWeek: emptyTimeFilteredStats(),
|
|
306
|
+
lastWeek: emptyTimeFilteredStats(),
|
|
240
307
|
allTime: emptyTimeFilteredStats(),
|
|
241
308
|
};
|
|
242
309
|
}
|
|
243
310
|
|
|
244
|
-
function getPeriodsForTimestamp(timestamp: number, todayMs: number, weekStartMs: number): TabName[] {
|
|
311
|
+
function getPeriodsForTimestamp(timestamp: number, todayMs: number, weekStartMs: number, lastWeekStartMs: number): TabName[] {
|
|
245
312
|
const periods: TabName[] = ["allTime"];
|
|
246
313
|
if (timestamp >= todayMs) periods.push("today");
|
|
247
|
-
if (timestamp >= weekStartMs)
|
|
314
|
+
if (timestamp >= weekStartMs) {
|
|
315
|
+
periods.push("thisWeek");
|
|
316
|
+
} else if (timestamp >= lastWeekStartMs) {
|
|
317
|
+
periods.push("lastWeek");
|
|
318
|
+
}
|
|
248
319
|
return periods;
|
|
249
320
|
}
|
|
250
321
|
|
|
@@ -253,20 +324,22 @@ function addMessagesToUsageData(
|
|
|
253
324
|
sessionId: string,
|
|
254
325
|
messages: SessionMessage[],
|
|
255
326
|
todayMs: number,
|
|
256
|
-
weekStartMs: number
|
|
327
|
+
weekStartMs: number,
|
|
328
|
+
lastWeekStartMs: number
|
|
257
329
|
): void {
|
|
258
|
-
const sessionContributed = { today: false, thisWeek: false, allTime: false };
|
|
330
|
+
const sessionContributed = { today: false, thisWeek: false, lastWeek: false, allTime: false };
|
|
259
331
|
|
|
260
332
|
for (const msg of messages) {
|
|
261
|
-
const periods = getPeriodsForTimestamp(msg.timestamp, todayMs, weekStartMs);
|
|
333
|
+
const periods = getPeriodsForTimestamp(msg.timestamp, todayMs, weekStartMs, lastWeekStartMs);
|
|
262
334
|
const tokens = {
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
total: msg.input + msg.output,
|
|
335
|
+
// Count fresh tokens processed this turn.
|
|
336
|
+
// Include cacheWrite because those prompt tokens were newly written and billed.
|
|
337
|
+
// Exclude cacheRead because repeated cache hits would otherwise dominate totals.
|
|
338
|
+
total: msg.input + msg.output + msg.cacheWrite,
|
|
267
339
|
input: msg.input,
|
|
268
340
|
output: msg.output,
|
|
269
|
-
|
|
341
|
+
cacheRead: msg.cacheRead,
|
|
342
|
+
cacheWrite: msg.cacheWrite,
|
|
270
343
|
};
|
|
271
344
|
|
|
272
345
|
for (const period of periods) {
|
|
@@ -297,6 +370,7 @@ function addMessagesToUsageData(
|
|
|
297
370
|
|
|
298
371
|
if (sessionContributed.today) data.today.totals.sessions++;
|
|
299
372
|
if (sessionContributed.thisWeek) data.thisWeek.totals.sessions++;
|
|
373
|
+
if (sessionContributed.lastWeek) data.lastWeek.totals.sessions++;
|
|
300
374
|
if (sessionContributed.allTime) data.allTime.totals.sessions++;
|
|
301
375
|
}
|
|
302
376
|
|
|
@@ -313,6 +387,11 @@ async function collectUsageData(signal?: AbortSignal): Promise<UsageData | null>
|
|
|
313
387
|
startOfWeek.setHours(0, 0, 0, 0);
|
|
314
388
|
const weekStartMs = startOfWeek.getTime();
|
|
315
389
|
|
|
390
|
+
// Start of last week (previous Monday 00:00)
|
|
391
|
+
const startOfLastWeek = new Date(startOfWeek);
|
|
392
|
+
startOfLastWeek.setDate(startOfLastWeek.getDate() - 7);
|
|
393
|
+
const lastWeekStartMs = startOfLastWeek.getTime();
|
|
394
|
+
|
|
316
395
|
const data = emptyUsageData();
|
|
317
396
|
|
|
318
397
|
const sessionFiles = await getAllSessionFiles(signal);
|
|
@@ -325,7 +404,7 @@ async function collectUsageData(signal?: AbortSignal): Promise<UsageData | null>
|
|
|
325
404
|
if (signal?.aborted) return null;
|
|
326
405
|
if (!parsed) continue;
|
|
327
406
|
|
|
328
|
-
addMessagesToUsageData(data, parsed.sessionId, parsed.messages, todayMs, weekStartMs);
|
|
407
|
+
addMessagesToUsageData(data, parsed.sessionId, parsed.messages, todayMs, weekStartMs, lastWeekStartMs);
|
|
329
408
|
|
|
330
409
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
331
410
|
}
|
|
@@ -372,6 +451,54 @@ function padRight(s: string, len: number): string {
|
|
|
372
451
|
return s + " ".repeat(len - vis);
|
|
373
452
|
}
|
|
374
453
|
|
|
454
|
+
function sumColumnWidths(columns: DataColumn[]): number {
|
|
455
|
+
return columns.reduce((sum, col) => sum + col.width, 0);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function fitCell(s: string, len: number, align: "left" | "right" = "left"): string {
|
|
459
|
+
if (len <= 0) return "";
|
|
460
|
+
const truncated = truncateToWidth(s, len);
|
|
461
|
+
return align === "right" ? padLeft(truncated, len) : padRight(truncated, len);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function clampLines(lines: string[], width: number): string[] {
|
|
465
|
+
return lines.map((line) => truncateToWidth(line, Math.max(width, 0)));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function pickFittingText(width: number, variants: string[]): string {
|
|
469
|
+
for (const variant of variants) {
|
|
470
|
+
if (visibleWidth(variant) <= width) return variant;
|
|
471
|
+
}
|
|
472
|
+
return variants[variants.length - 1] || "";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function getTableLayout(width: number): TableLayout {
|
|
476
|
+
const safeWidth = Math.max(width, 0);
|
|
477
|
+
|
|
478
|
+
for (const candidate of TABLE_LAYOUTS) {
|
|
479
|
+
const columnsWidth = sumColumnWidths(candidate.columns);
|
|
480
|
+
const nameWidth = Math.min(MAX_NAME_COL_WIDTH, Math.max(safeWidth - columnsWidth, 0));
|
|
481
|
+
if (nameWidth >= candidate.minNameWidth) {
|
|
482
|
+
return {
|
|
483
|
+
columns: candidate.columns,
|
|
484
|
+
nameWidth,
|
|
485
|
+
tableWidth: nameWidth + columnsWidth,
|
|
486
|
+
compact: candidate.compact ?? false,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const fallback = TABLE_LAYOUTS[TABLE_LAYOUTS.length - 1]!;
|
|
492
|
+
const fallbackColumnsWidth = sumColumnWidths(fallback.columns);
|
|
493
|
+
const fallbackNameWidth = Math.min(MAX_NAME_COL_WIDTH, Math.max(safeWidth - fallbackColumnsWidth, 0));
|
|
494
|
+
return {
|
|
495
|
+
columns: fallback.columns,
|
|
496
|
+
nameWidth: fallbackNameWidth,
|
|
497
|
+
tableWidth: fallbackNameWidth + fallbackColumnsWidth,
|
|
498
|
+
compact: fallback.compact ?? false,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
375
502
|
// =============================================================================
|
|
376
503
|
// Component
|
|
377
504
|
// =============================================================================
|
|
@@ -379,10 +506,11 @@ function padRight(s: string, len: number): string {
|
|
|
379
506
|
const TAB_LABELS: Record<TabName, string> = {
|
|
380
507
|
today: "Today",
|
|
381
508
|
thisWeek: "This Week",
|
|
509
|
+
lastWeek: "Last Week",
|
|
382
510
|
allTime: "All Time",
|
|
383
511
|
};
|
|
384
512
|
|
|
385
|
-
const TAB_ORDER: TabName[] = ["today", "thisWeek", "allTime"];
|
|
513
|
+
const TAB_ORDER: TabName[] = ["today", "thisWeek", "lastWeek", "allTime"];
|
|
386
514
|
|
|
387
515
|
class UsageComponent {
|
|
388
516
|
private activeTab: TabName = "allTime";
|
|
@@ -453,15 +581,20 @@ class UsageComponent {
|
|
|
453
581
|
// Render Methods
|
|
454
582
|
// -------------------------------------------------------------------------
|
|
455
583
|
|
|
456
|
-
render(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
584
|
+
render(width: number): string[] {
|
|
585
|
+
const layout = getTableLayout(width);
|
|
586
|
+
return clampLines(
|
|
587
|
+
[
|
|
588
|
+
...this.renderTitle(),
|
|
589
|
+
...this.renderTabs(width, layout),
|
|
590
|
+
...this.renderHeader(layout),
|
|
591
|
+
...this.renderRows(layout),
|
|
592
|
+
...this.renderTotals(layout),
|
|
593
|
+
...this.renderFormulaNote(width),
|
|
594
|
+
...this.renderHelp(width),
|
|
595
|
+
],
|
|
596
|
+
width
|
|
597
|
+
);
|
|
465
598
|
}
|
|
466
599
|
|
|
467
600
|
private renderTitle(): string[] {
|
|
@@ -469,52 +602,67 @@ class UsageComponent {
|
|
|
469
602
|
return [th.fg("accent", th.bold("Usage Statistics")), ""];
|
|
470
603
|
}
|
|
471
604
|
|
|
472
|
-
private renderTabs(): string[] {
|
|
605
|
+
private renderTabs(width: number, layout: TableLayout): string[] {
|
|
473
606
|
const th = this.theme;
|
|
474
|
-
const
|
|
607
|
+
const fullTabs = TAB_ORDER.map((tab) => {
|
|
475
608
|
const label = TAB_LABELS[tab];
|
|
476
609
|
return tab === this.activeTab ? th.fg("accent", `[${label}]`) : th.fg("dim", ` ${label} `);
|
|
477
610
|
}).join(" ");
|
|
478
|
-
|
|
611
|
+
|
|
612
|
+
const activeTabOnly = th.fg("accent", `[${TAB_LABELS[this.activeTab]}]`);
|
|
613
|
+
const tabLine = pickFittingText(width, [
|
|
614
|
+
fullTabs,
|
|
615
|
+
`${activeTabOnly} ${th.fg("dim", "[Tab/ββ]")}`,
|
|
616
|
+
activeTabOnly,
|
|
617
|
+
]);
|
|
618
|
+
|
|
619
|
+
const infoLines = layout.compact
|
|
620
|
+
? wrapTextWithAnsi(th.fg("dim", "Compact view. Widen the terminal for more columns."), Math.max(width, 1))
|
|
621
|
+
: [];
|
|
622
|
+
|
|
623
|
+
return [tabLine, ...infoLines, ""];
|
|
479
624
|
}
|
|
480
625
|
|
|
481
|
-
private renderHeader(): string[] {
|
|
626
|
+
private renderHeader(layout: TableLayout): string[] {
|
|
482
627
|
const th = this.theme;
|
|
483
628
|
|
|
484
|
-
let headerLine =
|
|
485
|
-
for (const col of
|
|
486
|
-
const label =
|
|
629
|
+
let headerLine = fitCell("Provider / Model", layout.nameWidth);
|
|
630
|
+
for (const col of layout.columns) {
|
|
631
|
+
const label = fitCell(col.label, col.width, "right");
|
|
487
632
|
headerLine += col.dimmed ? th.fg("dim", label) : label;
|
|
488
633
|
}
|
|
489
634
|
|
|
490
|
-
return [th.fg("muted", headerLine), th.fg("border", "β".repeat(
|
|
635
|
+
return [th.fg("muted", headerLine), th.fg("border", "β".repeat(layout.tableWidth))];
|
|
491
636
|
}
|
|
492
637
|
|
|
493
638
|
private renderDataRow(
|
|
494
639
|
name: string,
|
|
495
640
|
stats: BaseStats & { sessions: Set<string> | number },
|
|
496
|
-
|
|
641
|
+
layout: TableLayout,
|
|
642
|
+
options: { indent?: number; selected?: boolean; dimAll?: boolean; prefix?: string } = {}
|
|
497
643
|
): string {
|
|
498
644
|
const th = this.theme;
|
|
499
|
-
const { indent = 0, selected = false, dimAll = false } = options;
|
|
645
|
+
const { indent = 0, selected = false, dimAll = false, prefix } = options;
|
|
500
646
|
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
const
|
|
647
|
+
const rawPrefix = prefix ?? " ".repeat(indent);
|
|
648
|
+
const safePrefix = layout.nameWidth > 0 ? truncateToWidth(rawPrefix, layout.nameWidth, "") : "";
|
|
649
|
+
const prefixWidth = visibleWidth(safePrefix);
|
|
650
|
+
const innerNameWidth = Math.max(layout.nameWidth - prefixWidth, 0);
|
|
651
|
+
const truncName = innerNameWidth > 0 ? truncateToWidth(name, innerNameWidth) : "";
|
|
504
652
|
const styledName = selected ? th.fg("accent", truncName) : dimAll ? th.fg("dim", truncName) : truncName;
|
|
505
653
|
|
|
506
|
-
let row =
|
|
654
|
+
let row = safePrefix + (innerNameWidth > 0 ? padRight(styledName, innerNameWidth) : "");
|
|
507
655
|
|
|
508
|
-
for (const col of
|
|
509
|
-
const value = col.getValue(stats);
|
|
656
|
+
for (const col of layout.columns) {
|
|
657
|
+
const value = fitCell(col.getValue(stats), col.width, "right");
|
|
510
658
|
const shouldDim = col.dimmed || dimAll;
|
|
511
|
-
row += shouldDim ? th.fg("dim",
|
|
659
|
+
row += shouldDim ? th.fg("dim", value) : value;
|
|
512
660
|
}
|
|
513
661
|
|
|
514
662
|
return row;
|
|
515
663
|
}
|
|
516
664
|
|
|
517
|
-
private renderRows(): string[] {
|
|
665
|
+
private renderRows(layout: TableLayout): string[] {
|
|
518
666
|
const th = this.theme;
|
|
519
667
|
const stats = this.data[this.activeTab];
|
|
520
668
|
const lines: string[] = [];
|
|
@@ -529,22 +677,21 @@ class UsageComponent {
|
|
|
529
677
|
const providerStats = stats.providers.get(providerName)!;
|
|
530
678
|
const isSelected = i === this.selectedIndex;
|
|
531
679
|
const isExpanded = this.expanded.has(providerName);
|
|
532
|
-
|
|
533
|
-
// Provider row with expand/collapse arrow
|
|
534
680
|
const arrow = isExpanded ? "βΎ" : "βΈ";
|
|
535
|
-
const prefix = isSelected ? th.fg("accent", arrow
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
681
|
+
const prefix = isSelected ? th.fg("accent", `${arrow} `) : th.fg("dim", `${arrow} `);
|
|
682
|
+
|
|
683
|
+
lines.push(
|
|
684
|
+
this.renderDataRow(providerName, providerStats, layout, {
|
|
685
|
+
selected: isSelected,
|
|
686
|
+
prefix,
|
|
687
|
+
})
|
|
688
|
+
);
|
|
541
689
|
|
|
542
|
-
// Model rows (if expanded)
|
|
543
690
|
if (isExpanded) {
|
|
544
691
|
const models = Array.from(providerStats.models.entries()).sort((a, b) => b[1].cost - a[1].cost);
|
|
545
692
|
|
|
546
693
|
for (const [modelName, modelStats] of models) {
|
|
547
|
-
lines.push(this.renderDataRow(modelName, modelStats, { indent: 4, dimAll: true }));
|
|
694
|
+
lines.push(this.renderDataRow(modelName, modelStats, layout, { indent: 4, dimAll: true }));
|
|
548
695
|
}
|
|
549
696
|
}
|
|
550
697
|
}
|
|
@@ -552,21 +699,38 @@ class UsageComponent {
|
|
|
552
699
|
return lines;
|
|
553
700
|
}
|
|
554
701
|
|
|
555
|
-
private renderTotals(): string[] {
|
|
702
|
+
private renderTotals(layout: TableLayout): string[] {
|
|
556
703
|
const th = this.theme;
|
|
557
704
|
const stats = this.data[this.activeTab];
|
|
558
705
|
|
|
559
|
-
let totalRow =
|
|
560
|
-
for (const col of
|
|
561
|
-
const value = col.getValue(stats.totals);
|
|
562
|
-
totalRow += col.dimmed ? th.fg("dim",
|
|
706
|
+
let totalRow = fitCell(th.bold("Total"), layout.nameWidth);
|
|
707
|
+
for (const col of layout.columns) {
|
|
708
|
+
const value = fitCell(col.getValue(stats.totals), col.width, "right");
|
|
709
|
+
totalRow += col.dimmed ? th.fg("dim", value) : value;
|
|
563
710
|
}
|
|
564
711
|
|
|
565
|
-
return [th.fg("border", "β".repeat(
|
|
712
|
+
return [th.fg("border", "β".repeat(layout.tableWidth)), totalRow, ""];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private renderFormulaNote(width: number): string[] {
|
|
716
|
+
const line = pickFittingText(width, [
|
|
717
|
+
"Tokens = Input + Output + CacheWrite Β· βIn = Input + CacheWrite (as of 0.2.0)",
|
|
718
|
+
"Tokens = In + Out + CacheWrite Β· βIn = In + CacheWrite (v0.2.0+)",
|
|
719
|
+
"Tokens & βIn include CacheWrite (v0.2.0+)",
|
|
720
|
+
"Incl. CacheWrite (v0.2.0+)",
|
|
721
|
+
]);
|
|
722
|
+
return [this.theme.fg("dim", line), ""];
|
|
566
723
|
}
|
|
567
724
|
|
|
568
|
-
private renderHelp(): string[] {
|
|
569
|
-
|
|
725
|
+
private renderHelp(width: number): string[] {
|
|
726
|
+
const line = pickFittingText(width, [
|
|
727
|
+
"[Tab/ββ] period [ββ] select [Enter] expand [q] close",
|
|
728
|
+
"[Tab] period [ββ] select [Enter] expand [q] close",
|
|
729
|
+
"[ββ] select [Enter] expand [q] close",
|
|
730
|
+
"[ββ] select [q] close",
|
|
731
|
+
"[q] close",
|
|
732
|
+
]);
|
|
733
|
+
return [this.theme.fg("dim", line)];
|
|
570
734
|
}
|
|
571
735
|
|
|
572
736
|
invalidate(): void {}
|
|
@@ -625,10 +789,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
625
789
|
|
|
626
790
|
return {
|
|
627
791
|
render: (w: number) => {
|
|
628
|
-
const borderLines = container.render(w);
|
|
792
|
+
const borderLines = clampLines(container.render(w), w);
|
|
629
793
|
const usageLines = usage.render(w);
|
|
630
794
|
const bottomBorder = theme.fg("border", "β".repeat(w));
|
|
631
|
-
return [...borderLines, ...usageLines, "", bottomBorder];
|
|
795
|
+
return clampLines([...borderLines, ...usageLines, "", bottomBorder], w);
|
|
632
796
|
},
|
|
633
797
|
invalidate: () => container.invalidate(),
|
|
634
798
|
handleInput: (input: string) => usage.handleInput(input),
|