pi-mono-all 1.2.0 → 1.2.1
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# pi-mono-usage
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add a Tools view grouped by currently registered extension ownership, with expandable per-tool breakdowns sorted by usage.
|
|
8
|
+
- Replace Last Week with This Month and show the active period date range in the header.
|
|
9
|
+
- Tighten sustainability copy, show a single random equivalence, and simplify the grid/profile label.
|
|
10
|
+
|
|
3
11
|
## 0.1.0
|
|
4
12
|
|
|
5
13
|
### Initial release
|
|
@@ -33,8 +33,8 @@ import { estimateAiImpact, type AiEstimateResult } from "impact-equivalences";
|
|
|
33
33
|
// Types
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
|
|
36
|
-
type Period = "day" | "week" | "
|
|
37
|
-
type View = "summary" | "providers" | "patterns";
|
|
36
|
+
type Period = "day" | "week" | "month" | "all";
|
|
37
|
+
type View = "summary" | "providers" | "patterns" | "tools";
|
|
38
38
|
|
|
39
39
|
interface TokenBucket {
|
|
40
40
|
input: number;
|
|
@@ -56,6 +56,18 @@ interface ProviderBucket extends Aggregate {
|
|
|
56
56
|
models: Map<string, ModelBucket>;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
interface ToolBucket {
|
|
60
|
+
calls: number;
|
|
61
|
+
resultTokens: number;
|
|
62
|
+
sessions: Set<string>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ToolGroupBucket extends ToolBucket {
|
|
66
|
+
tools: Map<string, ToolBucket>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type ToolRegistry = Map<string, string>;
|
|
70
|
+
|
|
59
71
|
interface RawTurn {
|
|
60
72
|
sessionId: string;
|
|
61
73
|
provider: string;
|
|
@@ -68,6 +80,13 @@ interface RawTurn {
|
|
|
68
80
|
ts: number;
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
interface RawToolUse {
|
|
84
|
+
sessionId: string;
|
|
85
|
+
name: string;
|
|
86
|
+
resultTokens: number;
|
|
87
|
+
ts: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
interface InsightRow {
|
|
72
91
|
weight: number;
|
|
73
92
|
headline: string;
|
|
@@ -76,6 +95,7 @@ interface InsightRow {
|
|
|
76
95
|
|
|
77
96
|
interface PeriodReport {
|
|
78
97
|
providers: Map<string, ProviderBucket>;
|
|
98
|
+
toolGroups: Map<string, ToolGroupBucket>;
|
|
79
99
|
totals: Aggregate;
|
|
80
100
|
turns: RawTurn[];
|
|
81
101
|
insights: InsightRow[];
|
|
@@ -89,7 +109,7 @@ interface SessionLifespan {
|
|
|
89
109
|
interface UsageReport {
|
|
90
110
|
day: PeriodReport;
|
|
91
111
|
week: PeriodReport;
|
|
92
|
-
|
|
112
|
+
month: PeriodReport;
|
|
93
113
|
all: PeriodReport;
|
|
94
114
|
lifespans: Map<string, SessionLifespan>;
|
|
95
115
|
}
|
|
@@ -97,25 +117,26 @@ interface UsageReport {
|
|
|
97
117
|
interface SessionRecord {
|
|
98
118
|
sessionId: string;
|
|
99
119
|
turns: RawTurn[];
|
|
120
|
+
tools: RawToolUse[];
|
|
100
121
|
}
|
|
101
122
|
|
|
102
123
|
interface PeriodBoundaries {
|
|
103
124
|
dayStart: number;
|
|
104
125
|
weekStart: number;
|
|
105
|
-
|
|
126
|
+
monthStart: number;
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
// ---------------------------------------------------------------------------
|
|
109
130
|
// Constants
|
|
110
131
|
// ---------------------------------------------------------------------------
|
|
111
132
|
|
|
112
|
-
const PERIOD_ORDER: readonly Period[] = ["day", "week", "
|
|
113
|
-
const VIEW_ORDER: readonly View[] = ["summary", "providers", "patterns"];
|
|
133
|
+
const PERIOD_ORDER: readonly Period[] = ["day", "week", "month", "all"];
|
|
134
|
+
const VIEW_ORDER: readonly View[] = ["summary", "providers", "patterns", "tools"];
|
|
114
135
|
|
|
115
136
|
const PERIOD_LABELS: Record<Period, string> = {
|
|
116
137
|
day: "Today",
|
|
117
138
|
week: "This Week",
|
|
118
|
-
|
|
139
|
+
month: "This Month",
|
|
119
140
|
all: "All Time",
|
|
120
141
|
};
|
|
121
142
|
|
|
@@ -123,6 +144,7 @@ const VIEW_LABELS: Record<View, string> = {
|
|
|
123
144
|
summary: "Summary",
|
|
124
145
|
providers: "Providers",
|
|
125
146
|
patterns: "Patterns",
|
|
147
|
+
tools: "Tools",
|
|
126
148
|
};
|
|
127
149
|
|
|
128
150
|
const NAME_COL_MAX = 28;
|
|
@@ -141,6 +163,12 @@ const SUMMARY_TOP_PROVIDERS = 3;
|
|
|
141
163
|
const BAR_WIDTH = 24;
|
|
142
164
|
const BAR_FILLED = "█";
|
|
143
165
|
const BAR_EMPTY = "░";
|
|
166
|
+
const BUILT_IN_TOOLS = new Set([
|
|
167
|
+
"bash",
|
|
168
|
+
"edit",
|
|
169
|
+
"read",
|
|
170
|
+
"write",
|
|
171
|
+
]);
|
|
144
172
|
|
|
145
173
|
// ---------------------------------------------------------------------------
|
|
146
174
|
// Path helpers
|
|
@@ -175,6 +203,85 @@ async function listSessionFiles(root: string, signal?: AbortSignal): Promise<str
|
|
|
175
203
|
return out.sort();
|
|
176
204
|
}
|
|
177
205
|
|
|
206
|
+
async function listFiles(root: string, predicate: (name: string) => boolean, signal?: AbortSignal): Promise<string[]> {
|
|
207
|
+
const queue: string[] = [root];
|
|
208
|
+
const out: string[] = [];
|
|
209
|
+
|
|
210
|
+
while (queue.length > 0) {
|
|
211
|
+
if (signal?.aborted) return [];
|
|
212
|
+
const dir = queue.shift()!;
|
|
213
|
+
let entries: import("node:fs").Dirent[];
|
|
214
|
+
try {
|
|
215
|
+
entries = (await readdir(dir, { withFileTypes: true })) as unknown as import("node:fs").Dirent[];
|
|
216
|
+
} catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
const name = entry.name;
|
|
221
|
+
if (name === "node_modules" || name === "dist" || name === "__tests__") continue;
|
|
222
|
+
const full = join(dir, name);
|
|
223
|
+
if (entry.isDirectory()) queue.push(full);
|
|
224
|
+
else if (entry.isFile() && predicate(name)) out.push(full);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return out.sort();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function buildToolRegistry(signal?: AbortSignal): Promise<ToolRegistry> {
|
|
232
|
+
const registry: ToolRegistry = new Map();
|
|
233
|
+
const extensionsDir = join(process.cwd(), "extensions");
|
|
234
|
+
let entries: import("node:fs").Dirent[];
|
|
235
|
+
try {
|
|
236
|
+
entries = (await readdir(extensionsDir, { withFileTypes: true })) as unknown as import("node:fs").Dirent[];
|
|
237
|
+
} catch {
|
|
238
|
+
return registry;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
if (signal?.aborted) return registry;
|
|
243
|
+
if (!entry.isDirectory()) continue;
|
|
244
|
+
const extensionDir = join(extensionsDir, entry.name);
|
|
245
|
+
const group = await extensionGroupName(extensionDir, entry.name);
|
|
246
|
+
const files = await listFiles(extensionDir, (name) => name.endsWith(".ts"), signal);
|
|
247
|
+
for (const file of files) {
|
|
248
|
+
if (signal?.aborted) return registry;
|
|
249
|
+
let source = "";
|
|
250
|
+
try {
|
|
251
|
+
source = await readFile(file, "utf8");
|
|
252
|
+
} catch {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
for (const toolName of extractRegisteredToolNames(source)) registry.set(toolName, group);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return registry;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function extensionGroupName(extensionDir: string, fallback: string): Promise<string> {
|
|
263
|
+
try {
|
|
264
|
+
const pkg = JSON.parse(await readFile(join(extensionDir, "package.json"), "utf8"));
|
|
265
|
+
const name = typeof pkg.name === "string" ? pkg.name : fallback;
|
|
266
|
+
return titleCaseWords(name.replace(/^pi-mono-/, ""));
|
|
267
|
+
} catch {
|
|
268
|
+
return titleCaseWords(fallback);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function extractRegisteredToolNames(source: string): string[] {
|
|
273
|
+
const names = new Set<string>();
|
|
274
|
+
const patterns = [
|
|
275
|
+
/registerTool\s*\(\s*{[\s\S]*?name:\s*["']([^"']+)["']/g,
|
|
276
|
+
/toolName:\s*["']([^"']+)["']/g,
|
|
277
|
+
];
|
|
278
|
+
for (const pattern of patterns) {
|
|
279
|
+
let match: RegExpExecArray | null;
|
|
280
|
+
while ((match = pattern.exec(source))) names.add(match[1]!);
|
|
281
|
+
}
|
|
282
|
+
return [...names];
|
|
283
|
+
}
|
|
284
|
+
|
|
178
285
|
// ---------------------------------------------------------------------------
|
|
179
286
|
// Parsing
|
|
180
287
|
// ---------------------------------------------------------------------------
|
|
@@ -198,6 +305,7 @@ async function parseSessionFile(
|
|
|
198
305
|
if (signal?.aborted) return null;
|
|
199
306
|
|
|
200
307
|
const turns: RawTurn[] = [];
|
|
308
|
+
const tools: RawToolUse[] = [];
|
|
201
309
|
let sessionId = "";
|
|
202
310
|
const lines = raw.trim().split("\n");
|
|
203
311
|
|
|
@@ -222,6 +330,16 @@ async function parseSessionFile(
|
|
|
222
330
|
|
|
223
331
|
if (entry.type !== "message") continue;
|
|
224
332
|
const msg = entry.message;
|
|
333
|
+
if (msg?.role === "toolResult" && typeof msg.toolName === "string") {
|
|
334
|
+
const ts = timestampForMessage(entry, msg);
|
|
335
|
+
tools.push({
|
|
336
|
+
sessionId: "", // filled later once header parsed
|
|
337
|
+
name: msg.toolName,
|
|
338
|
+
resultTokens: estimateTextTokens(toolResultText(msg.content)),
|
|
339
|
+
ts,
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
225
343
|
if (!msg || msg.role !== "assistant" || !msg.usage || !msg.provider || !msg.model) continue;
|
|
226
344
|
|
|
227
345
|
const input = numeric(msg.usage.input);
|
|
@@ -230,13 +348,7 @@ async function parseSessionFile(
|
|
|
230
348
|
const cacheWrite = numeric(msg.usage.cacheWrite);
|
|
231
349
|
const cost = numeric(msg.usage.cost?.total);
|
|
232
350
|
|
|
233
|
-
const
|
|
234
|
-
typeof msg.timestamp === "number"
|
|
235
|
-
? msg.timestamp
|
|
236
|
-
: entry.timestamp
|
|
237
|
-
? Date.parse(entry.timestamp)
|
|
238
|
-
: 0;
|
|
239
|
-
const ts = Number.isFinite(tsCandidate) ? Number(tsCandidate) : 0;
|
|
351
|
+
const ts = timestampForMessage(entry, msg);
|
|
240
352
|
|
|
241
353
|
const fp = turnFingerprint({ input, output, cacheRead, cacheWrite, ts });
|
|
242
354
|
if (seen.has(fp)) continue;
|
|
@@ -257,7 +369,8 @@ async function parseSessionFile(
|
|
|
257
369
|
|
|
258
370
|
if (!sessionId) return null;
|
|
259
371
|
for (const turn of turns) turn.sessionId = sessionId;
|
|
260
|
-
|
|
372
|
+
for (const tool of tools) tool.sessionId = sessionId;
|
|
373
|
+
return { sessionId, turns, tools };
|
|
261
374
|
}
|
|
262
375
|
|
|
263
376
|
function numeric(value: unknown): number {
|
|
@@ -265,6 +378,48 @@ function numeric(value: unknown): number {
|
|
|
265
378
|
return Number.isFinite(n) ? n : 0;
|
|
266
379
|
}
|
|
267
380
|
|
|
381
|
+
function timestampForMessage(entry: any, msg: any): number {
|
|
382
|
+
const tsCandidate =
|
|
383
|
+
typeof msg?.timestamp === "number"
|
|
384
|
+
? msg.timestamp
|
|
385
|
+
: entry.timestamp
|
|
386
|
+
? Date.parse(entry.timestamp)
|
|
387
|
+
: 0;
|
|
388
|
+
return Number.isFinite(tsCandidate) ? Number(tsCandidate) : 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function toolResultText(content: unknown): string {
|
|
392
|
+
if (typeof content === "string") return content;
|
|
393
|
+
if (!Array.isArray(content)) return "";
|
|
394
|
+
return content
|
|
395
|
+
.map((part) => {
|
|
396
|
+
if (typeof part === "string") return part;
|
|
397
|
+
if (part && typeof part === "object" && "text" in part) return String((part as { text?: unknown }).text ?? "");
|
|
398
|
+
return "";
|
|
399
|
+
})
|
|
400
|
+
.join("\n");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function estimateTextTokens(text: string): number {
|
|
404
|
+
if (!text) return 0;
|
|
405
|
+
return Math.ceil(text.length / 4);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function groupForTool(name: string, registry: ToolRegistry): string {
|
|
409
|
+
const registeredGroup = registry.get(name);
|
|
410
|
+
if (registeredGroup) return registeredGroup;
|
|
411
|
+
if (BUILT_IN_TOOLS.has(name)) return "Built-in";
|
|
412
|
+
return "Other";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function titleCaseWords(value: string): string {
|
|
416
|
+
return value
|
|
417
|
+
.split(/[\s_-]+/)
|
|
418
|
+
.filter(Boolean)
|
|
419
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
420
|
+
.join(" ");
|
|
421
|
+
}
|
|
422
|
+
|
|
268
423
|
// ---------------------------------------------------------------------------
|
|
269
424
|
// Aggregation
|
|
270
425
|
// ---------------------------------------------------------------------------
|
|
@@ -281,9 +436,18 @@ function emptyProvider(): ProviderBucket {
|
|
|
281
436
|
return { ...emptyAggregate(), models: new Map() };
|
|
282
437
|
}
|
|
283
438
|
|
|
439
|
+
function emptyToolBucket(): ToolBucket {
|
|
440
|
+
return { calls: 0, resultTokens: 0, sessions: new Set() };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function emptyToolGroup(): ToolGroupBucket {
|
|
444
|
+
return { ...emptyToolBucket(), tools: new Map() };
|
|
445
|
+
}
|
|
446
|
+
|
|
284
447
|
function emptyPeriod(): PeriodReport {
|
|
285
448
|
return {
|
|
286
449
|
providers: new Map(),
|
|
450
|
+
toolGroups: new Map(),
|
|
287
451
|
totals: emptyAggregate(),
|
|
288
452
|
turns: [],
|
|
289
453
|
insights: [],
|
|
@@ -300,11 +464,17 @@ function applyTurn(target: Aggregate, sessionId: string, turn: RawTurn): void {
|
|
|
300
464
|
target.sessions.add(sessionId);
|
|
301
465
|
}
|
|
302
466
|
|
|
467
|
+
function applyTool(target: ToolBucket, sessionId: string, tool: RawToolUse): void {
|
|
468
|
+
target.calls += 1;
|
|
469
|
+
target.resultTokens += tool.resultTokens;
|
|
470
|
+
target.sessions.add(sessionId);
|
|
471
|
+
}
|
|
472
|
+
|
|
303
473
|
function periodsFor(ts: number, b: PeriodBoundaries): Period[] {
|
|
304
474
|
const periods: Period[] = ["all"];
|
|
305
475
|
if (ts >= b.dayStart) periods.push("day");
|
|
306
476
|
if (ts >= b.weekStart) periods.push("week");
|
|
307
|
-
|
|
477
|
+
if (ts >= b.monthStart) periods.push("month");
|
|
308
478
|
return periods;
|
|
309
479
|
}
|
|
310
480
|
|
|
@@ -318,13 +488,14 @@ function computeBoundaries(now = new Date()): PeriodBoundaries {
|
|
|
318
488
|
week.setDate(week.getDate() - offsetToMonday);
|
|
319
489
|
week.setHours(0, 0, 0, 0);
|
|
320
490
|
|
|
321
|
-
const
|
|
322
|
-
|
|
491
|
+
const month = new Date(now);
|
|
492
|
+
month.setDate(1);
|
|
493
|
+
month.setHours(0, 0, 0, 0);
|
|
323
494
|
|
|
324
495
|
return {
|
|
325
496
|
dayStart: day.getTime(),
|
|
326
497
|
weekStart: week.getTime(),
|
|
327
|
-
|
|
498
|
+
monthStart: month.getTime(),
|
|
328
499
|
};
|
|
329
500
|
}
|
|
330
501
|
|
|
@@ -360,12 +531,34 @@ function placeTurn(
|
|
|
360
531
|
}
|
|
361
532
|
}
|
|
362
533
|
|
|
534
|
+
function placeTool(
|
|
535
|
+
report: UsageReport,
|
|
536
|
+
tool: RawToolUse,
|
|
537
|
+
boundaries: PeriodBoundaries,
|
|
538
|
+
toolRegistry: ToolRegistry,
|
|
539
|
+
): void {
|
|
540
|
+
for (const period of periodsFor(tool.ts, boundaries)) {
|
|
541
|
+
const slice = report[period];
|
|
542
|
+
const groupName = groupForTool(tool.name, toolRegistry);
|
|
543
|
+
const group = slice.toolGroups.get(groupName) ?? emptyToolGroup();
|
|
544
|
+
applyTool(group, tool.sessionId, tool);
|
|
545
|
+
|
|
546
|
+
const toolBucket = group.tools.get(tool.name) ?? emptyToolBucket();
|
|
547
|
+
applyTool(toolBucket, tool.sessionId, tool);
|
|
548
|
+
|
|
549
|
+
group.tools.set(tool.name, toolBucket);
|
|
550
|
+
slice.toolGroups.set(groupName, group);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
363
554
|
async function buildReport(signal?: AbortSignal): Promise<UsageReport | null> {
|
|
364
555
|
const boundaries = computeBoundaries();
|
|
556
|
+
const toolRegistry = await buildToolRegistry(signal);
|
|
557
|
+
if (signal?.aborted) return null;
|
|
365
558
|
const report: UsageReport = {
|
|
366
559
|
day: emptyPeriod(),
|
|
367
560
|
week: emptyPeriod(),
|
|
368
|
-
|
|
561
|
+
month: emptyPeriod(),
|
|
369
562
|
all: emptyPeriod(),
|
|
370
563
|
lifespans: new Map(),
|
|
371
564
|
};
|
|
@@ -379,6 +572,7 @@ async function buildReport(signal?: AbortSignal): Promise<UsageReport | null> {
|
|
|
379
572
|
const session = await parseSessionFile(file, seen, signal);
|
|
380
573
|
if (!session) continue;
|
|
381
574
|
for (const turn of session.turns) placeTurn(report, turn, boundaries);
|
|
575
|
+
for (const tool of session.tools) placeTool(report, tool, boundaries, toolRegistry);
|
|
382
576
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
383
577
|
}
|
|
384
578
|
|
|
@@ -571,6 +765,35 @@ function formatPercent(p: number): string {
|
|
|
571
765
|
return `${(Math.round(p * 10) / 10).toFixed(1)}%`;
|
|
572
766
|
}
|
|
573
767
|
|
|
768
|
+
function formatDate(date: Date): string {
|
|
769
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
770
|
+
month: "short",
|
|
771
|
+
day: "numeric",
|
|
772
|
+
year: "numeric",
|
|
773
|
+
}).format(date);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function dateRangeForPeriod(period: Period, report: UsageReport, now = new Date()): [Date, Date] {
|
|
777
|
+
const boundaries = computeBoundaries(now);
|
|
778
|
+
switch (period) {
|
|
779
|
+
case "day":
|
|
780
|
+
return [new Date(boundaries.dayStart), now];
|
|
781
|
+
case "week":
|
|
782
|
+
return [new Date(boundaries.weekStart), now];
|
|
783
|
+
case "month":
|
|
784
|
+
return [new Date(boundaries.monthStart), now];
|
|
785
|
+
case "all": {
|
|
786
|
+
let first = Number.POSITIVE_INFINITY;
|
|
787
|
+
let last = 0;
|
|
788
|
+
for (const span of report.lifespans.values()) {
|
|
789
|
+
if (span.first > 0 && span.first < first) first = span.first;
|
|
790
|
+
if (span.last > last) last = span.last;
|
|
791
|
+
}
|
|
792
|
+
return Number.isFinite(first) ? [new Date(first), new Date(last)] : [now, now];
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
574
797
|
function humanThreshold(n: number): string {
|
|
575
798
|
if (n >= 1_000_000) return `${n / 1_000_000}M`;
|
|
576
799
|
if (n >= 1_000) return `${n / 1_000}k`;
|
|
@@ -597,6 +820,11 @@ function pickFitting(width: number, options: string[]): string {
|
|
|
597
820
|
return options[options.length - 1] ?? "";
|
|
598
821
|
}
|
|
599
822
|
|
|
823
|
+
function randomItem<T>(items: readonly T[]): T | undefined {
|
|
824
|
+
if (items.length === 0) return undefined;
|
|
825
|
+
return items[Math.floor(Math.random() * items.length)];
|
|
826
|
+
}
|
|
827
|
+
|
|
600
828
|
// ---------------------------------------------------------------------------
|
|
601
829
|
// Table layout
|
|
602
830
|
// ---------------------------------------------------------------------------
|
|
@@ -608,6 +836,12 @@ interface TableColumn {
|
|
|
608
836
|
value: (row: Aggregate) => string;
|
|
609
837
|
}
|
|
610
838
|
|
|
839
|
+
interface ToolColumn {
|
|
840
|
+
label: string;
|
|
841
|
+
width: number;
|
|
842
|
+
value: (row: ToolBucket) => string;
|
|
843
|
+
}
|
|
844
|
+
|
|
611
845
|
const COL_SESSIONS: TableColumn = {
|
|
612
846
|
label: "Sess",
|
|
613
847
|
width: 7,
|
|
@@ -663,6 +897,14 @@ interface TableLayout {
|
|
|
663
897
|
compact: boolean;
|
|
664
898
|
}
|
|
665
899
|
|
|
900
|
+
function toolColumns(): ToolColumn[] {
|
|
901
|
+
return [
|
|
902
|
+
{ label: "Calls", width: 8, value: (r) => formatCount(r.calls) },
|
|
903
|
+
{ label: "Result", width: 9, value: (r) => formatTokens(r.resultTokens) },
|
|
904
|
+
{ label: "Sess", width: 7, value: (r) => formatCount(r.sessions.size) },
|
|
905
|
+
];
|
|
906
|
+
}
|
|
907
|
+
|
|
666
908
|
function pickLayout(width: number): TableLayout {
|
|
667
909
|
const safe = Math.max(width, 0);
|
|
668
910
|
const choose = (candidate: LayoutCandidate): TableLayout => {
|
|
@@ -693,6 +935,7 @@ class UsagePanel {
|
|
|
693
935
|
private cursor = 0;
|
|
694
936
|
private expanded = new Set<string>();
|
|
695
937
|
private providerOrder: string[] = [];
|
|
938
|
+
private toolGroupOrder: string[] = [];
|
|
696
939
|
private impactCache = new Map<Period, AiEstimateResult | null>();
|
|
697
940
|
|
|
698
941
|
constructor(
|
|
@@ -702,6 +945,7 @@ class UsagePanel {
|
|
|
702
945
|
private readonly close: () => void,
|
|
703
946
|
) {
|
|
704
947
|
this.refreshProviderOrder();
|
|
948
|
+
this.refreshToolGroupOrder();
|
|
705
949
|
}
|
|
706
950
|
|
|
707
951
|
handleInput(input: string): void {
|
|
@@ -725,20 +969,23 @@ class UsagePanel {
|
|
|
725
969
|
if (input === "1") return this.gotoView("summary");
|
|
726
970
|
if (input === "2") return this.gotoView("providers");
|
|
727
971
|
if (input === "3") return this.gotoView("patterns");
|
|
972
|
+
if (input === "4") return this.gotoView("tools");
|
|
728
973
|
|
|
729
|
-
if (this.view !== "providers") return;
|
|
974
|
+
if (this.view !== "providers" && this.view !== "tools") return;
|
|
975
|
+
|
|
976
|
+
const order = this.view === "providers" ? this.providerOrder : this.toolGroupOrder;
|
|
730
977
|
|
|
731
978
|
if (matchesKey(input, "up") && this.cursor > 0) {
|
|
732
979
|
this.cursor--;
|
|
733
980
|
this.requestRender();
|
|
734
|
-
} else if (matchesKey(input, "down") && this.cursor <
|
|
981
|
+
} else if (matchesKey(input, "down") && this.cursor < order.length - 1) {
|
|
735
982
|
this.cursor++;
|
|
736
983
|
this.requestRender();
|
|
737
984
|
} else if (matchesKey(input, "enter") || matchesKey(input, "space")) {
|
|
738
|
-
const
|
|
739
|
-
if (
|
|
740
|
-
if (this.expanded.has(
|
|
741
|
-
else this.expanded.add(
|
|
985
|
+
const expandable = order[this.cursor];
|
|
986
|
+
if (expandable) {
|
|
987
|
+
if (this.expanded.has(expandable)) this.expanded.delete(expandable);
|
|
988
|
+
else this.expanded.add(expandable);
|
|
742
989
|
this.requestRender();
|
|
743
990
|
}
|
|
744
991
|
}
|
|
@@ -755,6 +1002,10 @@ class UsagePanel {
|
|
|
755
1002
|
}
|
|
756
1003
|
case "patterns":
|
|
757
1004
|
return clipLines([...head, ...this.renderPatterns(width)], width);
|
|
1005
|
+
case "tools": {
|
|
1006
|
+
const layout = pickLayout(width);
|
|
1007
|
+
return clipLines([...head, ...this.renderTools(layout)], width);
|
|
1008
|
+
}
|
|
758
1009
|
}
|
|
759
1010
|
}
|
|
760
1011
|
|
|
@@ -771,11 +1022,20 @@ class UsagePanel {
|
|
|
771
1022
|
this.cursor = Math.min(this.cursor, Math.max(0, this.providerOrder.length - 1));
|
|
772
1023
|
}
|
|
773
1024
|
|
|
1025
|
+
private refreshToolGroupOrder(): void {
|
|
1026
|
+
const slice = this.report[this.period];
|
|
1027
|
+
this.toolGroupOrder = Array.from(slice.toolGroups.entries())
|
|
1028
|
+
.sort((a, b) => b[1].calls - a[1].calls || b[1].resultTokens - a[1].resultTokens)
|
|
1029
|
+
.map(([name]) => name);
|
|
1030
|
+
this.cursor = Math.min(this.cursor, Math.max(0, this.toolGroupOrder.length - 1));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
774
1033
|
private shiftPeriod(direction: 1 | -1): void {
|
|
775
1034
|
const idx = PERIOD_ORDER.indexOf(this.period);
|
|
776
1035
|
const next = (idx + direction + PERIOD_ORDER.length) % PERIOD_ORDER.length;
|
|
777
1036
|
this.period = PERIOD_ORDER[next]!;
|
|
778
1037
|
this.refreshProviderOrder();
|
|
1038
|
+
this.refreshToolGroupOrder();
|
|
779
1039
|
this.requestRender();
|
|
780
1040
|
}
|
|
781
1041
|
|
|
@@ -808,7 +1068,8 @@ class UsagePanel {
|
|
|
808
1068
|
const title = th.fg("accent", th.bold("Pi Usage"));
|
|
809
1069
|
const tabs = this.renderViewTabs();
|
|
810
1070
|
const periods = this.renderPeriodTabs(width);
|
|
811
|
-
|
|
1071
|
+
const dateRange = th.fg("dim", this.renderPeriodDateRange());
|
|
1072
|
+
return [title, "", periods, tabs, dateRange, ""];
|
|
812
1073
|
}
|
|
813
1074
|
|
|
814
1075
|
private renderViewTabs(): string {
|
|
@@ -830,6 +1091,11 @@ class UsagePanel {
|
|
|
830
1091
|
return pickFitting(width, [full, `${fallback} ${th.fg("dim", "[Tab/←→]")}`, fallback]);
|
|
831
1092
|
}
|
|
832
1093
|
|
|
1094
|
+
private renderPeriodDateRange(): string {
|
|
1095
|
+
const [from, to] = dateRangeForPeriod(this.period, this.report);
|
|
1096
|
+
return `From ${formatDate(from)} to ${formatDate(to)}`;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
833
1099
|
// ----- summary view -----------------------------------------------------
|
|
834
1100
|
|
|
835
1101
|
private renderSummary(width: number): string[] {
|
|
@@ -857,7 +1123,7 @@ class UsagePanel {
|
|
|
857
1123
|
}
|
|
858
1124
|
|
|
859
1125
|
lines.push(th.bold("Sustainability"));
|
|
860
|
-
lines.push(th.fg("dim", "
|
|
1126
|
+
lines.push(th.fg("dim", "AI estimates are approximate inference ranges using impact-equivalences."));
|
|
861
1127
|
lines.push("");
|
|
862
1128
|
lines.push(...this.renderImpactBlock(width));
|
|
863
1129
|
lines.push("");
|
|
@@ -921,7 +1187,7 @@ class UsagePanel {
|
|
|
921
1187
|
const carbon = impact.carbon.kgCO2e;
|
|
922
1188
|
const lines: string[] = [];
|
|
923
1189
|
|
|
924
|
-
const profileNote = `${impact.profile.label} ·
|
|
1190
|
+
const profileNote = `${impact.profile.label} · ${impact.region.label}`;
|
|
925
1191
|
lines.push(`${indent}${th.fg("dim", profileNote)}`);
|
|
926
1192
|
|
|
927
1193
|
lines.push(
|
|
@@ -931,24 +1197,11 @@ class UsagePanel {
|
|
|
931
1197
|
`${indent}${th.fg("dim", padTo("Carbon", 12, "right"))} ${th.bold(formatRange(carbon.min, carbon.typical, carbon.max, "kg CO₂e"))}`,
|
|
932
1198
|
);
|
|
933
1199
|
|
|
934
|
-
const
|
|
935
|
-
if (
|
|
936
|
-
lines.push("");
|
|
937
|
-
lines.push(`${indent}${th.fg("dim", "Roughly equivalent to:")}`);
|
|
938
|
-
const bodyWidth = Math.max(20, width - indent.length - 4);
|
|
939
|
-
for (const phrase of equivalents) {
|
|
940
|
-
const wrapped = wrapTextWithAnsi(`• ${phrase}`, bodyWidth);
|
|
941
|
-
for (let i = 0; i < wrapped.length; i++) {
|
|
942
|
-
const prefix = i === 0 ? `${indent} ` : `${indent} `;
|
|
943
|
-
lines.push(`${prefix}${wrapped[i]}`);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
if (impact.disclaimer) {
|
|
1200
|
+
const equivalent = randomItem(impact.equivalents);
|
|
1201
|
+
if (equivalent) {
|
|
949
1202
|
lines.push("");
|
|
950
1203
|
const wrapped = wrapTextWithAnsi(
|
|
951
|
-
th.fg("dim",
|
|
1204
|
+
th.fg("dim", `Roughly equivalent to ${equivalent}`),
|
|
952
1205
|
Math.max(20, width - indent.length),
|
|
953
1206
|
);
|
|
954
1207
|
for (const part of wrapped) lines.push(`${indent}${part}`);
|
|
@@ -1047,6 +1300,96 @@ class UsagePanel {
|
|
|
1047
1300
|
return [th.fg("border", "─".repeat(layout.totalWidth)), row, ""];
|
|
1048
1301
|
}
|
|
1049
1302
|
|
|
1303
|
+
// ----- tools view --------------------------------------------------------
|
|
1304
|
+
|
|
1305
|
+
private renderTools(layout: TableLayout): string[] {
|
|
1306
|
+
const th = this.theme;
|
|
1307
|
+
const slice = this.report[this.period];
|
|
1308
|
+
const lines: string[] = [];
|
|
1309
|
+
const columns = toolColumns();
|
|
1310
|
+
const totalWidth = layout.nameWidth + columns.reduce((acc, col) => acc + col.width, 0);
|
|
1311
|
+
|
|
1312
|
+
lines.push(th.bold("Extensions / tools"));
|
|
1313
|
+
lines.push(th.fg("dim", "Sorted by call count. Result tokens are estimated from tool output size."));
|
|
1314
|
+
lines.push("");
|
|
1315
|
+
lines.push(this.renderToolHeader(layout.nameWidth, columns));
|
|
1316
|
+
lines.push(th.fg("border", "─".repeat(totalWidth)));
|
|
1317
|
+
|
|
1318
|
+
if (this.toolGroupOrder.length === 0) {
|
|
1319
|
+
lines.push(th.fg("dim", " No tool usage recorded for this period."));
|
|
1320
|
+
} else {
|
|
1321
|
+
for (let i = 0; i < this.toolGroupOrder.length; i++) {
|
|
1322
|
+
const name = this.toolGroupOrder[i]!;
|
|
1323
|
+
const group = slice.toolGroups.get(name)!;
|
|
1324
|
+
const isSelected = i === this.cursor;
|
|
1325
|
+
const isExpanded = this.expanded.has(name);
|
|
1326
|
+
lines.push(this.renderToolGroupRow(name, group, layout.nameWidth, columns, isSelected, isExpanded));
|
|
1327
|
+
|
|
1328
|
+
if (isExpanded) {
|
|
1329
|
+
const tools = Array.from(group.tools.entries()).sort(
|
|
1330
|
+
(a, b) => b[1].calls - a[1].calls || b[1].resultTokens - a[1].resultTokens,
|
|
1331
|
+
);
|
|
1332
|
+
for (const [toolName, stats] of tools) {
|
|
1333
|
+
lines.push(this.renderToolRow(toolName, stats, layout.nameWidth, columns));
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
lines.push(th.fg("border", "─".repeat(totalWidth)));
|
|
1340
|
+
lines.push(this.renderToolTotalRow(slice, layout.nameWidth, columns));
|
|
1341
|
+
lines.push("");
|
|
1342
|
+
lines.push(...this.renderHelp(totalWidth));
|
|
1343
|
+
return lines;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
private renderToolHeader(nameWidth: number, columns: ToolColumn[]): string {
|
|
1347
|
+
const th = this.theme;
|
|
1348
|
+
let header = padTo("Extension / Tool", nameWidth);
|
|
1349
|
+
for (const col of columns) header += th.fg("dim", padTo(col.label, col.width, "left"));
|
|
1350
|
+
return th.fg("muted", header);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
private renderToolGroupRow(
|
|
1354
|
+
name: string,
|
|
1355
|
+
stats: ToolGroupBucket,
|
|
1356
|
+
nameWidth: number,
|
|
1357
|
+
columns: ToolColumn[],
|
|
1358
|
+
selected: boolean,
|
|
1359
|
+
expanded: boolean,
|
|
1360
|
+
): string {
|
|
1361
|
+
const th = this.theme;
|
|
1362
|
+
const arrow = expanded ? "▾" : "▸";
|
|
1363
|
+
const prefix = selected ? th.fg("accent", `${arrow} `) : th.fg("dim", `${arrow} `);
|
|
1364
|
+
const innerWidth = Math.max(nameWidth - 2, 0);
|
|
1365
|
+
const display = innerWidth > 0 ? truncateToWidth(name, innerWidth) : "";
|
|
1366
|
+
const styled = selected ? th.fg("accent", display) : display;
|
|
1367
|
+
return prefix + padTo(styled, innerWidth) + columns.map((col) => padTo(col.value(stats), col.width, "left")).join("");
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private renderToolRow(
|
|
1371
|
+
name: string,
|
|
1372
|
+
stats: ToolBucket,
|
|
1373
|
+
nameWidth: number,
|
|
1374
|
+
columns: ToolColumn[],
|
|
1375
|
+
): string {
|
|
1376
|
+
const th = this.theme;
|
|
1377
|
+
const indent = " ";
|
|
1378
|
+
const innerWidth = Math.max(nameWidth - indent.length, 0);
|
|
1379
|
+
const display = innerWidth > 0 ? truncateToWidth(name, innerWidth) : "";
|
|
1380
|
+
return indent + padTo(th.fg("dim", display), innerWidth) + columns.map((col) => th.fg("dim", padTo(col.value(stats), col.width, "left"))).join("");
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private renderToolTotalRow(slice: PeriodReport, nameWidth: number, columns: ToolColumn[]): string {
|
|
1384
|
+
const total = emptyToolBucket();
|
|
1385
|
+
for (const group of slice.toolGroups.values()) {
|
|
1386
|
+
total.calls += group.calls;
|
|
1387
|
+
total.resultTokens += group.resultTokens;
|
|
1388
|
+
for (const sessionId of group.sessions) total.sessions.add(sessionId);
|
|
1389
|
+
}
|
|
1390
|
+
return padTo(this.theme.bold("Total"), nameWidth) + columns.map((col) => padTo(col.value(total), col.width, "left")).join("");
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1050
1393
|
// ----- patterns view ----------------------------------------------------
|
|
1051
1394
|
|
|
1052
1395
|
private renderPatterns(width: number): string[] {
|
|
@@ -1086,14 +1429,14 @@ class UsagePanel {
|
|
|
1086
1429
|
private renderHelp(width: number): string[] {
|
|
1087
1430
|
const th = this.theme;
|
|
1088
1431
|
const variants =
|
|
1089
|
-
this.view === "providers"
|
|
1432
|
+
this.view === "providers" || this.view === "tools"
|
|
1090
1433
|
? [
|
|
1091
|
-
"[Tab/←→] period · [↑↓] select · [Enter] expand · [v/1-
|
|
1434
|
+
"[Tab/←→] period · [↑↓] select · [Enter] expand · [v/1-4] view · [q] close",
|
|
1092
1435
|
"[Tab] period · [↑↓] · [Enter] · [v] view · [q]",
|
|
1093
1436
|
"[↑↓] · [Enter] · [q]",
|
|
1094
1437
|
]
|
|
1095
1438
|
: [
|
|
1096
|
-
"[Tab/←→] period · [v/1-
|
|
1439
|
+
"[Tab/←→] period · [v/1-4] view · [q] close",
|
|
1097
1440
|
"[Tab] period · [v] view · [q]",
|
|
1098
1441
|
"[v] view · [q]",
|
|
1099
1442
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mono-all",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "All pi-mono extensions and bundled skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -10,23 +10,23 @@
|
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"pi-mono-ask-user-question": "1.7.4",
|
|
13
|
+
"pi-mono-auto-fix": "0.3.1",
|
|
13
14
|
"pi-mono-btw": "1.7.4",
|
|
14
15
|
"pi-mono-clear": "1.7.3",
|
|
15
16
|
"pi-mono-context": "0.1.1",
|
|
16
|
-
"pi-mono-auto-fix": "0.3.1",
|
|
17
17
|
"pi-mono-context-guard": "1.7.3",
|
|
18
18
|
"pi-mono-linear": "0.2.2",
|
|
19
19
|
"pi-mono-figma": "0.2.2",
|
|
20
|
-
"pi-mono-loop": "1.7.3",
|
|
21
|
-
"pi-mono-multi-edit": "1.7.3",
|
|
22
|
-
"pi-mono-review": "1.8.2",
|
|
23
20
|
"pi-common": "0.1.1",
|
|
24
|
-
"pi-mono-
|
|
21
|
+
"pi-mono-review": "1.8.2",
|
|
22
|
+
"pi-mono-loop": "1.7.3",
|
|
25
23
|
"pi-mono-simplify": "1.7.3",
|
|
26
|
-
"pi-mono-team-mode": "2.3.2",
|
|
27
24
|
"pi-mono-status-line": "1.7.3",
|
|
28
|
-
"pi-mono-
|
|
29
|
-
"pi-mono-
|
|
25
|
+
"pi-mono-multi-edit": "1.7.3",
|
|
26
|
+
"pi-mono-team-mode": "2.3.2",
|
|
27
|
+
"pi-mono-usage": "0.1.1",
|
|
28
|
+
"pi-mono-web-search": "0.1.0",
|
|
29
|
+
"pi-mono-sentinel": "1.10.2"
|
|
30
30
|
},
|
|
31
31
|
"bundledDependencies": [
|
|
32
32
|
"pi-mono-ask-user-question",
|