opencode-usage-insights 0.0.0-stage → 0.1.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/dist/tui.js ADDED
@@ -0,0 +1,3515 @@
1
+ // @bun
2
+ // src/tui.tsx
3
+ import { use as _$use2 } from "@opentui/solid";
4
+ import { createElement as _$createElement3 } from "@opentui/solid";
5
+ import { createComponent as _$createComponent3 } from "@opentui/solid";
6
+ import { Plugin } from "@opencode/plugin/tui";
7
+ import { StyledText, fg } from "@opentui/core";
8
+ import { Show, createMemo as createMemo3, createRenderEffect as createRenderEffect2, createSignal as createSignal2 } from "solid-js";
9
+
10
+ // src/core.ts
11
+ var PROMPT_BOUNDARIES = new Set(["user", "synthetic", "system"]);
12
+ function transcriptTail(messages, count) {
13
+ const segments = [];
14
+ let current = [];
15
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
16
+ const message = messages[index];
17
+ if (message.type === "assistant") {
18
+ current.unshift(message);
19
+ continue;
20
+ }
21
+ if (!PROMPT_BOUNDARIES.has(message.type))
22
+ continue;
23
+ if (current.length === 0)
24
+ continue;
25
+ segments.unshift({ assistants: current, prompt: message });
26
+ if (segments.length >= count)
27
+ return segments;
28
+ current = [];
29
+ }
30
+ if (current.length > 0 && segments.length < count)
31
+ segments.unshift({ assistants: current, prompt: undefined });
32
+ return segments;
33
+ }
34
+ function tokenCounts(message) {
35
+ const tokens = message.tokens;
36
+ return {
37
+ input: tokens?.input ?? 0,
38
+ output: tokens?.output ?? 0,
39
+ reasoning: tokens?.reasoning ?? 0,
40
+ read: tokens?.cache?.read ?? 0,
41
+ write: tokens?.cache?.write ?? 0
42
+ };
43
+ }
44
+ function totalize(steps) {
45
+ const totals = { input: 0, cached: 0, output: 0, reasoning: 0, cacheWrite: 0, cost: 0, steps: steps.length };
46
+ for (const step of steps) {
47
+ const counts = tokenCounts(step);
48
+ totals.input += counts.input;
49
+ totals.cached += counts.read;
50
+ totals.output += counts.output;
51
+ totals.reasoning += counts.reasoning;
52
+ totals.cacheWrite += counts.write;
53
+ totals.cost += step.cost ?? 0;
54
+ }
55
+ return totals;
56
+ }
57
+ function identityKey(model) {
58
+ if (!model)
59
+ return;
60
+ return `${model.providerID}/${model.id}${model.variant ? `#${model.variant}` : ""}`;
61
+ }
62
+ function cacheRead(message) {
63
+ return message?.tokens?.cache?.read ?? 0;
64
+ }
65
+ function stepCreated(message) {
66
+ return message?.time?.created;
67
+ }
68
+ function stepEnd(message) {
69
+ const time = message?.time;
70
+ if (!time)
71
+ return;
72
+ return time.completed ?? time.streamed ?? time.created;
73
+ }
74
+ function cacheDrop(reference, current, scope = "provider") {
75
+ if (!reference)
76
+ return;
77
+ const before = reference.model;
78
+ const after = current.model;
79
+ if (!before || !after)
80
+ return;
81
+ if (scope === "provider" && before.providerID !== after.providerID)
82
+ return;
83
+ if (before.id !== after.id || (before.variant ?? "") !== (after.variant ?? "")) {
84
+ return;
85
+ }
86
+ const drop = cacheRead(reference) - cacheRead(current);
87
+ if (isNoiseDrop(after.providerID, drop))
88
+ return;
89
+ return drop > 0 ? drop : undefined;
90
+ }
91
+ function stepGap(reference, current) {
92
+ const ended = stepEnd(reference);
93
+ const started = stepCreated(current);
94
+ if (ended === undefined || started === undefined)
95
+ return;
96
+ return Math.max(0, started - ended);
97
+ }
98
+ function tokensPerSecond(produced, from, to) {
99
+ if (from === undefined || to === undefined || to <= from)
100
+ return;
101
+ if (produced <= 0)
102
+ return;
103
+ return produced / (to - from) * 1000;
104
+ }
105
+ var DROP_EXEMPT_PROVIDERS = new Set(["openai"]);
106
+ function isSettledStep(message) {
107
+ if (message.time?.completed !== undefined)
108
+ return true;
109
+ return message.finish !== undefined && message.finish !== null;
110
+ }
111
+ function computeTurn(messages, session) {
112
+ const segments = transcriptTail(messages, 2);
113
+ const last = segments[segments.length - 1];
114
+ if (!last)
115
+ return;
116
+ const previous = segments[segments.length - 2];
117
+ const lastStep = last.assistants[last.assistants.length - 1];
118
+ if (!lastStep)
119
+ return;
120
+ if (!isSettledStep(lastStep))
121
+ return;
122
+ const previousStep = previous?.assistants[previous.assistants.length - 1];
123
+ const settled = last.assistants.filter(isSettledStep);
124
+ const drops = [];
125
+ let reference = previousStep !== undefined && isSettledStep(previousStep) ? previousStep : undefined;
126
+ for (const step of settled) {
127
+ const drop = cacheDrop(reference, step);
128
+ if (drop !== undefined) {
129
+ drops.push({
130
+ messageID: step.id,
131
+ tokens: drop,
132
+ prevCached: cacheRead(reference),
133
+ gapMs: stepGap(reference, step)
134
+ });
135
+ }
136
+ reference = step;
137
+ }
138
+ const provider = lastStep.model?.providerID;
139
+ const previousProvider = previousStep?.model?.providerID;
140
+ const key = identityKey(lastStep.model);
141
+ const previousKey = identityKey(previousStep?.model);
142
+ return {
143
+ endAt: stepEnd(lastStep),
144
+ startAt: stepCreated(settled[0] ?? lastStep),
145
+ promptAt: last.prompt?.time?.created,
146
+ promptGapMs: promptGap(last.prompt, previous?.prompt),
147
+ providerID: provider,
148
+ modelID: lastStep.model?.id,
149
+ variant: lastStep.model?.variant,
150
+ providerChanged: previousProvider !== undefined && provider !== undefined && previousProvider !== provider,
151
+ modelChanged: previousKey !== undefined && key !== undefined && previousKey !== key && previousProvider === provider,
152
+ totals: totalize(settled),
153
+ latest: totalize([lastStep]),
154
+ session,
155
+ drops,
156
+ cacheDropTokens: drops.reduce((sum, drop) => sum + drop.tokens, 0),
157
+ cacheExpired: drops.length > 0,
158
+ steps: settled.length
159
+ };
160
+ }
161
+ function promptGap(current, previous) {
162
+ const now = current?.time?.created;
163
+ const before = previous?.time?.created;
164
+ if (now === undefined || before === undefined)
165
+ return;
166
+ return Math.max(0, now - before);
167
+ }
168
+ function isNoiseDrop(providerID, drop) {
169
+ if (providerID === undefined)
170
+ return false;
171
+ return DROP_EXEMPT_PROVIDERS.has(providerID) && drop >= 1024 && drop <= 2048;
172
+ }
173
+ var DEFAULT_LABELS = {
174
+ delta: "\u0394",
175
+ turn: "",
176
+ session: "sess",
177
+ step: "step",
178
+ steps: "steps",
179
+ input: "in",
180
+ cached: "cached",
181
+ output: "out",
182
+ reasoning: "reason",
183
+ cost: "cost",
184
+ cacheExpired: "cache expired",
185
+ providerChanged: "changed",
186
+ modelChanged: "model changed"
187
+ };
188
+ var DEFAULT_FIELDS = {
189
+ clock: true,
190
+ delta: true,
191
+ model: true,
192
+ variant: true,
193
+ turn: true,
194
+ session: true,
195
+ reasoning: false,
196
+ cost: true,
197
+ cacheExpiry: true
198
+ };
199
+ function formatClock(ms) {
200
+ const date = new Date(ms);
201
+ const pad = (value) => String(value).padStart(2, "0");
202
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
203
+ }
204
+ function formatDuration(ms) {
205
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
206
+ const seconds = totalSeconds % 60;
207
+ const minutes = Math.floor(totalSeconds / 60) % 60;
208
+ const hours = Math.floor(totalSeconds / 3600) % 24;
209
+ const days = Math.floor(totalSeconds / 86400);
210
+ if (days > 0)
211
+ return `${days}d${hours}h`;
212
+ if (hours > 0)
213
+ return `${hours}h${String(minutes).padStart(2, "0")}m`;
214
+ if (minutes > 0)
215
+ return `${minutes}m${String(seconds).padStart(2, "0")}s`;
216
+ return `${seconds}s`;
217
+ }
218
+ function formatTokens(value) {
219
+ const n = Math.max(0, Math.round(value));
220
+ if (n < 1000)
221
+ return String(n);
222
+ if (n < 999500) {
223
+ const k = n / 1000;
224
+ return `${trimZeros(k.toFixed(k < 10 ? 2 : 1))}k`;
225
+ }
226
+ if (n < 999500000) {
227
+ const m = n / 1e6;
228
+ return `${trimZeros(m.toFixed(m < 10 ? 2 : 1))}M`;
229
+ }
230
+ return `${Math.round(n / 1e6)}M`;
231
+ }
232
+ function trimZeros(value) {
233
+ return value.includes(".") ? value.replace(/0+$/, "").replace(/\.$/, "") : value;
234
+ }
235
+ function formatCost(value) {
236
+ if (value <= 0)
237
+ return "$0";
238
+ if (value < 1)
239
+ return `$${value.toFixed(4)}`;
240
+ return `$${value.toFixed(2)}`;
241
+ }
242
+ function displayTotals(totals) {
243
+ const prompt = totals.input + totals.cached + totals.cacheWrite;
244
+ return {
245
+ input: prompt - totals.cached,
246
+ cached: totals.cached,
247
+ cacheWrite: totals.cacheWrite,
248
+ output: totals.output + totals.reasoning,
249
+ reasoning: totals.reasoning,
250
+ cost: totals.cost,
251
+ steps: totals.steps,
252
+ prompt
253
+ };
254
+ }
255
+ function totalsText(prefix, totals, labels, fields, stepCount = 0, cost = totals.cost) {
256
+ const display = displayTotals(totals);
257
+ const input = display.prompt === display.input ? `${labels.input} ${formatTokens(display.input)}` : `${labels.input} ${formatTokens(display.input)} (${formatTokens(display.prompt)})`;
258
+ const parts = [
259
+ input,
260
+ `${labels.cached} ${formatTokens(display.cached)}`,
261
+ `${labels.output} ${formatTokens(display.output)}`
262
+ ];
263
+ if (fields.reasoning)
264
+ parts.push(`${labels.reasoning} ${formatTokens(display.reasoning)}`);
265
+ if (fields.cost && cost > 0)
266
+ parts.push(`${labels.cost} ${formatCost(cost)}`);
267
+ const steps = stepCount > 1 ? `${stepCount} ${stepCount === 1 ? labels.step : labels.steps}` : "";
268
+ const head = [prefix, steps].filter((piece) => piece !== "").join(" ");
269
+ const numbers = parts.join(", ");
270
+ if (head === "")
271
+ return numbers;
272
+ return stepCount > 1 ? `${head} \xB7 ${numbers}` : `${head} ${numbers}`;
273
+ }
274
+ function formatLineSegments(stats, options) {
275
+ const labels = { ...DEFAULT_LABELS, ...options?.labels };
276
+ const fields = { ...DEFAULT_FIELDS, ...options?.fields };
277
+ const turnTotals = options?.turnTotals ?? "latest";
278
+ const segments = [];
279
+ if (fields.clock && stats.endAt !== undefined) {
280
+ segments.push({ text: formatClock(stats.endAt), tone: "muted" });
281
+ }
282
+ if (fields.delta && stats.promptGapMs !== undefined) {
283
+ segments.push({ text: `${labels.delta} ${formatDuration(stats.promptGapMs)}`, tone: "muted" });
284
+ }
285
+ if (stats.providerID !== undefined) {
286
+ let text = stats.providerID;
287
+ if (fields.model && stats.modelID !== undefined)
288
+ text += `/${stats.modelID}`;
289
+ if (fields.variant && stats.variant)
290
+ text += ` ${stats.variant}`;
291
+ segments.push({ text, tone: stats.providerChanged ? "warning" : "base" });
292
+ if (stats.providerChanged)
293
+ segments.push({ text: labels.providerChanged, tone: "warning" });
294
+ else if (stats.modelChanged)
295
+ segments.push({ text: labels.modelChanged, tone: "muted" });
296
+ }
297
+ if (fields.turn && stats.totals.steps > 0) {
298
+ const totals = turnTotals === "sum" ? stats.totals : stats.latest;
299
+ segments.push({
300
+ text: totalsText(labels.turn, totals, labels, fields, turnTotals === "sum" ? stats.totals.steps : stats.steps, stats.totals.cost),
301
+ tone: "base"
302
+ });
303
+ }
304
+ if (fields.session && stats.session) {
305
+ segments.push({ text: totalsText(labels.session, stats.session, labels, fields, 0, stats.session.cost), tone: "muted" });
306
+ }
307
+ if (fields.cacheExpiry && stats.cacheExpired) {
308
+ const count = stats.drops.length > 1 ? `\xD7${stats.drops.length}` : "";
309
+ segments.push({ text: `\u26A0 ${labels.cacheExpired} (\u2212${formatTokens(stats.cacheDropTokens)}${count})`, tone: "warning" });
310
+ }
311
+ return segments;
312
+ }
313
+ function formatLine(stats, options) {
314
+ return formatLineSegments(stats, options).map((segment) => segment.text).join(" \xB7 ");
315
+ }
316
+
317
+ // src/text.ts
318
+ var TEXT_LIMIT = 512;
319
+ function sanitizeText(value, limit = TEXT_LIMIT) {
320
+ if (limit <= 0)
321
+ return "";
322
+ let cleaned = "";
323
+ for (const character of value) {
324
+ const code = character.codePointAt(0) ?? 0;
325
+ if (code <= 31 || code === 127 || code >= 128 && code <= 159)
326
+ continue;
327
+ cleaned += character;
328
+ if (cleaned.length >= limit)
329
+ break;
330
+ }
331
+ return cleaned;
332
+ }
333
+ function sanitizeLabel(value, limit = TEXT_LIMIT) {
334
+ return sanitizeText(value, limit).trim();
335
+ }
336
+ var TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
337
+ function isToken(value, limit = 64) {
338
+ return value.length > 0 && value.length <= limit && TOKEN.test(value);
339
+ }
340
+
341
+ // src/line.ts
342
+ function toTranscript(messages) {
343
+ const transcript = [];
344
+ for (const message of messages) {
345
+ if (typeof message.id !== "string" || typeof message.type !== "string")
346
+ continue;
347
+ if (message.type !== "assistant") {
348
+ transcript.push({ type: message.type, id: message.id, time: message.time });
349
+ continue;
350
+ }
351
+ transcript.push({
352
+ type: "assistant",
353
+ id: message.id,
354
+ time: message.time,
355
+ model: toModelRef(message.model),
356
+ tokens: message.tokens,
357
+ cost: message.cost,
358
+ finish: message.finish ?? undefined,
359
+ rawFinish: message.rawFinish ?? undefined
360
+ });
361
+ }
362
+ return transcript;
363
+ }
364
+ function toTranscriptTail(messages, segments = 2) {
365
+ const wanted = Math.max(2, segments);
366
+ const tail = [];
367
+ let collected = 0;
368
+ let closed = 0;
369
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
370
+ const message = messages[index];
371
+ if (message.type === "assistant") {
372
+ tail.push(message);
373
+ collected += 1;
374
+ continue;
375
+ }
376
+ if (message.type === undefined || !PROMPT_BOUNDARIES.has(message.type))
377
+ continue;
378
+ if (collected === 0)
379
+ continue;
380
+ tail.push(message);
381
+ collected = 0;
382
+ closed += 1;
383
+ if (closed >= wanted)
384
+ break;
385
+ }
386
+ tail.reverse();
387
+ return toTranscript(tail);
388
+ }
389
+ function toModelRef(model) {
390
+ if (model === undefined || model.providerID === undefined || model.id === undefined)
391
+ return;
392
+ if (model.variant === undefined)
393
+ return { providerID: model.providerID, id: model.id };
394
+ return { providerID: model.providerID, id: model.id, variant: model.variant };
395
+ }
396
+ function sessionTotals(info) {
397
+ if (info === undefined)
398
+ return;
399
+ const tokens = info.tokens;
400
+ return {
401
+ input: tokens?.input ?? 0,
402
+ cached: tokens?.cache?.read ?? 0,
403
+ output: tokens?.output ?? 0,
404
+ reasoning: tokens?.reasoning ?? 0,
405
+ cacheWrite: tokens?.cache?.write ?? 0,
406
+ cost: info.cost ?? 0,
407
+ steps: 0
408
+ };
409
+ }
410
+ function lineParts(stats, options) {
411
+ if (stats === undefined)
412
+ return [];
413
+ const parts = [];
414
+ const segments = formatLineSegments(stats, options);
415
+ segments.forEach((segment, index) => {
416
+ if (index > 0)
417
+ parts.push({ text: " \xB7 ", tone: "muted" });
418
+ parts.push({ text: sanitizeLabel(segment.text), tone: segment.tone });
419
+ });
420
+ return parts;
421
+ }
422
+ function lineText(parts) {
423
+ return parts.map((part) => part.text).join("");
424
+ }
425
+
426
+ // src/stats/queries.ts
427
+ var COMPACTION_KIND = "compaction";
428
+ var DEFAULT_TTL_HINT_MS = 10 * 60 * 1000;
429
+ var STEP_TIME = "COALESCE(created_at, recorded_at)";
430
+ var MODEL_KEY_SQL = "COALESCE(model_id, '(unknown)') || '#' || COALESCE(variant, '')";
431
+ function modelKey(modelID, variant) {
432
+ return `${modelID}#${variant ?? ""}`;
433
+ }
434
+ function modelLabel(modelID, variant) {
435
+ return variant === undefined ? modelID : `${modelID} ${variant}`;
436
+ }
437
+ var USAGE_SELECT = `COUNT(*) AS steps,
438
+ COALESCE(SUM(input_tokens), 0) AS input,
439
+ COALESCE(SUM(cache_read), 0) AS cached,
440
+ COALESCE(SUM(cache_write), 0) AS cacheWrite,
441
+ COALESCE(SUM(output_tokens), 0) AS output,
442
+ COALESCE(SUM(reasoning_tokens), 0) AS reasoning,
443
+ COALESCE(SUM(cost), 0) AS cost`;
444
+ function usageFrom(row) {
445
+ return {
446
+ steps: row.steps ?? 0,
447
+ input: row.input ?? 0,
448
+ cached: row.cached ?? 0,
449
+ cacheWrite: row.cacheWrite ?? 0,
450
+ output: row.output ?? 0,
451
+ reasoning: row.reasoning ?? 0,
452
+ cost: row.cost ?? 0
453
+ };
454
+ }
455
+ function rangeWhere(range, column = STEP_TIME) {
456
+ const clauses = [];
457
+ const params = [];
458
+ if (range?.from !== undefined) {
459
+ clauses.push(`${column} >= ?`);
460
+ params.push(range.from);
461
+ }
462
+ if (range?.to !== undefined) {
463
+ clauses.push(`${column} < ?`);
464
+ params.push(range.to);
465
+ }
466
+ return { where: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "", params };
467
+ }
468
+ function buildWhere(range, filters, column = STEP_TIME) {
469
+ const rangeClause = rangeWhere(range, column);
470
+ const clauses = rangeClause.where === "" ? [] : [rangeClause.where.slice("WHERE ".length)];
471
+ const params = [...rangeClause.params];
472
+ if (filters?.modelID !== undefined && filters.modelID !== "") {
473
+ clauses.push("model_id = ?");
474
+ params.push(filters.modelID);
475
+ }
476
+ if (filters?.modelKey !== undefined && filters.modelKey !== "") {
477
+ clauses.push(`${MODEL_KEY_SQL} = ?`);
478
+ params.push(filters.modelKey);
479
+ }
480
+ if (filters?.provider !== undefined && filters.provider !== "") {
481
+ clauses.push("provider_id = ?");
482
+ params.push(filters.provider);
483
+ }
484
+ if (filters?.sessionID !== undefined && filters.sessionID !== "") {
485
+ clauses.push("session_id = ?");
486
+ params.push(filters.sessionID);
487
+ }
488
+ return { where: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "", params };
489
+ }
490
+ function cacheHitRatio(usage) {
491
+ const total = usage.cached + usage.input + usage.cacheWrite;
492
+ return total > 0 ? usage.cached / total : undefined;
493
+ }
494
+ function overview(db, range, filters) {
495
+ const steps = buildWhere(range, filters);
496
+ const row = db.query(`SELECT ${USAGE_SELECT},
497
+ COUNT(DISTINCT session_id) AS sessions,
498
+ MIN(${STEP_TIME}) AS firstAt,
499
+ MAX(COALESCE(completed_at, ${STEP_TIME})) AS lastAt,
500
+ COALESCE(SUM(CASE WHEN streamed_at > created_at THEN output_tokens + reasoning_tokens ELSE 0 END), 0) AS produced,
501
+ COALESCE(SUM(CASE WHEN streamed_at > created_at THEN streamed_at - created_at ELSE 0 END), 0) AS window
502
+ FROM steps ${steps.where}`).get(...steps.params);
503
+ const prompts = rangeWhere(range, "created_at");
504
+ const promptRow = db.query(`SELECT COUNT(*) AS prompts FROM prompts ${prompts.where}`).get(...prompts.params);
505
+ const usage = usageFrom(row ?? { steps: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, cost: 0 });
506
+ return {
507
+ ...usage,
508
+ sessions: row?.sessions ?? 0,
509
+ prompts: promptRow?.prompts ?? 0,
510
+ firstAt: row?.firstAt ?? undefined,
511
+ lastAt: row?.lastAt ?? undefined,
512
+ cacheHitRatio: cacheHitRatio(usage),
513
+ tps: row === null ? undefined : tokensPerSecond(row.produced ?? 0, 0, row.window ?? 0)
514
+ };
515
+ }
516
+ function models(db, range, filters) {
517
+ const filter = buildWhere(range, filters);
518
+ const rows = db.query(`SELECT provider_id, model_id, variant,
519
+ ${USAGE_SELECT},
520
+ MIN(${STEP_TIME}) AS first_at,
521
+ MAX(COALESCE(completed_at, recorded_at)) AS last_at
522
+ FROM steps ${filter.where}
523
+ GROUP BY provider_id, model_id, variant
524
+ ORDER BY steps DESC, cached DESC`).all(...filter.params);
525
+ const models = new Map;
526
+ for (const row of rows) {
527
+ const modelID = row.model_id ?? "(unknown)";
528
+ const variant = row.variant ?? undefined;
529
+ const key = `${modelID}#${variant ?? ""}`;
530
+ let model = models.get(key);
531
+ if (!model) {
532
+ model = { modelID, variant, providers: [], totals: emptyUsage() };
533
+ models.set(key, model);
534
+ }
535
+ const usage = usageFrom(row);
536
+ model.providers.push({
537
+ ...usage,
538
+ providerID: row.provider_id ?? "(unknown)",
539
+ firstAt: row.first_at ?? undefined,
540
+ lastAt: row.last_at ?? undefined
541
+ });
542
+ addUsage(model.totals, usage);
543
+ }
544
+ return [...models.values()].sort((a, b) => billedTokens(b.totals) - billedTokens(a.totals));
545
+ }
546
+ function emptyUsage() {
547
+ return { steps: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, cost: 0 };
548
+ }
549
+ function addUsage(target, source) {
550
+ target.steps += source.steps;
551
+ target.input += source.input;
552
+ target.cached += source.cached;
553
+ target.cacheWrite += source.cacheWrite;
554
+ target.output += source.output;
555
+ target.reasoning += source.reasoning;
556
+ target.cost += source.cost;
557
+ }
558
+ function billedTokens(usage) {
559
+ return usage.input + usage.cached + usage.output;
560
+ }
561
+ function percentile(values, p) {
562
+ if (values.length === 0)
563
+ return;
564
+ const sorted = [...values].sort((a, b) => a - b);
565
+ const rank = Math.ceil(p / 100 * sorted.length);
566
+ const index = Math.min(sorted.length - 1, Math.max(0, rank - 1));
567
+ return sorted[index];
568
+ }
569
+ function latencyStats(values) {
570
+ return {
571
+ count: values.length,
572
+ p50: percentile(values, 50),
573
+ p90: percentile(values, 90),
574
+ p99: percentile(values, 99)
575
+ };
576
+ }
577
+ function sessions(db, options) {
578
+ if (options?.sessionIDs !== undefined && options.sessionIDs.length === 0)
579
+ return [];
580
+ const range = options?.range;
581
+ const filter = buildWhere(range, options?.filters, "COALESCE(st.created_at, st.recorded_at)");
582
+ const limit = options?.limit ?? 20;
583
+ const clauses = filter.where === "" ? [] : [filter.where.slice("WHERE ".length)];
584
+ const params = [...filter.params];
585
+ if (options?.sessionIDs !== undefined) {
586
+ const ids = options.sessionIDs.slice(0, 500);
587
+ clauses.push(`st.session_id IN (${ids.map(() => "?").join(", ")})`);
588
+ params.push(...ids);
589
+ }
590
+ const rows = db.query(`SELECT st.session_id, s.title, s.directory, s.parent_id,
591
+ GROUP_CONCAT(DISTINCT st.provider_id) AS providers,
592
+ GROUP_CONCAT(DISTINCT st.model_id) AS models,
593
+ (SELECT COUNT(*) FROM prompts p WHERE p.session_id = st.session_id) AS prompts,
594
+ COUNT(*) AS steps,
595
+ COALESCE(SUM(st.input_tokens), 0) AS input,
596
+ COALESCE(SUM(st.cache_read), 0) AS cached,
597
+ COALESCE(SUM(st.cache_write), 0) AS cacheWrite,
598
+ COALESCE(SUM(st.output_tokens), 0) AS output,
599
+ COALESCE(SUM(st.reasoning_tokens), 0) AS reasoning,
600
+ COALESCE(SUM(st.cost), 0) AS cost,
601
+ MIN(COALESCE(st.created_at, st.recorded_at)) AS first_at,
602
+ MAX(COALESCE(st.completed_at, st.created_at, st.recorded_at)) AS last_at
603
+ FROM steps st
604
+ LEFT JOIN sessions s ON s.session_id = st.session_id
605
+ ${clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : ""}
606
+ GROUP BY st.session_id
607
+ ORDER BY last_at DESC
608
+ LIMIT ?`).all(...params, limit);
609
+ return rows.map((row) => {
610
+ const usage = usageFrom(row);
611
+ return {
612
+ ...usage,
613
+ sessionID: row.session_id,
614
+ parentID: row.parent_id ?? undefined,
615
+ title: row.title ?? undefined,
616
+ directory: row.directory ?? undefined,
617
+ providers: row.providers ? row.providers.split(",").filter(Boolean) : [],
618
+ models: row.models ? row.models.split(",").filter(Boolean) : [],
619
+ prompts: row.prompts ?? 0,
620
+ firstAt: row.first_at ?? undefined,
621
+ lastAt: row.last_at ?? undefined,
622
+ cacheHitRatio: cacheHitRatio(usage)
623
+ };
624
+ });
625
+ }
626
+ function childSessionIDs(db, parentID) {
627
+ return db.query("SELECT session_id FROM sessions WHERE parent_id = ? ORDER BY session_id").all(parentID).map((row) => row.session_id);
628
+ }
629
+ function sessionUsageOf(db, sessionIDs) {
630
+ return sessions(db, { sessionIDs, limit: sessionIDs.length });
631
+ }
632
+ function turnFinished(finish) {
633
+ return finish !== null && finish !== "" && finish !== "tool-calls";
634
+ }
635
+ function stepUsage(row) {
636
+ return {
637
+ steps: 1,
638
+ input: row.input_tokens ?? 0,
639
+ cached: row.cache_read ?? 0,
640
+ cacheWrite: row.cache_write ?? 0,
641
+ output: row.output_tokens ?? 0,
642
+ reasoning: row.reasoning_tokens ?? 0,
643
+ cost: row.cost ?? 0
644
+ };
645
+ }
646
+ function stepMessage(row) {
647
+ return {
648
+ type: "assistant",
649
+ id: row.message_id,
650
+ time: { created: row.created_at ?? row.recorded_at, completed: row.completed_at ?? undefined },
651
+ model: row.provider_id !== null && row.model_id !== null ? { providerID: row.provider_id, id: row.model_id, variant: row.variant ?? undefined } : undefined,
652
+ tokens: {
653
+ input: row.input_tokens ?? 0,
654
+ output: row.output_tokens ?? 0,
655
+ reasoning: row.reasoning_tokens ?? 0,
656
+ cache: { read: row.cache_read ?? 0, write: row.cache_write ?? 0 }
657
+ },
658
+ finish: row.finish ?? undefined
659
+ };
660
+ }
661
+ function turns(db, options) {
662
+ const limit = options?.limit ?? 50;
663
+ const filter = buildWhere(options?.range, options?.filters);
664
+ const clauses = filter.where === "" ? [] : [filter.where.slice("WHERE ".length)];
665
+ const params = [...filter.params];
666
+ if (options?.sessionID !== undefined) {
667
+ clauses.push("session_id = ?");
668
+ params.push(options.sessionID);
669
+ }
670
+ const floor = options?.sessionID === undefined ? undefined : promptFloor(db, options.sessionID, limit + 1);
671
+ if (floor !== undefined) {
672
+ clauses.push(`${STEP_TIME} >= ?`);
673
+ params.push(floor);
674
+ }
675
+ const steps = db.query(`SELECT message_id, session_id, provider_id, model_id, variant,
676
+ input_tokens, cache_read, cache_write, output_tokens, reasoning_tokens, cost,
677
+ created_at, streamed_at, recorded_at, completed_at, finish
678
+ FROM steps ${clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : ""}
679
+ ORDER BY session_id, ${STEP_TIME}`).all(...params);
680
+ const promptClause = options?.sessionID === undefined ? "" : `WHERE session_id = ?${floor === undefined ? "" : " AND created_at >= ?"}`;
681
+ const promptParams = options?.sessionID === undefined ? [] : floor === undefined ? [options.sessionID] : [options.sessionID, floor];
682
+ const promptRows = db.query(`SELECT session_id, created_at FROM prompts ${promptClause} ORDER BY session_id, created_at`).all(...promptParams);
683
+ const promptsBySession = new Map;
684
+ for (const row of promptRows) {
685
+ if (row.created_at === null)
686
+ continue;
687
+ const list = promptsBySession.get(row.session_id) ?? [];
688
+ list.push(row.created_at);
689
+ promptsBySession.set(row.session_id, list);
690
+ }
691
+ const result = [];
692
+ let sessionID;
693
+ let bucket = [];
694
+ let promptIndex = -2;
695
+ let promptAt;
696
+ let previousPromptAt;
697
+ function close() {
698
+ const last = bucket[bucket.length - 1];
699
+ if (last === undefined || !turnFinished(last.finish)) {
700
+ bucket = [];
701
+ return;
702
+ }
703
+ const totals = emptyUsage();
704
+ for (const row of bucket)
705
+ addUsage(totals, stepUsage(row));
706
+ const drops = [];
707
+ let reference;
708
+ for (const row of bucket) {
709
+ const message = stepMessage(row);
710
+ const drop = cacheDrop(reference, message);
711
+ if (drop !== undefined) {
712
+ drops.push({ messageID: row.message_id, tokens: drop, gapMs: stepGap(reference, message) });
713
+ }
714
+ reference = message;
715
+ }
716
+ const providerID = last.provider_id ?? undefined;
717
+ const modelID = last.model_id ?? undefined;
718
+ const variant = last.variant ?? undefined;
719
+ let produced = 0;
720
+ let window = 0;
721
+ for (const row of bucket) {
722
+ if (row.created_at === null || row.streamed_at === null || row.streamed_at <= row.created_at)
723
+ continue;
724
+ produced += (row.output_tokens ?? 0) + (row.reasoning_tokens ?? 0);
725
+ window += row.streamed_at - row.created_at;
726
+ }
727
+ const previous = result.length > 0 && result[result.length - 1].sessionID === last.session_id ? result[result.length - 1] : undefined;
728
+ result.push({
729
+ sessionID: last.session_id,
730
+ promptAt,
731
+ endAt: last.completed_at ?? last.created_at ?? last.recorded_at,
732
+ promptGapMs: promptAt !== undefined && previousPromptAt !== undefined ? Math.max(0, promptAt - previousPromptAt) : undefined,
733
+ providerID,
734
+ modelID,
735
+ variant,
736
+ providerChanged: previous?.providerID !== undefined && providerID !== undefined && previous.providerID !== providerID,
737
+ modelChanged: previous !== undefined && previous.providerID === providerID && `${previous.modelID ?? ""}#${previous.variant ?? ""}` !== `${modelID ?? ""}#${variant ?? ""}`,
738
+ steps: bucket.length,
739
+ latest: stepUsage(last),
740
+ totals,
741
+ drops,
742
+ finish: last.finish ?? undefined,
743
+ tps: tokensPerSecond(produced, 0, window)
744
+ });
745
+ bucket = [];
746
+ }
747
+ for (const row of steps) {
748
+ if (row.session_id !== sessionID) {
749
+ close();
750
+ sessionID = row.session_id;
751
+ promptIndex = -2;
752
+ promptAt = undefined;
753
+ previousPromptAt = undefined;
754
+ }
755
+ const time = row.created_at ?? row.recorded_at;
756
+ const prompts = promptsBySession.get(row.session_id) ?? [];
757
+ let index = -1;
758
+ for (let candidate = prompts.length - 1;candidate >= 0; candidate -= 1) {
759
+ if ((prompts[candidate] ?? Number.POSITIVE_INFINITY) <= time) {
760
+ index = candidate;
761
+ break;
762
+ }
763
+ }
764
+ if (index !== promptIndex) {
765
+ close();
766
+ promptIndex = index;
767
+ promptAt = index >= 0 ? prompts[index] : undefined;
768
+ previousPromptAt = index >= 1 ? prompts[index - 1] : undefined;
769
+ }
770
+ bucket.push(row);
771
+ }
772
+ close();
773
+ return result.reverse().slice(0, limit);
774
+ }
775
+ function promptFloor(db, sessionID, count) {
776
+ const rows = db.query("SELECT created_at FROM prompts WHERE session_id = ? AND created_at IS NOT NULL ORDER BY created_at DESC LIMIT ?").all(sessionID, count);
777
+ return rows.length === count ? rows[count - 1].created_at : undefined;
778
+ }
779
+ function filterOptions(db) {
780
+ const pick = (column) => db.query(`SELECT ${column} AS value, COUNT(*) AS steps FROM steps WHERE ${column} IS NOT NULL GROUP BY ${column} ORDER BY steps DESC`).all().map((row) => ({ value: row.value, steps: row.steps }));
781
+ return { models: pick("model_id"), providers: pick("provider_id") };
782
+ }
783
+
784
+ // src/panel-data.ts
785
+ var PANEL_NAME = "usage-insights";
786
+ var PANEL_VIEWS = ["overview", "models", "providers", "cache", "periods", "latency", "sessions", "turns", "db"];
787
+ var RANGE_KEYS = ["session", "today", "7d", "30d", "all"];
788
+ var GRANULARITIES = ["day", "week", "month"];
789
+ function parseArgs(input) {
790
+ const parsed = {};
791
+ for (const token of (input ?? "").split(/\s+/)) {
792
+ const value = token.trim().toLowerCase();
793
+ if (value === "")
794
+ continue;
795
+ if (PANEL_VIEWS.includes(value))
796
+ parsed.view = value;
797
+ else if (RANGE_KEYS.includes(value))
798
+ parsed.range = value;
799
+ }
800
+ return parsed;
801
+ }
802
+ function rangeLabel(key) {
803
+ switch (key) {
804
+ case "session":
805
+ return "this session";
806
+ case "today":
807
+ return "today";
808
+ case "7d":
809
+ return "7 days";
810
+ case "30d":
811
+ return "30 days";
812
+ case "all":
813
+ return "all time";
814
+ }
815
+ }
816
+ function nextIn(values, current, step) {
817
+ const index = values.indexOf(current);
818
+ if (index === -1)
819
+ return values[0];
820
+ return values[(index + step + values.length) % values.length];
821
+ }
822
+ var DEFAULT_SETTINGS = {
823
+ view: "overview",
824
+ range: "all",
825
+ sort: "",
826
+ sortDesc: true,
827
+ granularity: "day",
828
+ turnsScope: "session",
829
+ modelID: "",
830
+ providerID: ""
831
+ };
832
+ function sanitizeSettings(raw) {
833
+ const settings = { ...DEFAULT_SETTINGS, ...raw ?? {} };
834
+ return {
835
+ view: PANEL_VIEWS.includes(settings.view) ? settings.view : DEFAULT_SETTINGS.view,
836
+ range: RANGE_KEYS.includes(settings.range) ? settings.range : DEFAULT_SETTINGS.range,
837
+ sort: typeof settings.sort === "string" ? settings.sort : DEFAULT_SETTINGS.sort,
838
+ sortDesc: settings.sortDesc === true,
839
+ granularity: GRANULARITIES.includes(settings.granularity) ? settings.granularity : DEFAULT_SETTINGS.granularity,
840
+ turnsScope: settings.turnsScope === "all" ? "all" : "session",
841
+ modelID: typeof settings.modelID === "string" ? settings.modelID : "",
842
+ providerID: typeof settings.providerID === "string" ? settings.providerID : ""
843
+ };
844
+ }
845
+ function formatPercent(value, digits = 1) {
846
+ return value === undefined ? "\u2013" : `${(value * 100).toFixed(digits)}%`;
847
+ }
848
+ function formatCount(value) {
849
+ return Math.round(value).toLocaleString("en-US");
850
+ }
851
+ function formatMs(value) {
852
+ if (value === undefined)
853
+ return "\u2013";
854
+ if (value < 1000)
855
+ return `${Math.round(value)}ms`;
856
+ if (value < 60000)
857
+ return `${(value / 1000).toFixed(1)}s`;
858
+ return formatDuration(value);
859
+ }
860
+ function formatTps(value) {
861
+ return value === undefined ? "\u2013" : `${value.toFixed(1)}/s`;
862
+ }
863
+ function formatTpsCompact(value) {
864
+ if (value === undefined)
865
+ return "\u2013";
866
+ return value >= 10 ? `${Math.round(value)}/s` : `${value.toFixed(1)}/s`;
867
+ }
868
+ function formatGap(value) {
869
+ if (value === undefined)
870
+ return "\u2013";
871
+ if (value < 60000)
872
+ return `${Math.round(value / 1000)}s`;
873
+ if (value < 3600000)
874
+ return `${Math.round(value / 60000)}m`;
875
+ if (value < 86400000)
876
+ return `${(value / 3600000).toFixed(1)}h`;
877
+ return `${(value / 86400000).toFixed(1)}d`;
878
+ }
879
+ function truncate(value, width) {
880
+ if (width <= 1)
881
+ return "";
882
+ const clean = sanitizeText(value);
883
+ if (clean.length <= width)
884
+ return clean;
885
+ return `${clean.slice(0, width - 1).trimEnd()}\u2026`;
886
+ }
887
+ function truncateMiddle(value, width) {
888
+ if (width <= 1)
889
+ return "";
890
+ const clean = sanitizeText(value);
891
+ if (clean.length <= width)
892
+ return clean;
893
+ const head = Math.ceil((width - 1) / 2);
894
+ const tail = width - 1 - head;
895
+ return `${clean.slice(0, head)}\u2026${clean.slice(clean.length - tail)}`;
896
+ }
897
+ function tableWidths(width, layouts) {
898
+ for (const layout of layouts) {
899
+ const total = layout.reduce((sum, column) => sum + column, 0) + layout.length - 1;
900
+ if (total <= width)
901
+ return [...layout];
902
+ }
903
+ const last = layouts[layouts.length - 1];
904
+ return last === undefined ? [] : [...last];
905
+ }
906
+ function modelCell(modelID, variant, width) {
907
+ return truncateMiddle(variant === undefined ? modelID : `${modelID} ${variant}`, width);
908
+ }
909
+ function padCell(value, width, align = "left") {
910
+ if (value.length >= width)
911
+ return value;
912
+ const padding = " ".repeat(width - value.length);
913
+ return align === "right" ? `${padding}${value}` : `${value}${padding}`;
914
+ }
915
+ function formatTableRow(cells, widths, aligns) {
916
+ const parts = [];
917
+ for (let index = 0;index < cells.length; index += 1) {
918
+ const width = widths[index] ?? 8;
919
+ const align = aligns?.[index] ?? "left";
920
+ parts.push(padCell(truncate(cells[index] ?? "", width), width, align));
921
+ }
922
+ return parts.join(" ");
923
+ }
924
+ function count(n, singular, plural = `${singular}s`) {
925
+ return `${formatCount(n)} ${n === 1 ? singular : plural}`;
926
+ }
927
+ function kvGrid(pairs, width) {
928
+ const lines = [];
929
+ const label = 11;
930
+ const column = Math.floor(width / 2);
931
+ for (let index = 0;index < pairs.length; index += 2) {
932
+ const left = pairs[index];
933
+ const right = pairs[index + 1];
934
+ const leftText = `${padCell(left[0], label)}${left[1]}`;
935
+ if (right === undefined) {
936
+ lines.push({ text: leftText, tone: "base" });
937
+ continue;
938
+ }
939
+ const rightText = `${padCell(right[0], label)}${right[1]}`;
940
+ const gap = Math.max(2, column - leftText.length);
941
+ if (leftText.length + gap + rightText.length <= width) {
942
+ lines.push({ text: `${leftText}${" ".repeat(gap)}${rightText}`, tone: "base" });
943
+ } else {
944
+ lines.push({ text: leftText, tone: "base" });
945
+ lines.push({ text: rightText, tone: "base" });
946
+ }
947
+ }
948
+ return lines;
949
+ }
950
+ var HEADER_TONE = "accent";
951
+ var SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
952
+ function reduceSeries(values, maxBars = 48) {
953
+ if (values.length <= maxBars)
954
+ return [...values];
955
+ const group = Math.ceil(values.length / maxBars);
956
+ const reduced = [];
957
+ for (let index = 0;index < values.length; index += group) {
958
+ reduced.push(values.slice(index, index + group).reduce((sum, value) => sum + value, 0));
959
+ }
960
+ return reduced;
961
+ }
962
+ function sparkline(values, maxBars = 48) {
963
+ if (values.length === 0)
964
+ return "";
965
+ const series = reduceSeries(values, maxBars);
966
+ const max = Math.max(...series);
967
+ if (max <= 0)
968
+ return SPARK[0].repeat(series.length);
969
+ return series.map((value) => SPARK[Math.min(SPARK.length - 1, Math.max(0, Math.round(value / max * (SPARK.length - 1))))]).join("");
970
+ }
971
+ function usageBuckets(usage) {
972
+ const prompt = usage.input + usage.cached + usage.cacheWrite;
973
+ return {
974
+ steps: usage.steps,
975
+ input: prompt - usage.cached,
976
+ prompt,
977
+ cached: usage.cached,
978
+ cacheWrite: usage.cacheWrite,
979
+ output: usage.output + usage.reasoning,
980
+ reasoning: usage.reasoning,
981
+ cost: usage.cost,
982
+ hit: prompt > 0 ? usage.cached / prompt : undefined
983
+ };
984
+ }
985
+ function viewBar(view, width = 200) {
986
+ const key = (index) => String((index + 1) % 10);
987
+ const full = PANEL_VIEWS.map((name, index) => name === view ? `[${key(index)} ${name}]` : `${key(index)} ${name}`);
988
+ const candidates = [full, PANEL_VIEWS.map((name, index) => name === view ? `[${key(index)} ${name}]` : `${key(index)} ${name.slice(0, 3)}`)];
989
+ for (const parts of candidates) {
990
+ const text = parts.join(" ");
991
+ if (text.length <= width)
992
+ return { text, tone: "muted" };
993
+ }
994
+ return { text: `[${PANEL_VIEWS.indexOf(view) + 1} ${view}] \xB7 1-9 or [ ] to switch`, tone: "muted" };
995
+ }
996
+ function keysFor(of) {
997
+ if (of === "db")
998
+ return "\u2191\u2193 pick a retention \xB7 enter prune (asks first) \xB7 v vacuum \xB7 r refresh \xB7 1-9 view \xB7 ? help";
999
+ if (of === "periods")
1000
+ return "\u2191\u2193 move \xB7 enter drill \xB7 b bucket \xB7 t range \xB7 m/p filter \xB7 x clear \xB7 1-9 view \xB7 ? help";
1001
+ if (of === "turns")
1002
+ return "\u2191\u2193 move \xB7 a scope \xB7 t range \xB7 m/p filter \xB7 x clear \xB7 1-9 view \xB7 ? help";
1003
+ return "\u2191\u2193 move \xB7 enter drill \xB7 s sort \xB7 t range \xB7 m/p filter \xB7 x clear \xB7 1-9 view \xB7 ? help";
1004
+ }
1005
+ function filterSummary(filters) {
1006
+ const parts = [];
1007
+ if (filters.modelID !== undefined && filters.modelID !== "")
1008
+ parts.push(`model=${filters.modelID}`);
1009
+ if (filters.providerID !== undefined && filters.providerID !== "")
1010
+ parts.push(`provider=${filters.providerID}`);
1011
+ return parts.length > 0 ? parts.join(" \xB7 ") : undefined;
1012
+ }
1013
+ function granularityLabel(granularity) {
1014
+ return granularity === "day" ? "per day" : granularity === "week" ? "per week" : "per month";
1015
+ }
1016
+ function helpLines(width) {
1017
+ const lines = [
1018
+ { text: "keys", tone: HEADER_TONE },
1019
+ { text: " \u2191 \u2193 j k move the cursor \xB7 enter drills in \xB7 backspace goes back" },
1020
+ { text: " 1-9 [ ] choose a view \xB7 cycle views (9 is db)" },
1021
+ { text: " t shift+t previous / next range" },
1022
+ { text: " s shift+s cycle sort column \xB7 reverse" },
1023
+ { text: " b periods bucket: day \u2192 week \u2192 month" },
1024
+ { text: " a turns scope: this session \u2194 all sessions" },
1025
+ { text: " m p filter by model / provider (x clears)" },
1026
+ { text: " r re-read the database \xB7 f full screen \xB7 escape close" },
1027
+ { text: "" },
1028
+ { text: "numbers \xB7 the same buckets as the live line", tone: HEADER_TONE },
1029
+ { text: " prompt everything the provider saw: cached + fresh + writes" },
1030
+ { text: " in the uncached part: fresh input + cache writes" },
1031
+ { text: " cached prompt read from the provider prompt cache" },
1032
+ { text: " write part of `in`: prompt written into that cache" },
1033
+ { text: " hit cached / prompt, weighted over the selection" },
1034
+ { text: " hit p50 median of the per-step hit: spots a split pattern" },
1035
+ { text: " out everything generated: visible output + reasoning" },
1036
+ { text: " reasoning part of `out`" },
1037
+ { text: " cost money, not tokens: a turn sums every step; $0 is left out" },
1038
+ { text: " wall request \u2192 completed step \xB7 stream request \u2192 last chunk" },
1039
+ { text: " tps out / streaming seconds, shown as p50" },
1040
+ { text: " switches how often a step's model or provider differs from the step" },
1041
+ { text: " before it in the same session (the models view's detail), with" },
1042
+ { text: " the cache a provider switch cost" },
1043
+ { text: "" },
1044
+ { text: "identity", tone: HEADER_TONE },
1045
+ { text: " provider the provider the model ran through, as OpenCode is configured" },
1046
+ { text: " (opencode, opencode-go, deepseek, \u2026). A gateway may re-route" },
1047
+ { text: " a request, but the upstream that ran the inference is not" },
1048
+ { text: " observable, so the provider is the identity every view shows." },
1049
+ { text: "" },
1050
+ { text: "database", tone: HEADER_TONE },
1051
+ { text: " the db view shows where the file lives, what it holds and how much" },
1052
+ { text: " of it is old: \u2191\u2193 pick a retention, enter prunes from its cutoff date," },
1053
+ { text: " v gives the space back" },
1054
+ { text: " local time \xB7 SQLite in the OpenCode state directory" }
1055
+ ];
1056
+ return lines.map((line) => ({ ...line, text: truncate(line.text, width) }));
1057
+ }
1058
+ function usagePairs(usage) {
1059
+ if (usage === undefined)
1060
+ return [];
1061
+ const buckets = usageBuckets(usage);
1062
+ const pairs = [
1063
+ ["steps", formatCount(buckets.steps)],
1064
+ ["in", formatTokens(buckets.input)],
1065
+ ["prompt", formatTokens(buckets.prompt)],
1066
+ ["cached", formatTokens(buckets.cached)],
1067
+ ["hit", formatPercent(buckets.hit)],
1068
+ ["out", formatTokens(buckets.output)],
1069
+ ["write", formatTokens(buckets.cacheWrite)]
1070
+ ];
1071
+ if (buckets.reasoning > 0)
1072
+ pairs.push(["reasoning", formatTokens(buckets.reasoning)]);
1073
+ pairs.push(["cost", formatCost(buckets.cost)]);
1074
+ if ("tps" in usage && usage.tps !== undefined)
1075
+ pairs.push(["tps", formatTps(usage.tps)]);
1076
+ return pairs;
1077
+ }
1078
+ function overviewLines(data, width) {
1079
+ const lines = [];
1080
+ lines.push({
1081
+ text: `${rangeLabel(data.range)} \xB7 ${count(data.usage.steps, "step")} \xB7 ${count(data.usage.sessions, "session")} \xB7 ${count(data.usage.prompts, "prompt")}`,
1082
+ tone: "muted"
1083
+ });
1084
+ lines.push(...kvGrid(buildOverviewPairs(data.usage, "all"), width));
1085
+ if (data.activity !== undefined && data.activity.length > 1) {
1086
+ const series = reduceSeries(data.activity);
1087
+ lines.push({ text: "", tone: "muted" });
1088
+ lines.push({ text: `activity ${sparkline(series)} ${count(series.length, "bucket")}`, tone: "muted" });
1089
+ }
1090
+ if (data.session !== undefined) {
1091
+ lines.push({ text: "", tone: "muted" });
1092
+ lines.push({ text: "this session", tone: HEADER_TONE });
1093
+ lines.push(...kvGrid(buildOverviewPairs(data.session, "session"), width));
1094
+ if (data.subagents !== undefined && data.subagents.length > 0) {
1095
+ const steps = data.subagents.reduce((sum, row) => sum + row.steps, 0);
1096
+ const cost = data.subagents.reduce((sum, row) => sum + row.cost, 0);
1097
+ lines.push({
1098
+ text: ` subagents ${count(data.subagents.length, "session")} \xB7 ${count(steps, "step")} \xB7 ${formatCost(cost)} (their own sessions)`,
1099
+ tone: "muted"
1100
+ });
1101
+ }
1102
+ }
1103
+ if (data.coverage !== undefined) {
1104
+ lines.push({ text: "", tone: "muted" });
1105
+ lines.push({ text: "data coverage", tone: HEADER_TONE });
1106
+ lines.push(...kvGrid(coveragePairs(data.coverage), width));
1107
+ }
1108
+ return lines;
1109
+ }
1110
+ function buildOverviewPairs(usage, scope) {
1111
+ const pairs = usagePairs(usage);
1112
+ if (scope === "all") {
1113
+ pairs.push(["first", usage.firstAt === undefined ? "\u2013" : new Date(usage.firstAt).toLocaleString("en-GB")]);
1114
+ pairs.push(["last", usage.lastAt === undefined ? "\u2013" : new Date(usage.lastAt).toLocaleString("en-GB")]);
1115
+ }
1116
+ return pairs;
1117
+ }
1118
+ function coveragePairs(coverage) {
1119
+ const share = (missing) => coverage.steps === 0 ? "\u2013" : `${(missing / coverage.steps * 100).toFixed(1)}%`;
1120
+ return [
1121
+ ["models", `${formatCount(coverage.models)} \xB7 unknown ${share(coverage.missingModel)}`],
1122
+ ["providers", formatCount(coverage.providers)],
1123
+ ["unsettled", `${formatCount(coverage.unsettled)} \xB7 no tps ${share(coverage.missingTps)}`],
1124
+ [
1125
+ "window",
1126
+ `${coverage.firstAt === undefined ? "\u2013" : new Date(coverage.firstAt).toLocaleString("en-GB")} \u2192 ${coverage.lastAt === undefined ? "\u2013" : new Date(coverage.lastAt).toLocaleString("en-GB")}`
1127
+ ]
1128
+ ];
1129
+ }
1130
+ function modelColumns(width) {
1131
+ const layouts = [
1132
+ { model: 30, steps: 6, prompt: 10, cached: 10, hit: 7, out: 9, cost: 9, spread: 24 },
1133
+ { model: 26, steps: 6, prompt: 9, cached: 9, hit: 7, out: 8, cost: 8, spread: 20 },
1134
+ { model: 20, steps: 6, prompt: 9, cached: 9, hit: 6, out: 8, cost: 8, spread: 0 },
1135
+ { model: 18, steps: 5, prompt: 8, cached: 8, hit: 0, out: 7, cost: 8, spread: 0 },
1136
+ { model: 14, steps: 5, prompt: 8, cached: 8, hit: 0, out: 7, cost: 0, spread: 0 }
1137
+ ];
1138
+ for (const layout of layouts) {
1139
+ const total = Object.values(layout).reduce((sum, column) => sum + column, 0) + Object.values(layout).filter((column) => column > 0).length - 1;
1140
+ if (total <= width)
1141
+ return layout;
1142
+ }
1143
+ return layouts[layouts.length - 1];
1144
+ }
1145
+ function modelsLines(rows, width, extras, limit = 40) {
1146
+ const columns = modelColumns(width);
1147
+ const widths = [columns.model, columns.steps, columns.prompt, columns.cached];
1148
+ const aligns = ["left", "right", "right", "right"];
1149
+ const header = ["model", "steps", "prompt", "cached"];
1150
+ if (columns.hit > 0) {
1151
+ widths.push(columns.hit);
1152
+ aligns.push("right");
1153
+ header.push("hit");
1154
+ }
1155
+ widths.push(columns.out);
1156
+ aligns.push("right");
1157
+ header.push("out");
1158
+ if (columns.cost > 0) {
1159
+ widths.push(columns.cost);
1160
+ aligns.push("right");
1161
+ header.push("cost");
1162
+ }
1163
+ if (columns.spread > 0) {
1164
+ widths.push(columns.spread);
1165
+ aligns.push("left");
1166
+ header.push("providers");
1167
+ }
1168
+ const lines = [{ text: formatTableRow(header, widths, aligns), tone: HEADER_TONE }];
1169
+ for (const model of rows.slice(0, limit)) {
1170
+ const key = modelKey(model.modelID, model.variant);
1171
+ const name = modelCell(model.modelID, model.variant, columns.model);
1172
+ const buckets = usageBuckets(model.totals);
1173
+ const providers = unique(model.providers.map((provider) => provider.providerID));
1174
+ const cache = extras?.cache?.get(key);
1175
+ const switches = extras?.switches?.get(key);
1176
+ const cells = [name, formatCount(buckets.steps), formatTokens(buckets.prompt), formatTokens(buckets.cached)];
1177
+ if (columns.hit > 0)
1178
+ cells.push(formatPercent(cache?.hit ?? buckets.hit));
1179
+ cells.push(formatTokens(buckets.output));
1180
+ if (columns.cost > 0)
1181
+ cells.push(formatCost(buckets.cost));
1182
+ if (columns.spread > 0)
1183
+ cells.push(providers.join(", "));
1184
+ lines.push({
1185
+ text: formatTableRow(cells, widths, aligns),
1186
+ tone: "base",
1187
+ row: { kind: "model", value: model.modelID, ...model.variant !== undefined ? { variant: model.variant } : {} }
1188
+ });
1189
+ const details = [];
1190
+ if (cache !== undefined)
1191
+ details.push(`p50 hit ${formatPercent(cache.hitP50)}`);
1192
+ if (switches !== undefined && switches.changes > 0) {
1193
+ const scope = switches.providerChanges > 0 ? ` (${formatCount(switches.providerChanges)} provider)` : "";
1194
+ details.push(`${count(switches.changes, "switch", "switches")}${scope}`);
1195
+ if (switches.cacheLost > 0)
1196
+ details.push(`${formatTokens(switches.cacheLost)} lost on a switch`);
1197
+ }
1198
+ if (details.length > 0) {
1199
+ lines.push({ text: truncate(`${" ".repeat(columns.model + 1)}${details.join(" \xB7 ")}`, width), tone: "muted" });
1200
+ }
1201
+ }
1202
+ if (rows.length > limit)
1203
+ lines.push({ text: `\u2026 ${count(rows.length - limit, "more model")} \xB7 narrow the range or filter`, tone: "muted" });
1204
+ if (rows.length === 0)
1205
+ lines.push({ text: "no usage recorded for this range", tone: "muted" });
1206
+ return lines;
1207
+ }
1208
+ function unique(values) {
1209
+ return [...new Set(values)];
1210
+ }
1211
+ var tableWidth = (columns) => columns.reduce((sum, column) => sum + column.width, 0) + columns.length - 1;
1212
+ function fitColumns(columns, width) {
1213
+ const chosen = [...columns];
1214
+ while (chosen.length > 1 && tableWidth(chosen) > width) {
1215
+ const optional = [...chosen].reverse().findIndex((column) => column.optional === true);
1216
+ if (optional === -1)
1217
+ break;
1218
+ chosen.splice(chosen.length - 1 - optional, 1);
1219
+ }
1220
+ return chosen;
1221
+ }
1222
+ function renderTable(rows, columns, width, options) {
1223
+ const chosen = fitColumns(columns, width);
1224
+ const widths = chosen.map((column) => column.width);
1225
+ const aligns = chosen.map((column) => column.align ?? "left");
1226
+ const alignsTyped = aligns;
1227
+ const lines = [{ text: formatTableRow(chosen.map((column) => column.header), widths, alignsTyped), tone: HEADER_TONE }];
1228
+ if (rows.length === 0)
1229
+ lines.push({ text: options.empty, tone: "muted" });
1230
+ for (const row of rows.slice(0, options.limit)) {
1231
+ const identity = options.row?.(row);
1232
+ lines.push({
1233
+ text: formatTableRow(chosen.map((column) => column.cell(row)), widths, alignsTyped),
1234
+ tone: options.tone?.(row) ?? "base",
1235
+ ...identity === undefined ? {} : { row: identity }
1236
+ });
1237
+ }
1238
+ if (rows.length > options.limit)
1239
+ lines.push({ text: `\u2026 ${count(rows.length - options.limit, "more row")}`, tone: "muted" });
1240
+ return lines;
1241
+ }
1242
+ function providerSummaryLines(rows, width, limit = 40) {
1243
+ const lines = renderTable(rows, [
1244
+ { header: "provider", width: 24, cell: (row) => truncate(row.provider ?? "\u2013 (unknown)", 24) },
1245
+ { header: "models", width: 13, align: "right", optional: true, cell: (row) => formatCount(row.models.length) },
1246
+ { header: "steps", width: 6, align: "right", cell: (row) => formatCount(row.usage.steps) },
1247
+ { header: "share", width: 6, align: "right", optional: true, cell: (row) => formatPercent(row.share, 0) },
1248
+ { header: "prompt", width: 10, align: "right", cell: (row) => formatTokens(row.cache.prompt) },
1249
+ { header: "cached", width: 10, align: "right", cell: (row) => formatTokens(row.cache.cached) },
1250
+ { header: "hit", width: 7, align: "right", optional: true, cell: (row) => formatPercent(row.cache.hit) },
1251
+ { header: "tps p50", width: 8, align: "right", optional: true, cell: (row) => formatTps(row.tps) },
1252
+ { header: "wall", width: 8, align: "right", optional: true, cell: (row) => formatMs(row.wall) },
1253
+ { header: "cost", width: 9, align: "right", optional: true, cell: (row) => formatCost(row.usage.cost) }
1254
+ ], width, {
1255
+ limit,
1256
+ empty: "no usage recorded for this range",
1257
+ row: (row) => row.provider === undefined ? undefined : { kind: "provider", value: row.provider },
1258
+ tone: (row) => row.provider === undefined ? "muted" : "base"
1259
+ });
1260
+ return lines;
1261
+ }
1262
+ function providerLines(rows, width, limit = 40) {
1263
+ return renderTable(rows, [
1264
+ { header: "model", width: 26, cell: (row) => modelCell(row.modelID, row.variant, 26) },
1265
+ { header: "provider", width: 22, cell: (row) => truncate(row.provider ?? "\u2013 (unknown)", 22) },
1266
+ { header: "steps", width: 6, align: "right", cell: (row) => formatCount(row.usage.steps) },
1267
+ { header: "share", width: 6, align: "right", optional: true, cell: (row) => formatPercent(row.share, 0) },
1268
+ { header: "prompt", width: 10, align: "right", cell: (row) => formatTokens(row.cache.prompt) },
1269
+ { header: "cached", width: 10, align: "right", cell: (row) => formatTokens(row.cache.cached) },
1270
+ { header: "hit", width: 7, align: "right", optional: true, cell: (row) => formatPercent(row.cache.hit) },
1271
+ { header: "tps p50", width: 8, align: "right", optional: true, cell: (row) => formatTps(row.tps) },
1272
+ { header: "wall", width: 8, align: "right", optional: true, cell: (row) => formatMs(row.wall) },
1273
+ { header: "cost", width: 9, align: "right", optional: true, cell: (row) => formatCost(row.usage.cost) }
1274
+ ], width, {
1275
+ limit,
1276
+ empty: "no usage recorded for this range",
1277
+ row: (row) => ({ kind: "model", value: row.modelID, ...row.variant !== undefined ? { variant: row.variant } : {} }),
1278
+ tone: (row) => row.provider === undefined ? "muted" : "base"
1279
+ });
1280
+ }
1281
+ function cacheLines(data, width) {
1282
+ const lines = [];
1283
+ const total = data.drops.reduce((sum, drop) => sum + drop.tokens, 0);
1284
+ lines.push({ text: `reuse losses ${count(data.drops.length, "event")} \xB7 ${formatTokens(total)} tokens`, tone: total > 0 ? "warning" : "base" });
1285
+ for (const reason of ["expired", "compaction", "unknown"]) {
1286
+ const group = data.drops.filter((drop) => drop.reason === reason);
1287
+ if (group.length === 0)
1288
+ continue;
1289
+ const tokens = group.reduce((sum, drop) => sum + drop.tokens, 0);
1290
+ lines.push({ text: ` ${padCell(reason, 11)}${formatCount(group.length)} \xB7 ${formatTokens(tokens)}`, tone: reason === "expired" ? "warning" : "muted" });
1291
+ }
1292
+ if (data.drops.length === 0)
1293
+ lines.push({ text: " none: prompt cache held through every gap", tone: "muted" });
1294
+ if (data.models !== undefined && data.models.length > 0) {
1295
+ lines.push({ text: "", tone: "muted" });
1296
+ lines.push({ text: "reuse by model", tone: HEADER_TONE });
1297
+ const widths = width >= 100 ? [30, 7, 11, 10, 8, 8, 10] : [18, 6, 9, 9, 7, 7, 9];
1298
+ lines.push({ text: formatTableRow(["model", "steps", "prompt", "cached", "write", "hit", "hit p50"], widths, ["left", "right", "right", "right", "right", "right", "right"]), tone: "muted" });
1299
+ for (const row of data.models.slice(0, 10)) {
1300
+ const name = modelCell(row.usage.modelID, row.usage.variant, widths[0]);
1301
+ lines.push({
1302
+ text: formatTableRow([name, formatCount(row.cache.steps), formatTokens(row.cache.prompt), formatTokens(row.cache.cached), formatTokens(row.cache.cacheWrite), formatPercent(row.cache.hit), formatPercent(row.cache.hitP50)], widths, ["left", "right", "right", "right", "right", "right", "right"]),
1303
+ tone: "base",
1304
+ row: { kind: "model", value: row.usage.modelID, ...row.usage.variant !== undefined ? { variant: row.usage.variant } : {} }
1305
+ });
1306
+ }
1307
+ }
1308
+ if (data.gap !== undefined && data.gap.samples > 0) {
1309
+ lines.push({ text: "", tone: "muted" });
1310
+ lines.push({ text: "cache survival against idle time", tone: HEADER_TONE });
1311
+ const widths = [9, 8, 9, 9, 10];
1312
+ lines.push({ text: formatTableRow(["gap", "pairs", "kept", "drops", "lost"], widths, ["left", "right", "right", "right", "right"]), tone: "muted" });
1313
+ for (const bucket of data.gap.buckets) {
1314
+ if (bucket.samples === 0)
1315
+ continue;
1316
+ lines.push({
1317
+ text: formatTableRow([bucket.label, formatCount(bucket.samples), formatPercent(bucket.retainedRatio, 0), formatCount(bucket.drops), formatTokens(bucket.lostTokens)], widths, ["left", "right", "right", "right", "right"]),
1318
+ tone: bucket.drops > 0 ? "warning" : "base"
1319
+ });
1320
+ }
1321
+ if (data.gap.providers.length > 1) {
1322
+ lines.push({ text: "", tone: "muted" });
1323
+ lines.push({ text: "reuse kept, by provider", tone: HEADER_TONE });
1324
+ const providerWidths = [26, 8, 9, 9];
1325
+ lines.push({ text: formatTableRow(["provider", "pairs", "kept", "lost"], providerWidths, ["left", "right", "right", "right"]), tone: "muted" });
1326
+ for (const row of data.gap.providers.slice(0, 8)) {
1327
+ lines.push({
1328
+ text: formatTableRow([row.provider, formatCount(row.samples), formatPercent(row.retainedRatio, 0), formatTokens(row.lostTokens)], providerWidths, ["left", "right", "right", "right"]),
1329
+ tone: "base"
1330
+ });
1331
+ }
1332
+ }
1333
+ }
1334
+ lines.push({ text: "", tone: "muted" });
1335
+ lines.push({ text: "observed cache TTL bounds", tone: HEADER_TONE });
1336
+ if (data.evidence.length === 0) {
1337
+ lines.push({ text: " no comparable step pairs yet", tone: "muted" });
1338
+ } else {
1339
+ const widths = [Math.max(18, Math.floor(width * 0.4)), 14, 12, 9];
1340
+ lines.push({ text: formatTableRow(["model", "hits up to", "drops from", "samples"], widths, ["left", "right", "right", "right"]), tone: "muted" });
1341
+ for (const row of data.evidence.slice(0, 10)) {
1342
+ lines.push({
1343
+ text: formatTableRow([`${row.providerID}/${row.modelID}`, formatGap(row.maxHitGapMs), formatGap(row.minDropGapMs), formatCount(row.samples)], widths, ["left", "right", "right", "right"]),
1344
+ tone: "base"
1345
+ });
1346
+ }
1347
+ }
1348
+ if (data.drops.length > 0) {
1349
+ lines.push({ text: "", tone: "muted" });
1350
+ lines.push({ text: "recent losses", tone: HEADER_TONE });
1351
+ for (const drop of [...data.drops].sort((a, b) => b.at - a.at).slice(0, 8)) {
1352
+ const when = new Date(drop.at).toLocaleTimeString("en-GB");
1353
+ lines.push({
1354
+ text: formatTableRow([when, `${drop.providerID ?? ""}/${drop.modelID ?? ""}`, `\u2212${formatTokens(drop.tokens)}`, `${formatGap(drop.gapMs)} idle \xB7 ${drop.reason}`], [10, Math.max(20, Math.floor(width * 0.42)), 10, 24], ["left", "left", "right", "left"]),
1355
+ tone: drop.reason === "expired" ? "warning" : "base"
1356
+ });
1357
+ }
1358
+ }
1359
+ return lines;
1360
+ }
1361
+ function periodLines(rows, width, granularity, limit = 40) {
1362
+ const lines = [];
1363
+ const totals = rows.reduce((sum, row) => {
1364
+ sum.steps += row.usage.steps;
1365
+ sum.sessions += row.sessions;
1366
+ sum.input += row.usage.input;
1367
+ sum.cached += row.usage.cached;
1368
+ sum.cacheWrite += row.usage.cacheWrite;
1369
+ sum.output += row.usage.output;
1370
+ sum.reasoning += row.usage.reasoning;
1371
+ sum.cost += row.usage.cost;
1372
+ return sum;
1373
+ }, { steps: 0, sessions: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, cost: 0 });
1374
+ const overall = usageBuckets(totals);
1375
+ lines.push({
1376
+ text: `${granularityLabel(granularity)} \xB7 ${count(rows.length, "period")} \xB7 ${count(totals.steps, "step")} \xB7 hit ${formatPercent(overall.hit)}`,
1377
+ tone: "muted"
1378
+ });
1379
+ if (rows.length > 1) {
1380
+ lines.push({ text: `steps ${sparkline(rows.map((row) => row.usage.steps))}`, tone: "muted" });
1381
+ lines.push({ text: `cached ${sparkline(rows.map((row) => row.usage.cached))}`, tone: "muted" });
1382
+ lines.push({ text: `cost ${sparkline(rows.map((row) => Math.round(row.usage.cost * 1e4)))}`, tone: "muted" });
1383
+ }
1384
+ const withSessions = tableWidths(width, [[12, 7, 7, 11, 10, 7, 9, 9]]).length === 8;
1385
+ const widths = withSessions ? [12, 7, 7, 11, 10, 7, 9, 9] : [12, 7, 11, 10, 7, 9, 9];
1386
+ const aligns = widths.map((_, index) => index === 0 ? "left" : "right");
1387
+ const header = withSessions ? ["period", "steps", "sess", "prompt", "cached", "hit", "out", "cost"] : ["period", "steps", "prompt", "cached", "hit", "out", "cost"];
1388
+ lines.push({ text: formatTableRow(header, widths, aligns), tone: HEADER_TONE });
1389
+ const newest = [...rows].sort((left, right) => right.at - left.at).slice(0, limit);
1390
+ for (const row of newest) {
1391
+ const buckets = usageBuckets(row.usage);
1392
+ const cells = [
1393
+ row.label,
1394
+ formatCount(row.steps),
1395
+ ...withSessions ? [formatCount(row.sessions)] : [],
1396
+ formatTokens(buckets.prompt),
1397
+ formatTokens(buckets.cached),
1398
+ formatPercent(buckets.hit, 0),
1399
+ formatTokens(buckets.output),
1400
+ formatCost(buckets.cost)
1401
+ ];
1402
+ lines.push({ text: formatTableRow(cells, widths, aligns), tone: "base", row: { kind: "period", value: row.key } });
1403
+ }
1404
+ lines.push({ text: "", tone: "muted" });
1405
+ lines.push({
1406
+ text: formatTableRow([
1407
+ "total",
1408
+ formatCount(totals.steps),
1409
+ ...withSessions ? [formatCount(totals.sessions)] : [],
1410
+ formatTokens(overall.prompt),
1411
+ formatTokens(overall.cached),
1412
+ formatPercent(overall.hit, 0),
1413
+ formatTokens(overall.output),
1414
+ formatCost(overall.cost)
1415
+ ], widths, aligns),
1416
+ tone: HEADER_TONE
1417
+ });
1418
+ if (rows.length === 0)
1419
+ lines.push({ text: "no usage recorded for this range", tone: "muted" });
1420
+ return lines;
1421
+ }
1422
+ function latencyLines(report, width, groups, limit = 12) {
1423
+ const lines = [];
1424
+ lines.push({
1425
+ text: formatTableRow(["wall", "p50", formatMs(report.wall.p50), "p90", formatMs(report.wall.p90), "p99", formatMs(report.wall.p99), `n ${formatCount(report.wall.count)}`], [10, 4, 9, 4, 9, 4, 9, 12]),
1426
+ tone: "base"
1427
+ });
1428
+ lines.push({
1429
+ text: formatTableRow(["stream", "p50", formatMs(report.stream.p50), "p90", formatMs(report.stream.p90), "p99", formatMs(report.stream.p99)], [10, 4, 9, 4, 9, 4, 9]),
1430
+ tone: "base"
1431
+ });
1432
+ lines.push({
1433
+ text: formatTableRow(["throughput", "p50", formatTps(report.tps.p50), "p90", formatTps(report.tps.p90), "p99", formatTps(report.tps.p99)], [10, 4, 9, 4, 9, 4, 9]),
1434
+ tone: "base"
1435
+ });
1436
+ lines.push({ text: "", tone: "muted" });
1437
+ lines.push({
1438
+ text: truncate("wall = request to completed \xB7 stream = request to last chunk \xB7 throughput = output+reasoning tokens/s", width),
1439
+ tone: "muted"
1440
+ });
1441
+ if (report.wall.count === 0)
1442
+ lines.push({ text: "no completed steps for this range", tone: "muted" });
1443
+ if (groups !== undefined && groups.length > 0) {
1444
+ lines.push({ text: "", tone: "muted" });
1445
+ lines.push({ text: "by group", tone: HEADER_TONE });
1446
+ const widths = width >= 110 ? [24, 6, 9, 9, 9, 9, 9, 9] : [16, 5, 8, 8, 8, 8, 8, 8];
1447
+ const aligns = ["left", "right", "right", "right", "right", "right", "right", "right"];
1448
+ lines.push({ text: formatTableRow(["group", "n", "wall p50", "wall p90", "stream", "tps p50", "tps p90", "cost"], widths, aligns), tone: "muted" });
1449
+ for (const row of groups.slice(0, limit)) {
1450
+ const thin = row.count < 5;
1451
+ lines.push({
1452
+ text: formatTableRow([row.label, formatCount(row.count), formatMs(row.wall.p50), formatMs(row.wall.p90), formatMs(row.stream.p50), formatTps(row.tps.p50), formatTps(row.tps.p90), formatCost(row.usage.cost)], widths, aligns),
1453
+ tone: thin ? "muted" : "base",
1454
+ row: { kind: "model", value: row.key }
1455
+ });
1456
+ }
1457
+ lines.push({ text: "n < 5 is dimmed: too few samples to compare", tone: "muted" });
1458
+ }
1459
+ return lines;
1460
+ }
1461
+ function sessionsLines(rows, width, options) {
1462
+ const limit = options?.limit ?? 20;
1463
+ const widths = [Math.max(18, Math.floor(width * 0.36)), 7, 11, 10, 7, 9, 9];
1464
+ const aligns = ["left", "right", "right", "right", "right", "right", "right"];
1465
+ const lines = [
1466
+ { text: formatTableRow(["title", "steps", "prompt", "cached", "hit", "out", "cost"], widths, aligns), tone: HEADER_TONE }
1467
+ ];
1468
+ const byID = new Map(rows.map((row) => [row.sessionID, row]));
1469
+ const ordered = rows.filter((row) => row.parentID === undefined || !byID.has(row.parentID));
1470
+ const children = rows.filter((row) => row.parentID !== undefined && byID.has(row.parentID));
1471
+ const orderedAll = [...ordered, ...children];
1472
+ for (const row of orderedAll.slice(0, limit)) {
1473
+ const child = row.parentID !== undefined && byID.has(row.parentID);
1474
+ const title = truncate(row.title ?? row.sessionID, Math.max(8, widths[0] - (child ? 2 : 0)));
1475
+ const buckets = usageBuckets(row);
1476
+ lines.push({
1477
+ text: formatTableRow([
1478
+ `${child ? "\u21B3 " : ""}${title}`,
1479
+ formatCount(buckets.steps),
1480
+ formatTokens(buckets.prompt),
1481
+ formatTokens(buckets.cached),
1482
+ formatPercent(buckets.hit, 0),
1483
+ formatTokens(buckets.output),
1484
+ formatCost(buckets.cost)
1485
+ ], widths, aligns),
1486
+ tone: child ? "muted" : "base",
1487
+ row: { kind: "session", value: row.sessionID }
1488
+ });
1489
+ const details = [row.directory ?? "", row.models.length > 1 ? count(row.models.length, "model") : ""].filter((part) => part !== "").join(" \xB7 ");
1490
+ lines.push({ text: truncate(`${" ".repeat(child ? 2 : 0)} ${details}`, width), tone: "muted" });
1491
+ }
1492
+ if (options?.rollup !== undefined && options.rollup.subagentSteps > 0) {
1493
+ const share = rows.reduce((sum, row) => sum + row.steps, 0);
1494
+ lines.push({ text: "", tone: "muted" });
1495
+ lines.push({
1496
+ text: `subagents ${formatCount(options.rollup.subagentSteps)} steps (${formatPercent(share > 0 ? options.rollup.subagentSteps / share : undefined)}) \xB7 ${formatCost(options.rollup.subagentCost)}`,
1497
+ tone: "muted"
1498
+ });
1499
+ }
1500
+ if (rows.length > limit)
1501
+ lines.push({ text: `\u2026 ${count(rows.length - limit, "more session")} \xB7 narrow the range`, tone: "muted" });
1502
+ if (rows.length === 0)
1503
+ lines.push({ text: "no sessions recorded for this range", tone: "muted" });
1504
+ return lines;
1505
+ }
1506
+ function dbLines(data, width) {
1507
+ const lines = [];
1508
+ const size = (bytes) => `${(bytes / 1048576).toFixed(1)} MB`;
1509
+ lines.push({ text: truncate(data.info.path, width), tone: "base" });
1510
+ lines.push({
1511
+ text: `${size(data.info.bytes)}${data.info.walBytes === undefined ? "" : ` + ${size(data.info.walBytes)} wal`} \xB7 schema v${data.info.schemaVersion}`,
1512
+ tone: "muted"
1513
+ });
1514
+ lines.push({ text: "", tone: "muted" });
1515
+ lines.push({ text: "what it holds", tone: HEADER_TONE });
1516
+ for (const table of data.info.tables) {
1517
+ lines.push({ text: ` ${padCell(table.name, 12)}${formatCount(table.rows)} rows`, tone: table.rows === 0 ? "muted" : "base" });
1518
+ }
1519
+ lines.push({
1520
+ text: ` window ${data.info.firstAt === undefined ? "\u2013" : new Date(data.info.firstAt).toLocaleDateString("en-GB")} \u2192 ${data.info.lastAt === undefined ? "\u2013" : new Date(data.info.lastAt).toLocaleDateString("en-GB")}`,
1521
+ tone: "muted"
1522
+ });
1523
+ lines.push({ text: "", tone: "muted" });
1524
+ lines.push({ text: "clean up", tone: HEADER_TONE });
1525
+ lines.push({ text: truncate(" \u2191\u2193 pick a retention \xB7 enter prunes the steps older than it (asks first) \xB7 v gives the space back", width), tone: "muted" });
1526
+ const widths = [10, 13, 10];
1527
+ lines.push({ text: formatTableRow(["keep", "prunes", "older than"], widths, ["left", "right", "left"]), tone: "muted" });
1528
+ for (const option of data.info.removable) {
1529
+ const before = new Date(data.now - option.days * 24 * 60 * 60 * 1000).toLocaleDateString("en-GB");
1530
+ lines.push({
1531
+ text: formatTableRow([`${option.days} days`, `${formatCount(option.steps)} steps`, before], widths, ["left", "right", "left"]),
1532
+ tone: option.steps === 0 ? "muted" : "base",
1533
+ row: { kind: "prune", value: String(option.days) }
1534
+ });
1535
+ }
1536
+ if (data.busy === true) {
1537
+ lines.push({ text: "", tone: "muted" });
1538
+ lines.push({ text: "the recorder is writing: try the vacuum again in a moment", tone: "warning" });
1539
+ }
1540
+ return lines;
1541
+ }
1542
+ function turnStats(turn) {
1543
+ return {
1544
+ endAt: turn.endAt,
1545
+ startAt: turn.promptAt ?? turn.endAt,
1546
+ promptAt: turn.promptAt,
1547
+ promptGapMs: turn.promptGapMs,
1548
+ providerID: turn.providerID,
1549
+ modelID: turn.modelID,
1550
+ variant: turn.variant,
1551
+ providerChanged: turn.providerChanged,
1552
+ modelChanged: turn.modelChanged,
1553
+ totals: turn.totals,
1554
+ latest: turn.latest,
1555
+ session: undefined,
1556
+ drops: turn.drops.map((drop) => ({ messageID: drop.messageID, tokens: drop.tokens, prevCached: 0, gapMs: drop.gapMs })),
1557
+ cacheDropTokens: turn.drops.reduce((sum, drop) => sum + drop.tokens, 0),
1558
+ cacheExpired: turn.drops.length > 0,
1559
+ steps: turn.steps
1560
+ };
1561
+ }
1562
+ function turnsLines(rows, width, options) {
1563
+ const lines = [];
1564
+ const warning = rows.filter((turn) => turn.drops.length > 0).length;
1565
+ lines.push({
1566
+ text: `${count(rows.length, "turn")} \xB7 newest first${warning > 0 ? ` \xB7 ${count(warning, "turn")} with a cache loss` : ""}`,
1567
+ tone: "muted"
1568
+ });
1569
+ const showSession = options?.sessionTitles !== undefined;
1570
+ for (const turn of rows) {
1571
+ const line = formatLine(turnStats(turn), {
1572
+ ...options,
1573
+ fields: { ...options?.fields, session: false }
1574
+ });
1575
+ const title = showSession ? `${truncate(options.sessionTitles.get(turn.sessionID) ?? turn.sessionID, 18)} ` : "";
1576
+ lines.push({
1577
+ text: truncate(`${title}${line}`, width),
1578
+ tone: turn.drops.length > 0 || turn.providerChanged ? "warning" : "base",
1579
+ row: { kind: "turn", value: turn.sessionID }
1580
+ });
1581
+ }
1582
+ if (rows.length === 0)
1583
+ lines.push({ text: "no finished turns for this range", tone: "muted" });
1584
+ return lines;
1585
+ }
1586
+ function sessionSummaryLines(usage, budget = 34) {
1587
+ const buckets = usageBuckets(usage);
1588
+ const steps = formatCount(buckets.steps);
1589
+ const input = formatTokens(buckets.input);
1590
+ const cached = formatTokens(buckets.cached);
1591
+ const out = formatTokens(buckets.output);
1592
+ const hit = formatPercent(buckets.hit);
1593
+ const tps = formatTpsCompact(usage.tps);
1594
+ const cost = formatCost(buckets.cost);
1595
+ const first = [
1596
+ ` ${steps} steps \xB7 in ${input}`,
1597
+ ` ${steps} steps`
1598
+ ].find((text) => text.length <= budget);
1599
+ const second = [
1600
+ ` cached ${cached} \xB7 out ${out}`,
1601
+ ` cached ${cached}`
1602
+ ].find((text) => text.length <= budget);
1603
+ const last = [
1604
+ ` hit ${hit} \xB7 ${tps} \xB7 ${cost}`,
1605
+ ` hit ${hit} \xB7 ${cost}`,
1606
+ ` ${cost}`
1607
+ ].find((text) => text.length <= budget);
1608
+ return [
1609
+ { text: truncate("session", budget), tone: HEADER_TONE },
1610
+ { text: truncate(first ?? ` ${steps} steps`, budget), tone: "muted" },
1611
+ { text: truncate(second ?? ` cached ${cached}`, budget), tone: "base" },
1612
+ { text: truncate(last ?? ` ${cost}`, budget), tone: "base" },
1613
+ { text: "", tone: "muted" }
1614
+ ];
1615
+ }
1616
+ function sidebarLines(rows, options) {
1617
+ const budget = options?.budget ?? 34;
1618
+ const labels = { input: options?.labels?.input ?? "in", output: options?.labels?.output ?? "out" };
1619
+ const lines = [];
1620
+ for (const [index, turn] of rows.entries()) {
1621
+ const clock = turn.endAt !== undefined ? formatClock(turn.endAt).slice(0, 5) : "--:--";
1622
+ const provider = turn.providerID ?? "unknown";
1623
+ const display = displayTotals(turn.latest);
1624
+ const input = formatTokens(display.input);
1625
+ const prompt = formatTokens(display.prompt);
1626
+ const output = formatTokens(display.output);
1627
+ const cost = formatCost(turn.totals.cost);
1628
+ const hit = formatPercent(display.prompt > 0 ? display.cached / display.prompt : undefined).replace(/\.0%$/, "%");
1629
+ const tps = turn.tps === undefined ? undefined : formatTpsCompact(turn.tps);
1630
+ const suffix = turn.drops.length > 0 ? " \xB7 \u26A0" : "";
1631
+ const volumes = [
1632
+ ` ${labels.input} ${input} (${prompt}) \xB7 ${labels.output} ${output}`,
1633
+ ` ${labels.input} ${input} \xB7 ${labels.output} ${output}`,
1634
+ ` ${labels.input} ${input}`
1635
+ ];
1636
+ const facts = [
1637
+ ` hit ${hit}${tps === undefined ? "" : ` \xB7 ${tps}`} \xB7 ${cost}${suffix}`,
1638
+ ` hit ${hit}${tps === undefined ? "" : ` \xB7 ${tps}`}${suffix}`,
1639
+ ` ${tps === undefined ? "" : `${tps} \xB7 `}${cost}${suffix}`,
1640
+ ` hit ${hit}${suffix}`
1641
+ ];
1642
+ lines.push({ text: truncate(`${clock} ${provider}`, budget), tone: turn.providerChanged ? "warning" : "base" });
1643
+ const edge = index === 0 || index === rows.length - 1;
1644
+ if (edge || turn.modelChanged || turn.providerChanged) {
1645
+ const model = turn.modelID === undefined ? "unknown" : `${turn.modelID}${turn.variant === undefined ? "" : ` ${turn.variant}`}`;
1646
+ lines.push({ text: truncate(` ${model}`, budget), tone: "muted" });
1647
+ }
1648
+ lines.push({
1649
+ text: truncate(volumes.find((text) => text.length <= budget) ?? volumes[volumes.length - 1], budget),
1650
+ tone: "muted"
1651
+ });
1652
+ lines.push({
1653
+ text: truncate(facts.find((text) => text.length <= budget) ?? facts[facts.length - 1], budget),
1654
+ tone: turn.drops.length > 0 ? "warning" : "muted"
1655
+ });
1656
+ }
1657
+ if (rows.length === 0)
1658
+ lines.push({ text: "no finished turns yet", tone: "muted" });
1659
+ return lines;
1660
+ }
1661
+
1662
+ // src/panel.tsx
1663
+ import { effect as _$effect } from "@opentui/solid";
1664
+ import { insertNode as _$insertNode } from "@opentui/solid";
1665
+ import { insert as _$insert } from "@opentui/solid";
1666
+ import { createComponent as _$createComponent } from "@opentui/solid";
1667
+ import { use as _$use } from "@opentui/solid";
1668
+ import { setProp as _$setProp } from "@opentui/solid";
1669
+ import { createElement as _$createElement } from "@opentui/solid";
1670
+ import { TextAttributes } from "@opentui/core";
1671
+ import { For, createMemo, createRenderEffect, createSignal } from "solid-js";
1672
+
1673
+ // src/stats/aggregates.ts
1674
+ var STEP_AT = "COALESCE(created_at, recorded_at)";
1675
+ function normalize(rows, optional) {
1676
+ for (const row of rows) {
1677
+ for (const key of optional)
1678
+ if (row[key] === null)
1679
+ row[key] = undefined;
1680
+ }
1681
+ return rows;
1682
+ }
1683
+ function groupBy(items, key) {
1684
+ const groups = new Map;
1685
+ for (const item of items) {
1686
+ const group = key(item);
1687
+ const list = groups.get(group);
1688
+ if (list !== undefined)
1689
+ list.push(item);
1690
+ else
1691
+ groups.set(group, [item]);
1692
+ }
1693
+ return groups;
1694
+ }
1695
+ var STEP_COLUMNS = `message_id AS messageID, session_id AS sessionID, provider_id AS providerID,
1696
+ model_id AS modelID, variant,
1697
+ COALESCE(input_tokens, 0) AS input, COALESCE(cache_read, 0) AS cached, COALESCE(cache_write, 0) AS cacheWrite,
1698
+ COALESCE(output_tokens, 0) AS output, COALESCE(reasoning_tokens, 0) AS reasoning, COALESCE(cost, 0) AS cost,
1699
+ created_at AS createdAt, streamed_at AS streamedAt, completed_at AS completedAt, ${STEP_AT} AS at`;
1700
+ var STEP_OPTIONAL = ["providerID", "modelID", "variant", "createdAt", "streamedAt", "completedAt"];
1701
+ var PAIR_COLUMNS = STEP_COLUMNS.replace("COALESCE(output_tokens, 0) AS output, COALESCE(reasoning_tokens, 0) AS reasoning, ", "");
1702
+ function stepRows(db, options) {
1703
+ const filter = buildWhere(options?.range, options?.filters);
1704
+ const rows = db.query(`SELECT ${STEP_COLUMNS} FROM steps ${filter.where}`).all(...filter.params);
1705
+ return normalize(rows, STEP_OPTIONAL);
1706
+ }
1707
+ function cachePairs(db, options) {
1708
+ const filter = buildWhere(options?.range, options?.filters);
1709
+ const rows = normalize(db.query(`SELECT ${PAIR_COLUMNS} FROM steps ${filter.where} ORDER BY session_id, ${STEP_AT}, message_id`).all(...filter.params), STEP_OPTIONAL);
1710
+ const from = options?.range?.from;
1711
+ if (from === undefined || rows.length === 0)
1712
+ return rows;
1713
+ const seek = db.query(`SELECT ${PAIR_COLUMNS} FROM steps
1714
+ WHERE session_id = ? AND COALESCE(created_at, recorded_at) < ?
1715
+ ORDER BY COALESCE(created_at, recorded_at) DESC, message_id DESC LIMIT 1`);
1716
+ for (const sessionID of new Set(rows.map((row) => row.sessionID))) {
1717
+ const boundary = seek.get(sessionID, from);
1718
+ if (boundary === null)
1719
+ continue;
1720
+ const step = normalize([boundary], STEP_OPTIONAL)[0];
1721
+ step.boundary = true;
1722
+ rows.push(step);
1723
+ }
1724
+ rows.sort((left, right) => {
1725
+ if (left.sessionID !== right.sessionID)
1726
+ return left.sessionID < right.sessionID ? -1 : 1;
1727
+ if (left.at !== right.at)
1728
+ return left.at - right.at;
1729
+ return left.messageID < right.messageID ? -1 : left.messageID > right.messageID ? 1 : 0;
1730
+ });
1731
+ return rows;
1732
+ }
1733
+ function message(step) {
1734
+ return {
1735
+ type: "assistant",
1736
+ id: step.messageID,
1737
+ ...step.modelID === undefined ? {} : { model: { providerID: step.providerID ?? "", id: step.modelID, ...step.variant === undefined ? {} : { variant: step.variant } } },
1738
+ tokens: { cache: { read: step.cached } },
1739
+ time: {
1740
+ ...step.createdAt === undefined ? {} : { created: step.createdAt },
1741
+ ...step.streamedAt === undefined ? {} : { streamed: step.streamedAt },
1742
+ ...step.completedAt === undefined ? {} : { completed: step.completedAt }
1743
+ }
1744
+ };
1745
+ }
1746
+ function pairsOf(rows) {
1747
+ const pairs = [];
1748
+ for (let index = 1;index < rows.length; index += 1) {
1749
+ const previous = rows[index - 1];
1750
+ const current = rows[index];
1751
+ if (previous.sessionID !== current.sessionID)
1752
+ continue;
1753
+ pairs.push({ previous, current });
1754
+ }
1755
+ return pairs;
1756
+ }
1757
+ function usageOf(steps) {
1758
+ const usage = { steps: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, cost: 0 };
1759
+ for (const step of steps) {
1760
+ usage.steps += 1;
1761
+ usage.input += step.input;
1762
+ usage.cached += step.cached;
1763
+ usage.cacheWrite += step.cacheWrite;
1764
+ usage.output += step.output;
1765
+ usage.reasoning += step.reasoning;
1766
+ usage.cost += step.cost;
1767
+ }
1768
+ return usage;
1769
+ }
1770
+ function coverageOf(db, options) {
1771
+ const filter = buildWhere(options?.range, options?.filters);
1772
+ const row = db.query(`SELECT COUNT(*) AS steps,
1773
+ COUNT(DISTINCT CASE WHEN model_id IS NOT NULL THEN model_id || '#' || COALESCE(variant, '') END) AS models,
1774
+ COUNT(DISTINCT provider_id) AS providers,
1775
+ COALESCE(SUM(CASE WHEN model_id IS NULL THEN 1 ELSE 0 END), 0) AS missingModel,
1776
+ COALESCE(SUM(CASE WHEN completed_at IS NOT NULL OR finish IS NOT NULL THEN 0 ELSE 1 END), 0) AS unsettled,
1777
+ COALESCE(SUM(CASE WHEN (output_tokens + reasoning_tokens) > 0 AND created_at IS NOT NULL AND streamed_at IS NOT NULL AND streamed_at > created_at THEN 0 ELSE 1 END), 0) AS missingTps,
1778
+ MIN(${STEP_AT}) AS firstAt,
1779
+ MAX(${STEP_AT}) AS lastAt
1780
+ FROM steps ${filter.where}`).get(...filter.params);
1781
+ return {
1782
+ steps: row.steps,
1783
+ models: row.models,
1784
+ providers: row.providers,
1785
+ missingModel: row.missingModel,
1786
+ unsettled: row.unsettled,
1787
+ missingTps: row.missingTps,
1788
+ firstAt: row.firstAt ?? undefined,
1789
+ lastAt: row.lastAt ?? undefined
1790
+ };
1791
+ }
1792
+ function groupedRows(db, options) {
1793
+ const filter = buildWhere(options?.range, options?.filters);
1794
+ const rows = db.query(`SELECT ${STEP_COLUMNS} FROM steps ${filter.where}`).all(...filter.params);
1795
+ return normalize(rows, STEP_OPTIONAL);
1796
+ }
1797
+ function timingOf(step) {
1798
+ const wall = step.createdAt !== undefined && step.completedAt !== undefined ? step.completedAt - step.createdAt : undefined;
1799
+ const stream = step.createdAt !== undefined && step.streamedAt !== undefined && step.streamedAt > step.createdAt ? step.streamedAt - step.createdAt : undefined;
1800
+ return { wall, stream, tps: tokensPerSecond(step.output + step.reasoning, step.createdAt, step.streamedAt) };
1801
+ }
1802
+ function timingRows(db, options) {
1803
+ return groupedRows(db, options);
1804
+ }
1805
+ function latencyOf(rows) {
1806
+ const wall = [];
1807
+ const stream = [];
1808
+ const tps = [];
1809
+ for (const step of rows) {
1810
+ const timing = timingOf(step);
1811
+ if (timing.wall !== undefined)
1812
+ wall.push(timing.wall);
1813
+ if (timing.stream !== undefined)
1814
+ stream.push(timing.stream);
1815
+ if (timing.tps !== undefined)
1816
+ tps.push(timing.tps);
1817
+ }
1818
+ return { wall: latencyStats(wall), stream: latencyStats(stream), tps: latencyStats(tps) };
1819
+ }
1820
+ function latencyGroupsOf(rows, by) {
1821
+ const key = (step) => by === "model" ? modelKey(step.modelID ?? "(unknown)", step.variant) : step.providerID ?? "(unknown)";
1822
+ const groups = [];
1823
+ for (const [group, steps] of groupBy(rows, key)) {
1824
+ const wall = [];
1825
+ const stream = [];
1826
+ const tps = [];
1827
+ for (const step of steps) {
1828
+ const timing = timingOf(step);
1829
+ if (timing.wall !== undefined)
1830
+ wall.push(timing.wall);
1831
+ if (timing.stream !== undefined)
1832
+ stream.push(timing.stream);
1833
+ if (timing.tps !== undefined)
1834
+ tps.push(timing.tps);
1835
+ }
1836
+ const [modelID, variant] = group.split("#");
1837
+ groups.push({
1838
+ key: group,
1839
+ label: by === "model" ? variant === undefined || variant === "" ? modelID : `${modelID} ${variant}` : group,
1840
+ count: steps.length,
1841
+ usage: usageOf(steps),
1842
+ wall: latencyStats(wall),
1843
+ stream: latencyStats(stream),
1844
+ tps: latencyStats(tps)
1845
+ });
1846
+ }
1847
+ return groups.sort((left, right) => right.count - left.count || left.key.localeCompare(right.key));
1848
+ }
1849
+ var EMPTY_CACHE = { steps: 0, input: 0, cached: 0, cacheWrite: 0, prompt: 0, hit: undefined, hitP50: undefined };
1850
+ function cacheStatsOfSteps(steps) {
1851
+ let input = 0;
1852
+ let cached = 0;
1853
+ let cacheWrite = 0;
1854
+ const ratios = [];
1855
+ for (const step of steps) {
1856
+ input += step.input;
1857
+ cached += step.cached;
1858
+ cacheWrite += step.cacheWrite;
1859
+ const prompt = step.input + step.cached + step.cacheWrite;
1860
+ if (prompt > 0)
1861
+ ratios.push(step.cached / prompt);
1862
+ }
1863
+ const prompt = input + cached + cacheWrite;
1864
+ return { steps: steps.length, input, cached, cacheWrite, prompt, hit: prompt > 0 ? cached / prompt : undefined, hitP50: percentile(ratios, 50) };
1865
+ }
1866
+ function cacheStatsOf(db, options) {
1867
+ const rows = stepRows(db, options);
1868
+ return rows.length === 0 ? EMPTY_CACHE : cacheStatsOfSteps(rows);
1869
+ }
1870
+ function cacheStatsByModel(rows) {
1871
+ const out = [];
1872
+ const inRange = rows.filter((row) => row.boundary !== true);
1873
+ for (const [, group] of groupBy(inRange, (row) => modelKey(row.modelID ?? "(unknown)", row.variant))) {
1874
+ out.push({ modelID: group[0].modelID ?? "(unknown)", variant: group[0].variant, cache: cacheStatsOfSteps(group) });
1875
+ }
1876
+ return out.sort((left, right) => right.cache.cached - left.cache.cached || left.modelID.localeCompare(right.modelID));
1877
+ }
1878
+ function switchesOf(rows) {
1879
+ const perModel = new Map;
1880
+ const totals = { pairs: 0, changes: 0, cacheLost: 0, cacheLostEvents: 0 };
1881
+ for (const { previous, current } of pairsOf(rows)) {
1882
+ if (previous.modelID === undefined || current.modelID === undefined)
1883
+ continue;
1884
+ const key = modelKey(current.modelID, current.variant);
1885
+ const row = perModel.get(key) ?? {
1886
+ modelID: current.modelID,
1887
+ variant: current.variant,
1888
+ steps: 0,
1889
+ pairs: 0,
1890
+ changes: 0,
1891
+ modelChanges: 0,
1892
+ providerChanges: 0,
1893
+ cacheLost: 0,
1894
+ cacheLostEvents: 0
1895
+ };
1896
+ row.steps += 1;
1897
+ row.pairs += 1;
1898
+ totals.pairs += 1;
1899
+ const modelChanged = key !== modelKey(previous.modelID, previous.variant);
1900
+ const providerChanged = previous.providerID !== undefined && current.providerID !== undefined && previous.providerID !== current.providerID;
1901
+ if (modelChanged)
1902
+ row.modelChanges += 1;
1903
+ if (providerChanged)
1904
+ row.providerChanges += 1;
1905
+ if (modelChanged || providerChanged) {
1906
+ row.changes += 1;
1907
+ totals.changes += 1;
1908
+ const lost = cacheDrop(message(previous), message(current), "model");
1909
+ if (lost !== undefined) {
1910
+ row.cacheLost += lost;
1911
+ row.cacheLostEvents += 1;
1912
+ totals.cacheLost += lost;
1913
+ totals.cacheLostEvents += 1;
1914
+ }
1915
+ }
1916
+ perModel.set(key, row);
1917
+ }
1918
+ return {
1919
+ rows: [...perModel.values()].sort((left, right) => right.changes - left.changes || right.steps - left.steps || left.modelID.localeCompare(right.modelID)),
1920
+ totals
1921
+ };
1922
+ }
1923
+ var GAP_BUCKETS = [
1924
+ ["<1m", 0, 60000],
1925
+ ["1\u20135m", 60000, 5 * 60000],
1926
+ ["5\u201315m", 5 * 60000, 15 * 60000],
1927
+ ["15\u201360m", 15 * 60000, 60 * 60000],
1928
+ ["1\u20136h", 60 * 60000, 6 * 60 * 60000],
1929
+ [">6h", 6 * 60 * 60000, undefined]
1930
+ ];
1931
+ function bucketFor(gapMs) {
1932
+ const index = GAP_BUCKETS.findIndex(([, from, to]) => gapMs >= from && (to === undefined || gapMs < to));
1933
+ return index === -1 ? GAP_BUCKETS.length - 1 : index;
1934
+ }
1935
+ function gapReportOf(rows) {
1936
+ const buckets = GAP_BUCKETS.map(([label]) => ({ label, samples: 0, retained: 0, drops: 0, lostTokens: 0, retainedRatio: undefined }));
1937
+ const providers = new Map;
1938
+ let samples = 0;
1939
+ for (const { previous, current } of pairsOf(rows)) {
1940
+ if (previous.modelID === undefined || current.modelID === undefined)
1941
+ continue;
1942
+ if (modelKey(previous.modelID, previous.variant) !== modelKey(current.modelID, current.variant))
1943
+ continue;
1944
+ if ((previous.providerID ?? "") !== (current.providerID ?? ""))
1945
+ continue;
1946
+ const gap = stepGap(message(previous), message(current));
1947
+ if (gap === undefined)
1948
+ continue;
1949
+ const lost = cacheDrop(message(previous), message(current));
1950
+ const bucket = buckets[bucketFor(gap)];
1951
+ bucket.samples += 1;
1952
+ samples += 1;
1953
+ if (lost === undefined)
1954
+ bucket.retained += 1;
1955
+ else {
1956
+ bucket.drops += 1;
1957
+ bucket.lostTokens += lost;
1958
+ }
1959
+ const provider = current.providerID ?? previous.providerID ?? "(unknown)";
1960
+ const row = providers.get(provider) ?? { provider, samples: 0, retained: 0, drops: 0, lostTokens: 0, retainedRatio: undefined };
1961
+ row.samples += 1;
1962
+ if (lost === undefined)
1963
+ row.retained += 1;
1964
+ else {
1965
+ row.drops += 1;
1966
+ row.lostTokens += lost;
1967
+ }
1968
+ providers.set(provider, row);
1969
+ }
1970
+ for (const bucket of buckets)
1971
+ bucket.retainedRatio = bucket.samples > 0 ? bucket.retained / bucket.samples : undefined;
1972
+ for (const row of providers.values())
1973
+ row.retainedRatio = row.samples > 0 ? row.retained / row.samples : undefined;
1974
+ return {
1975
+ buckets,
1976
+ providers: [...providers.values()].sort((left, right) => right.samples - left.samples || left.provider.localeCompare(right.provider)),
1977
+ samples
1978
+ };
1979
+ }
1980
+ function ttlEvidenceOf(rows) {
1981
+ const evidence = new Map;
1982
+ for (const { previous, current } of pairsOf(rows)) {
1983
+ if (previous.modelID === undefined || current.modelID === undefined)
1984
+ continue;
1985
+ const key = `${current.providerID ?? ""}/${current.modelID}`;
1986
+ const before = `${previous.providerID ?? ""}/${previous.modelID}#${previous.variant ?? ""}`;
1987
+ const after = `${current.providerID ?? ""}/${current.modelID}#${current.variant ?? ""}`;
1988
+ if (before !== after)
1989
+ continue;
1990
+ const row = evidence.get(key) ?? {
1991
+ providerID: current.providerID ?? "(unknown)",
1992
+ modelID: current.modelID,
1993
+ samples: 0,
1994
+ maxHitGapMs: undefined,
1995
+ minDropGapMs: undefined
1996
+ };
1997
+ row.samples += 1;
1998
+ const gapMs = stepGap(message(previous), message(current));
1999
+ if (gapMs !== undefined) {
2000
+ const lost = cacheDrop(message(previous), message(current));
2001
+ if (lost === undefined)
2002
+ row.maxHitGapMs = row.maxHitGapMs === undefined ? gapMs : Math.max(row.maxHitGapMs, gapMs);
2003
+ else
2004
+ row.minDropGapMs = row.minDropGapMs === undefined ? gapMs : Math.min(row.minDropGapMs, gapMs);
2005
+ }
2006
+ evidence.set(key, row);
2007
+ }
2008
+ return [...evidence.values()].sort((left, right) => right.samples - left.samples || `${left.providerID}/${left.modelID}`.localeCompare(`${right.providerID}/${right.modelID}`));
2009
+ }
2010
+ function cacheDropsOf(db, rows, options) {
2011
+ const ttlHint = options?.ttlHintMs ?? DEFAULT_TTL_HINT_MS;
2012
+ const losses = [];
2013
+ for (const { previous, current } of pairsOf(rows)) {
2014
+ const tokens = cacheDrop(message(previous), message(current), "model");
2015
+ if (tokens === undefined)
2016
+ continue;
2017
+ losses.push({ previous, current, tokens, gapMs: stepGap(message(previous), message(current)) });
2018
+ }
2019
+ if (losses.length === 0)
2020
+ return [];
2021
+ const compactions = new Map;
2022
+ const sessionIDs = [...new Set(losses.map((loss) => loss.current.sessionID))];
2023
+ const events = db.query(`SELECT session_id, at FROM events
2024
+ WHERE kind = 'compaction' AND at IS NOT NULL AND session_id IN (${sessionIDs.map(() => "?").join(", ")})
2025
+ ORDER BY at`).all(...sessionIDs);
2026
+ for (const event of events) {
2027
+ const list = compactions.get(event.session_id);
2028
+ if (list !== undefined)
2029
+ list.push(event.at);
2030
+ else
2031
+ compactions.set(event.session_id, [event.at]);
2032
+ }
2033
+ return losses.map((loss) => {
2034
+ const ended = loss.previous.completedAt ?? loss.previous.streamedAt ?? loss.previous.createdAt;
2035
+ const compacted = (compactions.get(loss.current.sessionID) ?? []).some((time) => ended !== undefined && time >= ended && time <= loss.current.at);
2036
+ const reason = compacted ? "compaction" : loss.gapMs !== undefined && loss.gapMs >= ttlHint ? "expired" : "unknown";
2037
+ return {
2038
+ sessionID: loss.current.sessionID,
2039
+ messageID: loss.current.messageID,
2040
+ providerID: loss.current.providerID,
2041
+ modelID: loss.current.modelID,
2042
+ variant: loss.current.variant,
2043
+ at: loss.current.at,
2044
+ tokens: loss.tokens,
2045
+ gapMs: loss.gapMs,
2046
+ prevCached: loss.previous.cached,
2047
+ reason
2048
+ };
2049
+ }).sort((left, right) => left.at - right.at);
2050
+ }
2051
+ function groupValues(steps) {
2052
+ const wall = [];
2053
+ const tps = [];
2054
+ for (const step of steps) {
2055
+ const timing = timingOf(step);
2056
+ if (timing.wall !== undefined)
2057
+ wall.push(timing.wall);
2058
+ if (timing.tps !== undefined)
2059
+ tps.push(timing.tps);
2060
+ }
2061
+ return { cache: cacheStatsOfSteps(steps), tps: latencyStats(tps).p50, wall: latencyStats(wall).p50 };
2062
+ }
2063
+ function providerOf(steps, models) {
2064
+ const values = groupValues(steps);
2065
+ const times = steps.map((step) => step.at);
2066
+ return {
2067
+ provider: steps[0].providerID,
2068
+ models: models.split("\x00"),
2069
+ share: 0,
2070
+ usage: usageOf(steps),
2071
+ cache: values.cache,
2072
+ tps: values.tps,
2073
+ wall: values.wall,
2074
+ firstAt: times.length > 0 ? Math.min(...times) : undefined,
2075
+ lastAt: times.length > 0 ? Math.max(...times) : undefined
2076
+ };
2077
+ }
2078
+ var modelNames = (steps) => [...new Set(steps.map((step) => modelKey(step.modelID ?? "(unknown)", step.variant)))].map((key) => modelLabel(key.slice(0, key.indexOf("#")), key.slice(key.indexOf("#") + 1) || undefined)).join("\x00");
2079
+ function providerSummaryOf(rows) {
2080
+ const out = [];
2081
+ for (const steps of groupBy(rows, (step) => step.providerID ?? "").values()) {
2082
+ const summary = providerOf(steps, modelNames(steps));
2083
+ out.push({ ...summary, share: rows.length === 0 ? 0 : steps.length / rows.length });
2084
+ }
2085
+ return out.sort((left, right) => right.usage.steps - left.usage.steps || (left.provider ?? "").localeCompare(right.provider ?? ""));
2086
+ }
2087
+ function providerBreakdownOf(rows) {
2088
+ const out = [];
2089
+ for (const [model, modelRows] of groupBy(rows, (step) => modelKey(step.modelID ?? "(unknown)", step.variant))) {
2090
+ for (const steps of groupBy(modelRows, (step) => step.providerID ?? "").values()) {
2091
+ const [modelID, variant] = [model.slice(0, model.indexOf("#")), model.slice(model.indexOf("#") + 1)];
2092
+ out.push({
2093
+ ...providerOf(steps, modelNames(steps)),
2094
+ modelID,
2095
+ variant: variant === "" ? undefined : variant,
2096
+ share: steps.length / modelRows.length
2097
+ });
2098
+ }
2099
+ }
2100
+ return out.sort((left, right) => right.usage.steps - left.usage.steps || right.cache.cached - left.cache.cached);
2101
+ }
2102
+ function isoWeek(date) {
2103
+ const monday = new Date(date.getFullYear(), date.getMonth(), date.getDate());
2104
+ monday.setDate(monday.getDate() - (monday.getDay() + 6) % 7);
2105
+ const thursday = new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + 3);
2106
+ const firstThursday = new Date(thursday.getFullYear(), 0, 4);
2107
+ firstThursday.setDate(firstThursday.getDate() - (firstThursday.getDay() + 6) % 7 + 3);
2108
+ const week = 1 + Math.round((thursday.getTime() - firstThursday.getTime()) / (7 * 24 * 60 * 60 * 1000));
2109
+ return { year: thursday.getFullYear(), week, monday };
2110
+ }
2111
+ function periodStart(at, granularity) {
2112
+ const date = new Date(at);
2113
+ const pad = (value) => String(value).padStart(2, "0");
2114
+ if (granularity === "month") {
2115
+ const start = new Date(date.getFullYear(), date.getMonth(), 1);
2116
+ return { key: `${start.getFullYear()}-${pad(start.getMonth() + 1)}`, label: `${start.getFullYear()}-${pad(start.getMonth() + 1)}`, start: start.getTime() };
2117
+ }
2118
+ if (granularity === "week") {
2119
+ const { year, week, monday } = isoWeek(date);
2120
+ return { key: `${year}-W${pad(week)}`, label: `${year}-W${pad(week)}`, start: monday.getTime() };
2121
+ }
2122
+ const start = new Date(date.getFullYear(), date.getMonth(), date.getDate());
2123
+ const key = `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}`;
2124
+ return { key, label: key, start: start.getTime() };
2125
+ }
2126
+ function periodsOf(rows, granularity) {
2127
+ const out = [];
2128
+ for (const [key, steps] of groupBy(rows, (step) => periodStart(step.at, granularity).key)) {
2129
+ const period = periodStart(steps[0].at, granularity);
2130
+ out.push({
2131
+ key,
2132
+ label: period.label,
2133
+ at: period.start,
2134
+ steps: steps.length,
2135
+ sessions: new Set(steps.map((step) => step.sessionID)).size,
2136
+ usage: usageOf(steps),
2137
+ cache: cacheStatsOfSteps(steps)
2138
+ });
2139
+ }
2140
+ return out.sort((left, right) => left.at - right.at);
2141
+ }
2142
+
2143
+ // src/panel-build.ts
2144
+ var SORT_KEYS = {
2145
+ models: ["steps", "cached", "hit", "out", "cost"],
2146
+ providers: ["steps", "cached", "hit", "cost", "share", "tps"],
2147
+ latency: ["n", "tps", "wall"],
2148
+ sessions: ["last", "steps", "cached", "cost"],
2149
+ turns: ["time", "cost", "tokens"]
2150
+ };
2151
+ function sortKeys(view) {
2152
+ return SORT_KEYS[view] ?? [];
2153
+ }
2154
+ function defaultSort(view) {
2155
+ return sortKeys(view)[0] ?? "";
2156
+ }
2157
+ function nextSort(view, current, step) {
2158
+ const keys = sortKeys(view);
2159
+ if (keys.length === 0)
2160
+ return current;
2161
+ const index = keys.indexOf(current);
2162
+ if (index === -1)
2163
+ return keys[0];
2164
+ return keys[(index + step + keys.length) % keys.length];
2165
+ }
2166
+ function detailLabel(row) {
2167
+ switch (row.kind) {
2168
+ case "model":
2169
+ return `model ${row.variant !== undefined ? `${row.value} ${row.variant}` : row.value}`;
2170
+ case "session":
2171
+ case "turn":
2172
+ return `session ${row.value}`;
2173
+ case "period":
2174
+ return `period ${row.value}`;
2175
+ case "provider":
2176
+ return `provider ${row.value}`;
2177
+ case "prune":
2178
+ return `prune older than ${row.value} days`;
2179
+ }
2180
+ }
2181
+ function buildPanel(request) {
2182
+ try {
2183
+ return build(request);
2184
+ } catch (error) {
2185
+ const message = error instanceof Error ? error.message : String(error);
2186
+ return [{ text: `stats query failed: ${message}`, tone: "warning" }];
2187
+ }
2188
+ }
2189
+ function build(request) {
2190
+ const { db, view, now, sessionStart, width } = request;
2191
+ const scoped = request.range === "session" && request.sessionID !== "";
2192
+ const range = scoped ? undefined : request.range === "session" ? sessionStart === undefined ? undefined : { from: sessionStart } : rangeForRequest(request.range, now);
2193
+ const filters = scoped ? { ...request.filters, sessionID: request.sessionID } : request.filters;
2194
+ if (request.detail !== undefined)
2195
+ return buildDetail(request, range, filters);
2196
+ switch (view) {
2197
+ case "overview": {
2198
+ const usage = overview(db, range, filters);
2199
+ const sessionUsage = request.sessionID === "" ? undefined : overview(db, undefined, { ...request.filters, sessionID: request.sessionID });
2200
+ const childIDs = request.sessionID === "" ? [] : childSessionIDs(db, request.sessionID);
2201
+ const children = childIDs.length === 0 ? [] : sessionUsageOf(db, childIDs);
2202
+ return overviewLines({
2203
+ usage,
2204
+ session: sessionUsage,
2205
+ ...children.length > 0 ? { subagents: children } : {},
2206
+ range: request.range,
2207
+ activity: activityOf(db, range, now),
2208
+ coverage: coverageOf(db, { range, filters })
2209
+ }, width);
2210
+ }
2211
+ case "models": {
2212
+ const rows = sortModels(models(db, range, filters), request.sort, request.sortDesc);
2213
+ const pairs = cachePairs(db, { range, filters });
2214
+ const switches = switchesOf(pairs);
2215
+ return modelsLines(rows, width, {
2216
+ cache: cacheByModel(pairs),
2217
+ switches: switchesByModel(switches.rows)
2218
+ }, 40);
2219
+ }
2220
+ case "providers":
2221
+ return providerSummaryLines(sortProviders(providerSummaryOf(stepRows(db, { range, filters })), request.sort, request.sortDesc), width, 40);
2222
+ case "cache": {
2223
+ const pairs = cachePairs(db, { range, filters });
2224
+ return cacheLines({
2225
+ drops: cacheDropsOf(db, pairs),
2226
+ evidence: ttlEvidenceOf(pairs),
2227
+ gap: gapReportOf(pairs),
2228
+ models: topCacheModels(pairs)
2229
+ }, width);
2230
+ }
2231
+ case "periods":
2232
+ return periodLines(periodsOf(stepRows(db, { range, filters }), request.granularity), width, request.granularity, 40);
2233
+ case "latency": {
2234
+ const timings = timingRows(db, { range, filters });
2235
+ const by = filters.modelID !== undefined ? "provider" : "model";
2236
+ return latencyLines(latencyOf(timings), width, sortLatency(latencyGroupsOf(timings, by), request.sort, request.sortDesc), 14);
2237
+ }
2238
+ case "sessions": {
2239
+ const rows = sortSessions(sessions(db, { range, filters, limit: 200 }), request.sort, request.sortDesc);
2240
+ const child = rows.filter((row) => row.parentID !== undefined && rows.some((parent) => parent.sessionID === row.parentID));
2241
+ return sessionsLines(rows, width, {
2242
+ limit: 20,
2243
+ rollup: { subagentSteps: child.reduce((sum, row) => sum + row.steps, 0), subagentCost: child.reduce((sum, row) => sum + row.cost, 0) }
2244
+ });
2245
+ }
2246
+ case "db":
2247
+ return request.database === undefined ? [{ text: "the database location is unavailable to this panel", tone: "warning" }] : dbLines({ info: request.database, busy: request.busy, now }, width);
2248
+ case "turns": {
2249
+ const sessionID = request.turnsScope === "session" ? request.sessionID : undefined;
2250
+ const rows = sortTurns(turns(db, { sessionID, range, filters, limit: 300 }), request.sort, request.sortDesc);
2251
+ const titles = request.turnsScope === "all" ? new Map(sessions(db, { range, filters, limit: 200 }).map((row) => [row.sessionID, row.title ?? row.sessionID])) : undefined;
2252
+ return turnsLines(rows.slice(0, 60), width, { ...request.format, sessionTitles: titles });
2253
+ }
2254
+ }
2255
+ }
2256
+ function rangeForRequest(key, now) {
2257
+ switch (key) {
2258
+ case "today": {
2259
+ const start = new Date(now);
2260
+ start.setHours(0, 0, 0, 0);
2261
+ return { from: start.getTime() };
2262
+ }
2263
+ case "7d":
2264
+ return { from: now - 7 * 24 * 60 * 60 * 1000 };
2265
+ case "30d":
2266
+ return { from: now - 30 * 24 * 60 * 60 * 1000 };
2267
+ default:
2268
+ return;
2269
+ }
2270
+ }
2271
+ function buildDetail(request, range, filters) {
2272
+ const row = request.detail;
2273
+ const db = request.db;
2274
+ const width = request.width;
2275
+ const head = { text: detailLabel(row), tone: "accent" };
2276
+ const back = { text: "backspace returns \xB7 the header shows where you came from", tone: "muted" };
2277
+ if (row.kind === "model") {
2278
+ const scoped = { ...filters, modelKey: modelKey(row.value, row.variant) };
2279
+ const stats = cacheStatsOf(db, { range, filters: scoped });
2280
+ const steps = stepRows(db, { range, filters: scoped });
2281
+ return [
2282
+ head,
2283
+ {
2284
+ text: `steps ${stats.steps} \xB7 in ${formatTokens(stats.prompt - stats.cached)} \xB7 prompt ${formatTokens(stats.prompt)} \xB7 cached ${formatTokens(stats.cached)} \xB7 write ${formatTokens(stats.cacheWrite)} \xB7 hit ${percent(stats.hit)} \xB7 p50 ${percent(stats.hitP50)}`,
2285
+ tone: "muted"
2286
+ },
2287
+ { text: "", tone: "muted" },
2288
+ ...providerLines(providerBreakdownOf(steps), width, 40),
2289
+ { text: "", tone: "muted" },
2290
+ ...latencyLines(latencyOf(steps), width, latencyGroupsOf(steps, "provider"), 14),
2291
+ { text: "", tone: "muted" },
2292
+ back
2293
+ ];
2294
+ }
2295
+ if (row.kind === "session" || row.kind === "turn") {
2296
+ const rows = turns(db, { sessionID: row.value, limit: 80 });
2297
+ return [head, { text: "", tone: "muted" }, ...turnsLines(rows, width, request.format), { text: "", tone: "muted" }, back];
2298
+ }
2299
+ if (row.kind === "provider") {
2300
+ const scoped = { ...filters, provider: row.value };
2301
+ const steps = stepRows(db, { range, filters: scoped });
2302
+ const stats = cacheStatsOf(db, { range, filters: scoped });
2303
+ return [
2304
+ head,
2305
+ {
2306
+ text: `steps ${stats.steps} \xB7 in ${formatTokens(stats.prompt - stats.cached)} \xB7 prompt ${formatTokens(stats.prompt)} \xB7 cached ${formatTokens(stats.cached)} \xB7 hit ${percent(stats.hit)} \xB7 p50 ${percent(stats.hitP50)}`,
2307
+ tone: "muted"
2308
+ },
2309
+ { text: "", tone: "muted" },
2310
+ ...providerLines(providerBreakdownOf(steps), width, 40),
2311
+ { text: "", tone: "muted" },
2312
+ ...latencyLines(latencyOf(steps), width, latencyGroupsOf(steps, "model"), 14),
2313
+ { text: "", tone: "muted" },
2314
+ back
2315
+ ];
2316
+ }
2317
+ if (row.kind === "period") {
2318
+ const periods = periodsOf(stepRows(db, { range, filters }), request.granularity);
2319
+ const period = periods.find((entry) => entry.key === row.value);
2320
+ return [
2321
+ head,
2322
+ { text: `${formatCount(period?.steps ?? 0)} steps`, tone: "muted" },
2323
+ { text: "", tone: "muted" },
2324
+ ...providerSummaryLines(providerSummaryOf(stepRows(db, { range, filters })), width, 40),
2325
+ { text: "", tone: "muted" },
2326
+ back
2327
+ ];
2328
+ }
2329
+ return [head, back];
2330
+ }
2331
+ function percent(value) {
2332
+ return value === undefined ? "\u2013" : `${(value * 100).toFixed(1)}%`;
2333
+ }
2334
+ function sortModels(rows, sort, desc) {
2335
+ const sign = desc ? -1 : 1;
2336
+ const pick = (model) => {
2337
+ if (sort === "cached")
2338
+ return model.totals.cached;
2339
+ if (sort === "out")
2340
+ return model.totals.output;
2341
+ if (sort === "cost")
2342
+ return model.totals.cost;
2343
+ return model.totals.steps;
2344
+ };
2345
+ return [...rows].sort((left, right) => sign * (pick(right) - pick(left)));
2346
+ }
2347
+ function sortProviders(rows, sort, desc) {
2348
+ const sign = desc ? -1 : 1;
2349
+ const pick = (row) => {
2350
+ if (sort === "cached")
2351
+ return row.cache.cached;
2352
+ if (sort === "hit")
2353
+ return row.cache.hit ?? -1;
2354
+ if (sort === "cost")
2355
+ return row.usage.cost;
2356
+ if (sort === "share")
2357
+ return row.share;
2358
+ if (sort === "tps")
2359
+ return row.tps ?? -1;
2360
+ return row.usage.steps;
2361
+ };
2362
+ return [...rows].sort((left, right) => sign * (pick(right) - pick(left)));
2363
+ }
2364
+ function sortLatency(rows, sort, desc) {
2365
+ const sign = desc ? -1 : 1;
2366
+ const pick = (row) => {
2367
+ if (sort === "tps")
2368
+ return row.tps.p50 ?? -1;
2369
+ if (sort === "wall")
2370
+ return row.wall.p50 ?? -1;
2371
+ return row.count;
2372
+ };
2373
+ return [...rows].sort((left, right) => sign * (pick(right) - pick(left)));
2374
+ }
2375
+ function sortSessions(rows, sort, desc) {
2376
+ const sign = desc ? -1 : 1;
2377
+ const pick = (row) => {
2378
+ if (sort === "steps")
2379
+ return row.steps;
2380
+ if (sort === "cached")
2381
+ return row.cached;
2382
+ if (sort === "cost")
2383
+ return row.cost;
2384
+ return row.lastAt ?? 0;
2385
+ };
2386
+ return [...rows].sort((left, right) => sign * (pick(right) - pick(left)));
2387
+ }
2388
+ function sortTurns(rows, sort, desc) {
2389
+ const sign = desc ? -1 : 1;
2390
+ const pick = (row) => {
2391
+ if (sort === "cost")
2392
+ return row.totals.cost;
2393
+ if (sort === "tokens")
2394
+ return row.totals.cached + row.totals.input + row.totals.output;
2395
+ return row.endAt ?? 0;
2396
+ };
2397
+ return [...rows].sort((left, right) => sign * (pick(right) - pick(left)));
2398
+ }
2399
+ function cacheByModel(pairs) {
2400
+ const map = new Map;
2401
+ for (const row of cacheStatsByModel(pairs)) {
2402
+ map.set(modelKey(row.modelID, row.variant), row.cache);
2403
+ }
2404
+ return map;
2405
+ }
2406
+ function switchesByModel(rows) {
2407
+ return new Map(rows.map((row) => [modelKey(row.modelID, row.variant), row]));
2408
+ }
2409
+ function topCacheModels(pairs) {
2410
+ return cacheStatsByModel(pairs).slice(0, 10).map((row) => ({ usage: { modelID: row.modelID, variant: row.variant }, cache: row.cache }));
2411
+ }
2412
+ function activityOf(db, range, now) {
2413
+ const earliest = db.query(`SELECT MIN(${STEP_TIME}) AS at FROM steps`).get();
2414
+ const from = range?.from ?? earliest?.at ?? 0;
2415
+ const to = range?.to ?? now;
2416
+ const span = Math.max(1, to - from);
2417
+ const ladder = [3600000, 3 * 3600000, 6 * 3600000, 12 * 3600000, 86400000, 3 * 86400000, 7 * 86400000, 30 * 86400000];
2418
+ const bucket = ladder.find((size) => span / size <= 48) ?? ladder[ladder.length - 1];
2419
+ const rows = db.query(`SELECT CAST(COALESCE(created_at, recorded_at) / ? AS INTEGER) AS bucket, COUNT(*) AS steps
2420
+ FROM steps
2421
+ WHERE COALESCE(created_at, recorded_at) >= ?
2422
+ GROUP BY bucket ORDER BY bucket`).all(bucket, from);
2423
+ if (rows.length === 0)
2424
+ return [];
2425
+ const first = Math.floor(from / bucket);
2426
+ const last = rows[rows.length - 1].bucket;
2427
+ const series = [];
2428
+ for (let index = first;index <= last; index += 1) {
2429
+ series.push(rows.find((row) => row.bucket === index)?.steps ?? 0);
2430
+ }
2431
+ return series;
2432
+ }
2433
+
2434
+ // src/stats/maintenance.ts
2435
+ import { statSync as statSync2 } from "fs";
2436
+
2437
+ // src/stats/db.ts
2438
+ import { Database } from "bun:sqlite";
2439
+ import { chmodSync, mkdirSync } from "fs";
2440
+ import { dirname } from "path";
2441
+
2442
+ // src/stats/paths.ts
2443
+ import { homedir } from "os";
2444
+ import { join } from "path";
2445
+ import { statSync } from "fs";
2446
+ function defaultDbPath(env = process.env, platform = process.platform) {
2447
+ const name = join("opencode", "opencode-usage-insights", "usage.db");
2448
+ if (platform === "win32") {
2449
+ return join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), name);
2450
+ }
2451
+ if (platform === "darwin") {
2452
+ return join(homedir(), "Library", "Application Support", name);
2453
+ }
2454
+ return join(env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), name);
2455
+ }
2456
+ function expandHome(path, home = homedir()) {
2457
+ if (path === "~")
2458
+ return home;
2459
+ if (path.startsWith("~/") || path.startsWith("~\\"))
2460
+ return join(home, path.slice(2));
2461
+ return path;
2462
+ }
2463
+ function resolveDbPath(configured, env = process.env, platform = process.platform) {
2464
+ const home = env.HOME ?? env.USERPROFILE ?? homedir();
2465
+ if (configured)
2466
+ return expandHome(configured, home);
2467
+ if (env.OPENCODE_USAGE_INSIGHTS_DB)
2468
+ return expandHome(env.OPENCODE_USAGE_INSIGHTS_DB, home);
2469
+ return defaultDbPath(env, platform);
2470
+ }
2471
+ function ensureRegularFile(path) {
2472
+ let stats;
2473
+ try {
2474
+ stats = statSync(path);
2475
+ } catch {
2476
+ return;
2477
+ }
2478
+ if (!stats.isFile())
2479
+ throw new Error(`not a regular file: ${path}`);
2480
+ }
2481
+
2482
+ // src/stats/db.ts
2483
+ var SCHEMA_VERSION = 9;
2484
+ var LABEL_LIMIT = 4 * TEXT_LIMIT;
2485
+ var SCHEMA = `
2486
+ CREATE TABLE IF NOT EXISTS meta (
2487
+ key TEXT PRIMARY KEY,
2488
+ value TEXT NOT NULL
2489
+ );
2490
+
2491
+ CREATE TABLE IF NOT EXISTS sessions (
2492
+ session_id TEXT PRIMARY KEY,
2493
+ parent_id TEXT,
2494
+ directory TEXT,
2495
+ title TEXT,
2496
+ started_at INTEGER,
2497
+ updated_at INTEGER
2498
+ );
2499
+
2500
+ CREATE TABLE IF NOT EXISTS prompts (
2501
+ session_id TEXT NOT NULL,
2502
+ message_id TEXT NOT NULL,
2503
+ created_at INTEGER,
2504
+ PRIMARY KEY (session_id, message_id)
2505
+ );
2506
+ CREATE INDEX IF NOT EXISTS prompts_created ON prompts(created_at);
2507
+
2508
+ CREATE TABLE IF NOT EXISTS steps (
2509
+ message_id TEXT PRIMARY KEY,
2510
+ session_id TEXT NOT NULL,
2511
+ parent_id TEXT,
2512
+ provider_id TEXT,
2513
+ model_id TEXT,
2514
+ variant TEXT,
2515
+ input_tokens INTEGER NOT NULL DEFAULT 0,
2516
+ output_tokens INTEGER NOT NULL DEFAULT 0,
2517
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0,
2518
+ cache_read INTEGER NOT NULL DEFAULT 0,
2519
+ cache_write INTEGER NOT NULL DEFAULT 0,
2520
+ cost REAL NOT NULL DEFAULT 0,
2521
+ created_at INTEGER,
2522
+ streamed_at INTEGER,
2523
+ completed_at INTEGER,
2524
+ finish TEXT,
2525
+ recorded_at INTEGER NOT NULL
2526
+ );
2527
+ CREATE INDEX IF NOT EXISTS steps_session ON steps(session_id, created_at);
2528
+ CREATE INDEX IF NOT EXISTS steps_model ON steps(provider_id, model_id, variant);
2529
+ CREATE INDEX IF NOT EXISTS steps_completed ON steps(completed_at);
2530
+ -- Every range filter and every ordering compares the step's own clock, which
2531
+ -- falls back to the recording time for a step that was already in flight when
2532
+ -- the plugin loaded. The index has to cover that expression, or every ranged
2533
+ -- view is a full table scan.
2534
+ CREATE INDEX IF NOT EXISTS steps_at ON steps(COALESCE(created_at, recorded_at));
2535
+ CREATE INDEX IF NOT EXISTS steps_session_at ON steps(session_id, COALESCE(created_at, recorded_at));
2536
+
2537
+ CREATE TABLE IF NOT EXISTS events (
2538
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2539
+ session_id TEXT NOT NULL,
2540
+ kind TEXT NOT NULL,
2541
+ message_id TEXT,
2542
+ at INTEGER,
2543
+ data TEXT
2544
+ );
2545
+ CREATE INDEX IF NOT EXISTS events_session ON events(session_id, at);
2546
+ CREATE INDEX IF NOT EXISTS events_kind ON events(kind, at);
2547
+ `;
2548
+ var TABLES = ["sessions", "prompts", "events", "steps"];
2549
+ function jsonText(value) {
2550
+ if (value === undefined || value === null)
2551
+ return null;
2552
+ if (typeof value === "string")
2553
+ return value;
2554
+ try {
2555
+ return JSON.stringify(value);
2556
+ } catch {
2557
+ return null;
2558
+ }
2559
+ }
2560
+ var REMOVED_COLUMNS = ["provider_state", "upstream", "upstream_source", "upstream_model", "tps", "router"];
2561
+ var REMOVED_EVENT_KINDS = ["execution", "step-failed", "model-selected", "usage-recorded"];
2562
+ function dropUnused(db) {
2563
+ const objects = db.query("SELECT name, type FROM sqlite_master WHERE type IN ('table', 'index')").all();
2564
+ const tables = new Set(objects.filter((row) => row.type === "table").map((row) => row.name));
2565
+ const indexes = new Set(objects.filter((row) => row.type === "index").map((row) => row.name));
2566
+ if (tables.has("http_responses"))
2567
+ db.exec("DROP TABLE http_responses");
2568
+ if (indexes.has("steps_upstream"))
2569
+ db.exec("DROP INDEX steps_upstream");
2570
+ if (indexes.has("steps_router"))
2571
+ db.exec("DROP INDEX steps_router");
2572
+ const columns = new Set(db.query("PRAGMA table_info(steps)").all().map((column) => column.name));
2573
+ for (const name of REMOVED_COLUMNS) {
2574
+ if (columns.has(name))
2575
+ db.exec(`ALTER TABLE steps DROP COLUMN ${name}`);
2576
+ }
2577
+ if (tables.has("events")) {
2578
+ const kinds = REMOVED_EVENT_KINDS.map((kind) => `'${kind}'`).join(", ");
2579
+ db.exec(`DELETE FROM events WHERE kind IN (${kinds})`);
2580
+ }
2581
+ }
2582
+ function migrate(db) {
2583
+ dropUnused(db);
2584
+ }
2585
+ var POSIX = process.platform !== "win32";
2586
+ function hardenDirectory(path) {
2587
+ if (!POSIX)
2588
+ return;
2589
+ try {
2590
+ chmodSync(path, 448);
2591
+ } catch {}
2592
+ }
2593
+ function hardenFiles(path) {
2594
+ if (!POSIX || path === ":memory:")
2595
+ return;
2596
+ for (const file of [path, `${path}-wal`, `${path}-shm`]) {
2597
+ try {
2598
+ chmodSync(file, 384);
2599
+ } catch {}
2600
+ }
2601
+ }
2602
+ function prepareDirectory(path) {
2603
+ if (path === ":memory:")
2604
+ return;
2605
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
2606
+ hardenDirectory(dirname(path));
2607
+ }
2608
+ function openStatsDb(path) {
2609
+ if (path !== ":memory:")
2610
+ ensureRegularFile(path);
2611
+ prepareDirectory(path);
2612
+ const db = new Database(path, { create: true });
2613
+ db.exec("PRAGMA journal_mode = WAL");
2614
+ db.exec("PRAGMA busy_timeout = 5000");
2615
+ db.exec("PRAGMA synchronous = NORMAL");
2616
+ db.exec(SCHEMA);
2617
+ migrate(db);
2618
+ db.query("INSERT INTO meta (key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(SCHEMA_VERSION));
2619
+ hardenFiles(path);
2620
+ return db;
2621
+ }
2622
+ function openStatsDbReadOnly(path) {
2623
+ ensureRegularFile(path);
2624
+ const db = new Database(path, { readonly: true });
2625
+ try {
2626
+ db.query("SELECT 1 FROM steps LIMIT 1").get();
2627
+ } catch (error) {
2628
+ db.close();
2629
+ throw error;
2630
+ }
2631
+ return db;
2632
+ }
2633
+ function openStatsDbWrite(path) {
2634
+ if (path !== ":memory:")
2635
+ ensureRegularFile(path);
2636
+ prepareDirectory(path);
2637
+ const db = new Database(path, { create: true });
2638
+ db.exec("PRAGMA busy_timeout = 250");
2639
+ db.exec("PRAGMA synchronous = NORMAL");
2640
+ hardenFiles(path);
2641
+ return db;
2642
+ }
2643
+ function recordStep(db, record) {
2644
+ db.query(`INSERT INTO steps (
2645
+ message_id, session_id, parent_id, provider_id, model_id, variant,
2646
+ input_tokens, output_tokens, reasoning_tokens, cache_read, cache_write, cost,
2647
+ created_at, streamed_at, completed_at, finish, recorded_at
2648
+ ) VALUES (
2649
+ $messageID, $sessionID, $parentID, $providerID, $modelID, $variant,
2650
+ $input, $output, $reasoning, $cacheRead, $cacheWrite, $cost,
2651
+ $createdAt, $streamedAt, $completedAt, $finish, $recordedAt
2652
+ )
2653
+ ON CONFLICT(message_id) DO UPDATE SET
2654
+ session_id = excluded.session_id,
2655
+ parent_id = COALESCE(excluded.parent_id, steps.parent_id),
2656
+ provider_id = COALESCE(excluded.provider_id, steps.provider_id),
2657
+ model_id = COALESCE(excluded.model_id, steps.model_id),
2658
+ variant = COALESCE(excluded.variant, steps.variant),
2659
+ input_tokens = excluded.input_tokens,
2660
+ output_tokens = excluded.output_tokens,
2661
+ reasoning_tokens = excluded.reasoning_tokens,
2662
+ cache_read = excluded.cache_read,
2663
+ cache_write = excluded.cache_write,
2664
+ cost = excluded.cost,
2665
+ created_at = COALESCE(excluded.created_at, steps.created_at),
2666
+ streamed_at = COALESCE(excluded.streamed_at, steps.streamed_at),
2667
+ completed_at = COALESCE(excluded.completed_at, steps.completed_at),
2668
+ finish = COALESCE(excluded.finish, steps.finish),
2669
+ recorded_at = excluded.recorded_at`).run({
2670
+ $messageID: record.messageID,
2671
+ $sessionID: record.sessionID,
2672
+ $parentID: record.parentID ?? null,
2673
+ $providerID: record.providerID !== undefined && isToken(record.providerID) ? record.providerID : null,
2674
+ $modelID: record.modelID ?? null,
2675
+ $variant: record.variant ?? null,
2676
+ $input: record.input ?? 0,
2677
+ $output: record.output ?? 0,
2678
+ $reasoning: record.reasoning ?? 0,
2679
+ $cacheRead: record.cacheRead ?? 0,
2680
+ $cacheWrite: record.cacheWrite ?? 0,
2681
+ $cost: record.cost ?? 0,
2682
+ $createdAt: record.createdAt ?? null,
2683
+ $streamedAt: record.streamedAt ?? null,
2684
+ $completedAt: record.completedAt ?? null,
2685
+ $finish: record.finish === undefined ? null : sanitizeLabel(record.finish, 64),
2686
+ $recordedAt: record.recordedAt ?? Date.now()
2687
+ });
2688
+ }
2689
+ function recordPrompt(db, record) {
2690
+ db.query(`INSERT INTO prompts (session_id, message_id, created_at)
2691
+ VALUES ($sessionID, $messageID, $createdAt)
2692
+ ON CONFLICT(session_id, message_id) DO UPDATE SET created_at = COALESCE(excluded.created_at, prompts.created_at)`).run({
2693
+ $sessionID: record.sessionID,
2694
+ $messageID: record.messageID,
2695
+ $createdAt: record.createdAt ?? null
2696
+ });
2697
+ }
2698
+ function recordEvent(db, record) {
2699
+ db.query(`INSERT INTO events (session_id, kind, message_id, at, data)
2700
+ VALUES ($sessionID, $kind, $messageID, $at, $data)`).run({
2701
+ $sessionID: record.sessionID,
2702
+ $kind: sanitizeLabel(record.kind, 64),
2703
+ $messageID: record.messageID ?? null,
2704
+ $at: record.at ?? Date.now(),
2705
+ $data: jsonText(record.data)
2706
+ });
2707
+ }
2708
+ function upsertSession(db, record) {
2709
+ db.query(`INSERT INTO sessions (session_id, parent_id, directory, title, started_at, updated_at)
2710
+ VALUES ($sessionID, $parentID, $directory, $title, $startedAt, $updatedAt)
2711
+ ON CONFLICT(session_id) DO UPDATE SET
2712
+ parent_id = COALESCE(excluded.parent_id, sessions.parent_id),
2713
+ directory = COALESCE(excluded.directory, sessions.directory),
2714
+ title = COALESCE(excluded.title, sessions.title),
2715
+ started_at = COALESCE(sessions.started_at, excluded.started_at),
2716
+ updated_at = COALESCE(excluded.updated_at, sessions.updated_at)`).run({
2717
+ $sessionID: record.sessionID,
2718
+ $parentID: record.parentID ?? null,
2719
+ $directory: record.directory === undefined ? null : sanitizeLabel(record.directory, LABEL_LIMIT),
2720
+ $title: record.title === undefined ? null : sanitizeLabel(record.title, LABEL_LIMIT),
2721
+ $startedAt: record.startedAt ?? null,
2722
+ $updatedAt: record.updatedAt ?? null
2723
+ });
2724
+ }
2725
+
2726
+ // src/stats/maintenance.ts
2727
+ var STEP_TIME2 = "COALESCE(created_at, recorded_at)";
2728
+ var DAY = 24 * 60 * 60 * 1000;
2729
+ function size(path) {
2730
+ try {
2731
+ return statSync2(path).size;
2732
+ } catch {
2733
+ return 0;
2734
+ }
2735
+ }
2736
+ function dbInfo(db, path, now = Date.now()) {
2737
+ const tables = TABLES.map((table) => ({
2738
+ name: table,
2739
+ rows: db.query(`SELECT COUNT(*) AS n FROM ${table}`).get().n
2740
+ }));
2741
+ const window = db.query(`SELECT MIN(${STEP_TIME2}) AS first, MAX(${STEP_TIME2}) AS last FROM steps`).get();
2742
+ const version = db.query("SELECT value FROM meta WHERE key = 'schema_version'").get();
2743
+ return {
2744
+ path,
2745
+ bytes: size(path),
2746
+ walBytes: path === ":memory:" ? undefined : size(`${path}-wal`) || undefined,
2747
+ schemaVersion: version === null ? 0 : Number.parseInt(version.value, 10),
2748
+ tables,
2749
+ firstAt: window.first ?? undefined,
2750
+ lastAt: window.last ?? undefined,
2751
+ removable: [30, 90, 365].map((days) => ({
2752
+ days,
2753
+ steps: db.query(`SELECT COUNT(*) AS n FROM steps WHERE ${STEP_TIME2} < ?`).get(now - days * DAY).n
2754
+ }))
2755
+ };
2756
+ }
2757
+ var PRUNE_BATCH = 5000;
2758
+ function deleteBatched(db, table, where, cutoff) {
2759
+ const statement = db.query(`DELETE FROM ${table} WHERE rowid IN (SELECT rowid FROM ${table} WHERE ${where} LIMIT ${PRUNE_BATCH})`);
2760
+ let removed = 0;
2761
+ for (;; ) {
2762
+ const changes = statement.run(cutoff).changes;
2763
+ removed += changes;
2764
+ if (changes < PRUNE_BATCH)
2765
+ break;
2766
+ }
2767
+ return removed;
2768
+ }
2769
+ function pruneOlderThan(db, cutoff) {
2770
+ return {
2771
+ steps: deleteBatched(db, "steps", `${STEP_TIME2} < ?`, cutoff),
2772
+ prompts: deleteBatched(db, "prompts", "created_at IS NOT NULL AND created_at < ?", cutoff),
2773
+ events: deleteBatched(db, "events", "at IS NOT NULL AND at < ?", cutoff)
2774
+ };
2775
+ }
2776
+ function footprint(path) {
2777
+ return size(path) + size(`${path}-wal`);
2778
+ }
2779
+ function vacuum(db, path) {
2780
+ const before = footprint(path);
2781
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
2782
+ db.exec("VACUUM");
2783
+ const after = footprint(path);
2784
+ return { before, after, freed: Math.max(0, before - after) };
2785
+ }
2786
+
2787
+ // src/panel.tsx
2788
+ function Panel(props) {
2789
+ const [anchor, setAnchor] = createSignal(undefined);
2790
+ const [detail, setDetail] = createSignal(undefined);
2791
+ const [help, setHelp] = createSignal(false);
2792
+ const [tick, setTick] = createSignal(0);
2793
+ const [busy, setBusy] = createSignal(false);
2794
+ let scroller;
2795
+ const width = () => Math.max(60, props.input.width - 4);
2796
+ const view = () => sanitizeSettings(props.settings).view;
2797
+ const settings = () => sanitizeSettings(props.settings);
2798
+ const filters = () => {
2799
+ const current = settings();
2800
+ return {
2801
+ ...current.modelID !== "" ? {
2802
+ modelID: current.modelID
2803
+ } : {},
2804
+ ...current.providerID !== "" ? {
2805
+ provider: current.providerID
2806
+ } : {}
2807
+ };
2808
+ };
2809
+ const rowKey = (row) => `${row.kind}:${row.value}${row.variant !== undefined ? `#${row.variant}` : ""}`;
2810
+ const database = createMemo(() => {
2811
+ tick();
2812
+ const handle = props.deps.db();
2813
+ if (handle === undefined)
2814
+ return;
2815
+ try {
2816
+ return dbInfo(handle, props.deps.dbPath(), props.deps.now());
2817
+ } catch {
2818
+ return;
2819
+ }
2820
+ });
2821
+ const content = createMemo(() => {
2822
+ tick();
2823
+ props.deps.refresh?.();
2824
+ const current = settings();
2825
+ if (help())
2826
+ return helpLines(width());
2827
+ const db = props.deps.db();
2828
+ if (db === undefined) {
2829
+ return [{
2830
+ text: `stats database is not readable at ${props.deps.dbPath()}; enable the usage-insights server plugin in opencode.json`,
2831
+ tone: "warning"
2832
+ }];
2833
+ }
2834
+ const info = current.view === "db" ? database() : undefined;
2835
+ const request = {
2836
+ db,
2837
+ view: current.view,
2838
+ range: current.range,
2839
+ filters: filters(),
2840
+ granularity: current.granularity,
2841
+ turnsScope: current.turnsScope,
2842
+ sort: current.sort,
2843
+ sortDesc: current.sortDesc,
2844
+ sessionID: props.input.sessionID,
2845
+ sessionStart: props.deps.sessionStart(props.input.sessionID),
2846
+ now: props.deps.now(),
2847
+ format: props.format,
2848
+ ...detail() !== undefined ? {
2849
+ detail: detail()
2850
+ } : {},
2851
+ ...info !== undefined ? {
2852
+ database: info
2853
+ } : {},
2854
+ busy: busy(),
2855
+ width: width()
2856
+ };
2857
+ return buildPanel(request);
2858
+ });
2859
+ const header = createMemo(() => {
2860
+ const current = settings();
2861
+ const filters = filterSummary(current);
2862
+ const where = detail() !== undefined ? " \xB7 backspace returns" : filters === undefined ? "" : ` \xB7 ${filters}`;
2863
+ return [{
2864
+ text: `usage insights \xB7 ${rangeLabel(current.range)}${where}`,
2865
+ tone: "accent"
2866
+ }, viewBar(current.view, width()), {
2867
+ text: "",
2868
+ tone: "muted"
2869
+ }];
2870
+ });
2871
+ const selectable = createMemo(() => {
2872
+ const list = [];
2873
+ const lines = content();
2874
+ for (let index = 0;index < lines.length; index += 1) {
2875
+ const row = lines[index].row;
2876
+ if (row !== undefined)
2877
+ list.push({
2878
+ key: rowKey(row),
2879
+ index,
2880
+ row
2881
+ });
2882
+ }
2883
+ return list;
2884
+ });
2885
+ const cursor = createMemo(() => {
2886
+ const list = selectable();
2887
+ if (list.length === 0)
2888
+ return -1;
2889
+ const key = anchor();
2890
+ const found = key === undefined ? -1 : list.findIndex((entry) => entry.key === key);
2891
+ return found === -1 ? 0 : found;
2892
+ });
2893
+ createRenderEffect(() => {
2894
+ const list = selectable();
2895
+ const index = cursor();
2896
+ if (index < 0)
2897
+ return;
2898
+ const entry = list[index];
2899
+ if (entry === undefined)
2900
+ return;
2901
+ scroller?.scrollChildIntoView?.(`line-${entry.index}`);
2902
+ });
2903
+ function move(delta) {
2904
+ const list = selectable();
2905
+ if (list.length === 0)
2906
+ return;
2907
+ const next = Math.min(list.length - 1, Math.max(0, cursor() + delta));
2908
+ setAnchor(list[next].key);
2909
+ }
2910
+ function moveEdge(toEnd) {
2911
+ const list = selectable();
2912
+ if (list.length === 0)
2913
+ return;
2914
+ setAnchor(list[toEnd ? list.length - 1 : 0].key);
2915
+ }
2916
+ function drill() {
2917
+ const entry = selectable()[cursor()];
2918
+ if (entry === undefined)
2919
+ return;
2920
+ if (entry.row.kind === "prune") {
2921
+ prune(Number.parseInt(entry.row.value, 10));
2922
+ return;
2923
+ }
2924
+ setDetail(entry.row);
2925
+ setAnchor(undefined);
2926
+ }
2927
+ function sessionRunning() {
2928
+ try {
2929
+ return props.ctx.data.session.status(props.input.sessionID) === "running";
2930
+ } catch {
2931
+ return false;
2932
+ }
2933
+ }
2934
+ async function prune(days) {
2935
+ if (!Number.isFinite(days))
2936
+ return;
2937
+ if (sessionRunning()) {
2938
+ props.ctx.ui.toast.show({
2939
+ message: "the session is running: prune again when it is idle",
2940
+ variant: "warning"
2941
+ });
2942
+ return;
2943
+ }
2944
+ const info = database();
2945
+ const steps = info?.removable.find((option) => option.days === days)?.steps;
2946
+ const confirmed = await props.ctx.ui.dialog.confirm({
2947
+ title: `Prune older than ${days} days`,
2948
+ message: `${steps === undefined ? "The recorded usage" : `${steps} of ${formatCount(info?.tables.find((table) => table.name === "steps")?.rows ?? 0)} steps`} would be deleted. This cannot be undone.`,
2949
+ label: {
2950
+ confirm: "Prune",
2951
+ cancel: "Keep"
2952
+ }
2953
+ });
2954
+ if (confirmed !== true)
2955
+ return;
2956
+ const cutoff = props.deps.now() - days * 24 * 60 * 60 * 1000;
2957
+ const result = props.deps.withWrite?.((handle) => pruneOlderThan(handle, cutoff));
2958
+ if (result === undefined) {
2959
+ props.ctx.ui.toast.show({
2960
+ message: "the recorder is writing: try again in a moment",
2961
+ variant: "warning"
2962
+ });
2963
+ } else {
2964
+ props.ctx.ui.toast.show({
2965
+ message: `pruned ${result.steps} steps \xB7 ${result.prompts} prompts \xB7 ${result.events} compactions`,
2966
+ variant: "success"
2967
+ });
2968
+ }
2969
+ setAnchor(undefined);
2970
+ setTick((value) => value + 1);
2971
+ }
2972
+ async function compact() {
2973
+ if (sessionRunning()) {
2974
+ props.ctx.ui.toast.show({
2975
+ message: "the session is running: compact again when it is idle",
2976
+ variant: "warning"
2977
+ });
2978
+ return;
2979
+ }
2980
+ const confirmed = await props.ctx.ui.dialog.confirm({
2981
+ title: "Compact the database",
2982
+ message: "SQLite rewrites the file to reclaim the space of deleted rows. It needs the recorder to be idle, and it can take a moment.",
2983
+ label: {
2984
+ confirm: "Compact",
2985
+ cancel: "Cancel"
2986
+ }
2987
+ });
2988
+ if (confirmed !== true)
2989
+ return;
2990
+ const path = props.deps.dbPath();
2991
+ let result;
2992
+ try {
2993
+ result = props.deps.withWrite?.((handle) => vacuum(handle, path));
2994
+ } catch {
2995
+ result = undefined;
2996
+ }
2997
+ if (result === undefined) {
2998
+ setBusy(true);
2999
+ props.ctx.ui.toast.show({
3000
+ message: "the recorder is writing: try again in a moment",
3001
+ variant: "warning"
3002
+ });
3003
+ } else {
3004
+ setBusy(false);
3005
+ props.ctx.ui.toast.show({
3006
+ message: `compacted: ${(result.freed / 1048576).toFixed(1)} MB back`,
3007
+ variant: "success"
3008
+ });
3009
+ }
3010
+ setTick((value) => value + 1);
3011
+ }
3012
+ function back() {
3013
+ if (detail() === undefined)
3014
+ return;
3015
+ setDetail(undefined);
3016
+ setAnchor(undefined);
3017
+ }
3018
+ async function pickFilter(kind) {
3019
+ const db = props.deps.db();
3020
+ if (db === undefined)
3021
+ return;
3022
+ const all = filterOptions(db);
3023
+ const list = kind === "model" ? all.models : all.providers;
3024
+ const current = settings();
3025
+ const selected = kind === "model" ? current.modelID : current.providerID;
3026
+ const value = await props.ctx.ui.dialog.select({
3027
+ title: kind === "model" ? "Filter by model" : "Filter by provider",
3028
+ current: selected,
3029
+ options: [{
3030
+ title: `any \xB7 ${list.length} observed`,
3031
+ value: ""
3032
+ }, ...list.map((entry) => ({
3033
+ title: entry.value,
3034
+ value: entry.value,
3035
+ description: `${entry.steps} steps`,
3036
+ category: kind
3037
+ }))]
3038
+ });
3039
+ if (value === undefined)
3040
+ return;
3041
+ props.update((draft) => {
3042
+ if (kind === "model")
3043
+ draft.modelID = value;
3044
+ else
3045
+ draft.providerID = value;
3046
+ });
3047
+ setAnchor(undefined);
3048
+ setDetail(undefined);
3049
+ }
3050
+ const command = (id, title, bind, run) => ({
3051
+ id,
3052
+ title,
3053
+ bind,
3054
+ run
3055
+ });
3056
+ props.ctx.keymap.layer(() => {
3057
+ return {
3058
+ commands: [command("usage-insights.cursor.up", "Move up", "up", () => move(-1)), command("usage-insights.cursor.down", "Move down", "down", () => move(1)), command("usage-insights.cursor.up.alt", "Move up", "k", () => move(-1)), command("usage-insights.cursor.down.alt", "Move down", "j", () => move(1)), command("usage-insights.page.up", "Page up", "pageup", () => move(-10)), command("usage-insights.page.down", "Page down", "pagedown", () => move(10)), command("usage-insights.first", "First row", "home", () => moveEdge(false)), command("usage-insights.last", "Last row", "end", () => moveEdge(true)), command("usage-insights.drill", "Open the selected row", "return", () => drill()), command("usage-insights.back", "Go back", "backspace", () => back()), command("usage-insights.view.next", "Next view", "]", () => {
3059
+ setHelp(false);
3060
+ props.update((draft) => {
3061
+ draft.view = nextIn(PANEL_VIEWS, sanitizeSettings(draft).view, 1);
3062
+ });
3063
+ }), command("usage-insights.view.previous", "Previous view", "[", () => {
3064
+ setHelp(false);
3065
+ props.update((draft) => {
3066
+ draft.view = nextIn(PANEL_VIEWS, sanitizeSettings(draft).view, -1);
3067
+ });
3068
+ }), command("usage-insights.range.next", "Next range", "t", () => props.update((draft) => {
3069
+ draft.range = nextIn(RANGE_KEYS, sanitizeSettings(draft).range, 1);
3070
+ })), command("usage-insights.range.previous", "Previous range", "shift+t", () => props.update((draft) => {
3071
+ draft.range = nextIn(RANGE_KEYS, sanitizeSettings(draft).range, -1);
3072
+ })), command("usage-insights.sort.cycle", "Cycle sort column", "s", () => props.update((draft) => {
3073
+ const state = sanitizeSettings(draft);
3074
+ draft.sort = nextSort(state.view, state.sort === "" ? defaultSort(state.view) : state.sort, 1);
3075
+ })), command("usage-insights.sort.reverse", "Reverse sort", "shift+s", () => props.update((draft) => {
3076
+ draft.sortDesc = !sanitizeSettings(draft).sortDesc;
3077
+ })), command("usage-insights.bucket", "Next period bucket", "b", () => props.update((draft) => {
3078
+ draft.granularity = nextIn(GRANULARITIES, sanitizeSettings(draft).granularity, 1);
3079
+ draft.view = "periods";
3080
+ })), command("usage-insights.scope", "Toggle the turns scope", "a", () => props.update((draft) => {
3081
+ draft.turnsScope = sanitizeSettings(draft).turnsScope === "session" ? "all" : "session";
3082
+ draft.view = "turns";
3083
+ })), command("usage-insights.filter.model", "Filter by model", "m", () => void pickFilter("model")), command("usage-insights.filter.provider", "Filter by provider", "p", () => void pickFilter("provider")), command("usage-insights.filter.clear", "Clear filters", "x", () => {
3084
+ props.update((draft) => {
3085
+ draft.modelID = "";
3086
+ draft.providerID = "";
3087
+ });
3088
+ setAnchor(undefined);
3089
+ }), command("usage-insights.refresh", "Refresh", "r", () => setTick((value) => value + 1)), command("usage-insights.vacuum", "Compact the database", "v", () => void compact()), command("usage-insights.help", "Help", "?", () => setHelp((open) => !open)), command("usage-insights.fullscreen", "Toggle full screen", "f", () => props.input.toggleFullscreen()), command("usage-insights.close", "Close usage panel", "escape", () => props.input.close()), ...PANEL_VIEWS.map((name, index) => command(`usage-insights.view.${name}`, `View ${name}`, String((index + 1) % 10), () => {
3090
+ setHelp(false);
3091
+ props.update((draft) => {
3092
+ draft.view = name;
3093
+ });
3094
+ }))]
3095
+ };
3096
+ });
3097
+ const color = (tone) => {
3098
+ switch (tone) {
3099
+ case "muted":
3100
+ return props.ctx.theme.text.muted;
3101
+ case "warning":
3102
+ return props.ctx.theme.text.feedback.warning.base;
3103
+ case "accent":
3104
+ return props.ctx.theme.text.feedback.info.base;
3105
+ default:
3106
+ return props.ctx.theme.text.base;
3107
+ }
3108
+ };
3109
+ const selectedBackground = () => props.ctx.theme.background.raised.high;
3110
+ const cursorIndex = () => cursor();
3111
+ return (() => {
3112
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("scrollbox"), _el$3 = _$createElement("text"), _el$4 = _$createElement("text");
3113
+ _$insertNode(_el$, _el$2);
3114
+ _$setProp(_el$, "flexDirection", "column");
3115
+ _$setProp(_el$, "paddingLeft", 1);
3116
+ _$setProp(_el$, "paddingRight", 1);
3117
+ _$insertNode(_el$2, _el$3);
3118
+ _$insertNode(_el$2, _el$4);
3119
+ _$use((target) => {
3120
+ scroller = target;
3121
+ const focusable = target;
3122
+ if (focusable !== null && typeof focusable === "object")
3123
+ focusable.focusable = false;
3124
+ }, _el$2);
3125
+ _$setProp(_el$2, "flexGrow", 1);
3126
+ _$setProp(_el$2, "scrollY", true);
3127
+ _$setProp(_el$2, "verticalScrollbarOptions", {
3128
+ visible: true
3129
+ });
3130
+ _$insert(_el$2, _$createComponent(For, {
3131
+ get each() {
3132
+ return header();
3133
+ },
3134
+ children: (line) => (() => {
3135
+ var _el$5 = _$createElement("text");
3136
+ _$insert(_el$5, () => line.text);
3137
+ _$effect((_$p) => _$setProp(_el$5, "fg", color(line.tone), _$p));
3138
+ return _el$5;
3139
+ })()
3140
+ }), _el$3);
3141
+ _$insert(_el$2, _$createComponent(For, {
3142
+ get each() {
3143
+ return content();
3144
+ },
3145
+ children: (line, index) => {
3146
+ const selected = () => index() === selectedLineIndex(content(), cursorIndex());
3147
+ return (() => {
3148
+ var _el$6 = _$createElement("text");
3149
+ _$insert(_el$6, () => line.text);
3150
+ _$effect((_p$) => {
3151
+ var _v$3 = `line-${index()}`, _v$4 = color(line.tone), _v$5 = selected() ? selectedBackground() : undefined, _v$6 = selected() ? TextAttributes.BOLD : undefined;
3152
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$6, "id", _v$3, _p$.e));
3153
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "fg", _v$4, _p$.t));
3154
+ _v$5 !== _p$.a && (_p$.a = _$setProp(_el$6, "bg", _v$5, _p$.a));
3155
+ _v$6 !== _p$.o && (_p$.o = _$setProp(_el$6, "attributes", _v$6, _p$.o));
3156
+ return _p$;
3157
+ }, {
3158
+ e: undefined,
3159
+ t: undefined,
3160
+ a: undefined,
3161
+ o: undefined
3162
+ });
3163
+ return _el$6;
3164
+ })();
3165
+ }
3166
+ }), _el$3);
3167
+ _$insert(_el$4, () => keysFor(view()));
3168
+ _$effect((_p$) => {
3169
+ var _v$ = props.ctx.theme.text.muted, _v$2 = props.ctx.theme.text.muted;
3170
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "fg", _v$, _p$.e));
3171
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "fg", _v$2, _p$.t));
3172
+ return _p$;
3173
+ }, {
3174
+ e: undefined,
3175
+ t: undefined
3176
+ });
3177
+ return _el$;
3178
+ })();
3179
+ }
3180
+ function selectedLineIndex(lines, cursor) {
3181
+ let seen = -1;
3182
+ for (let index = 0;index < lines.length; index += 1) {
3183
+ if (lines[index].row === undefined)
3184
+ continue;
3185
+ seen += 1;
3186
+ if (seen === cursor)
3187
+ return index;
3188
+ }
3189
+ return -1;
3190
+ }
3191
+
3192
+ // src/sidebar.tsx
3193
+ import { effect as _$effect2 } from "@opentui/solid";
3194
+ import { insert as _$insert2 } from "@opentui/solid";
3195
+ import { createComponent as _$createComponent2 } from "@opentui/solid";
3196
+ import { setProp as _$setProp2 } from "@opentui/solid";
3197
+ import { createElement as _$createElement2 } from "@opentui/solid";
3198
+ import { For as For2, createMemo as createMemo2 } from "solid-js";
3199
+ function TurnsSidebar(props) {
3200
+ const color = (tone) => {
3201
+ switch (tone) {
3202
+ case "muted":
3203
+ return props.ctx.theme.text.muted;
3204
+ case "warning":
3205
+ return props.ctx.theme.text.feedback.warning.base;
3206
+ case "accent":
3207
+ return props.ctx.theme.text.feedback.info.base;
3208
+ default:
3209
+ return props.ctx.theme.text.base;
3210
+ }
3211
+ };
3212
+ const lines = createMemo2(() => {
3213
+ props.deps.refresh();
3214
+ const db = props.deps.db();
3215
+ if (db === undefined)
3216
+ return [];
3217
+ const budget = props.deps.budget ?? 34;
3218
+ try {
3219
+ const summary = sessionSummaryLines(overview(db, undefined, {
3220
+ sessionID: props.sessionID
3221
+ }), budget);
3222
+ const history = sidebarLines(turns(db, {
3223
+ sessionID: props.sessionID,
3224
+ limit: props.deps.limit
3225
+ }), {
3226
+ ...props.options,
3227
+ budget
3228
+ });
3229
+ return [...summary, {
3230
+ text: "turns",
3231
+ tone: "accent"
3232
+ }, ...history];
3233
+ } catch {
3234
+ return [];
3235
+ }
3236
+ });
3237
+ return (() => {
3238
+ var _el$ = _$createElement2("box");
3239
+ _$setProp2(_el$, "flexDirection", "column");
3240
+ _$setProp2(_el$, "paddingLeft", 1);
3241
+ _$setProp2(_el$, "paddingRight", 1);
3242
+ _$insert2(_el$, _$createComponent2(For2, {
3243
+ get each() {
3244
+ return lines();
3245
+ },
3246
+ children: (line) => (() => {
3247
+ var _el$2 = _$createElement2("text");
3248
+ _$insert2(_el$2, () => line.text);
3249
+ _$effect2((_$p) => _$setProp2(_el$2, "fg", color(line.tone), _$p));
3250
+ return _el$2;
3251
+ })()
3252
+ }));
3253
+ return _el$;
3254
+ })();
3255
+ }
3256
+
3257
+ // src/tui.tsx
3258
+ var REFRESH_THROTTLE_MS = 300;
3259
+ var __test = {
3260
+ line: undefined,
3261
+ lineText: undefined,
3262
+ open: undefined,
3263
+ state: undefined
3264
+ };
3265
+ var tui_default = Plugin.define({
3266
+ id: "usage-insights",
3267
+ setup(ctx) {
3268
+ const options = ctx.options ?? {};
3269
+ if (options.enabled === false)
3270
+ return;
3271
+ const panelEnabled = options.panel !== false;
3272
+ const [settings, updateSettings] = ctx.storage.store("panel", {
3273
+ initial: DEFAULT_SETTINGS
3274
+ });
3275
+ const patchSettings = (mutation) => {
3276
+ updateSettings(mutation);
3277
+ };
3278
+ const [refresh, setRefresh] = createSignal2(0);
3279
+ let database;
3280
+ let databaseFailed = false;
3281
+ let refreshTimer;
3282
+ let lastRefresh = 0;
3283
+ function bumpRefresh() {
3284
+ const now = Date.now();
3285
+ const wait = lastRefresh + REFRESH_THROTTLE_MS - now;
3286
+ if (wait <= 0) {
3287
+ lastRefresh = now;
3288
+ setRefresh((value) => value + 1);
3289
+ return;
3290
+ }
3291
+ if (refreshTimer !== undefined)
3292
+ return;
3293
+ refreshTimer = setTimeout(() => {
3294
+ refreshTimer = undefined;
3295
+ lastRefresh = Date.now();
3296
+ setRefresh((value) => value + 1);
3297
+ }, wait);
3298
+ refreshTimer.unref?.();
3299
+ }
3300
+ function db() {
3301
+ if (database !== undefined)
3302
+ return database;
3303
+ if (databaseFailed)
3304
+ return;
3305
+ try {
3306
+ database = openStatsDbReadOnly(resolveDbPath(options.db));
3307
+ return database;
3308
+ } catch {
3309
+ databaseFailed = true;
3310
+ return;
3311
+ }
3312
+ }
3313
+ function withWrite(run) {
3314
+ let handle;
3315
+ try {
3316
+ handle = openStatsDbWrite(resolveDbPath(options.db));
3317
+ return run(handle);
3318
+ } catch {
3319
+ return;
3320
+ } finally {
3321
+ handle?.close();
3322
+ }
3323
+ }
3324
+ function sessionStart(sessionID) {
3325
+ return ctx.data.session.get(sessionID)?.time.created;
3326
+ }
3327
+ function sessionIdle(sessionID) {
3328
+ try {
3329
+ return ctx.data.session.status(sessionID) === "idle";
3330
+ } catch {
3331
+ return true;
3332
+ }
3333
+ }
3334
+ function lineFor(sessionID) {
3335
+ if (!sessionIdle(sessionID))
3336
+ return;
3337
+ const messages = ctx.data.session.message.list(sessionID);
3338
+ const info = ctx.data.session.get(sessionID);
3339
+ return computeTurn(toTranscriptTail(messages, 2), sessionTotals(info));
3340
+ }
3341
+ function toneColor(tone) {
3342
+ if (tone === "warning")
3343
+ return ctx.theme.text.feedback.warning.base;
3344
+ if (tone === "muted")
3345
+ return ctx.theme.text.muted;
3346
+ return ctx.theme.text.base;
3347
+ }
3348
+ function Line(props) {
3349
+ const parts = createMemo3(() => lineParts(lineFor(props.sessionID), {
3350
+ labels: options.labels,
3351
+ fields: options.fields,
3352
+ turnTotals: options.turnTotals
3353
+ }));
3354
+ return _$createComponent3(Show, {
3355
+ get when() {
3356
+ return parts().length > 0;
3357
+ },
3358
+ children: () => {
3359
+ let node;
3360
+ const element = (() => {
3361
+ var _el$ = _$createElement3("text");
3362
+ _$use2((target) => {
3363
+ node = target;
3364
+ }, _el$);
3365
+ return _el$;
3366
+ })();
3367
+ createRenderEffect2(() => {
3368
+ const rendered = node;
3369
+ if (rendered === undefined)
3370
+ return;
3371
+ const current = parts();
3372
+ rendered.content = current.length === 0 ? "" : new StyledText(current.map((part) => fg(toneColor(part.tone))(part.text)));
3373
+ });
3374
+ return element;
3375
+ }
3376
+ });
3377
+ }
3378
+ const disposeLine = ctx.ui.slot({
3379
+ append: "session.composer.top",
3380
+ render: (input) => _$createComponent3(Line, {
3381
+ get sessionID() {
3382
+ return input.sessionID;
3383
+ }
3384
+ })
3385
+ });
3386
+ const disposePanel = panelEnabled ? ctx.ui.slot({
3387
+ append: "session.panel",
3388
+ render: (input) => input.name === PANEL_NAME ? _$createComponent3(Panel, {
3389
+ input,
3390
+ ctx,
3391
+ deps: {
3392
+ db,
3393
+ dbPath: () => resolveDbPath(options.db),
3394
+ withWrite,
3395
+ now: () => Date.now(),
3396
+ sessionStart,
3397
+ refresh
3398
+ },
3399
+ settings,
3400
+ update: patchSettings,
3401
+ get format() {
3402
+ return {
3403
+ labels: options.labels,
3404
+ fields: options.fields,
3405
+ turnTotals: options.turnTotals
3406
+ };
3407
+ }
3408
+ }) : null
3409
+ }) : () => {};
3410
+ const disposeSidebar = ctx.ui.slot({
3411
+ append: "sidebar.content",
3412
+ render: (input) => options.sidebar === false ? null : _$createComponent3(TurnsSidebar, {
3413
+ get sessionID() {
3414
+ return input.sessionID;
3415
+ },
3416
+ ctx,
3417
+ get deps() {
3418
+ return {
3419
+ db,
3420
+ refresh,
3421
+ limit: Math.max(1, Math.round(options.sidebarTurns ?? 5))
3422
+ };
3423
+ },
3424
+ get options() {
3425
+ return {
3426
+ labels: options.labels,
3427
+ fields: options.fields,
3428
+ turnTotals: options.turnTotals
3429
+ };
3430
+ }
3431
+ })
3432
+ });
3433
+ function open(input) {
3434
+ const parsed = parseArgs(input);
3435
+ if (parsed.view !== undefined || parsed.range !== undefined) {
3436
+ patchSettings((draft) => {
3437
+ if (parsed.view !== undefined)
3438
+ draft.view = parsed.view;
3439
+ if (parsed.range !== undefined)
3440
+ draft.range = parsed.range;
3441
+ });
3442
+ }
3443
+ const tokens = (input ?? "").split(/\s+/).map((token) => token.trim()).filter((token) => token !== "" && !token.startsWith("/"));
3444
+ if (tokens.length === 0 && ctx.ui.panel.current()?.name === PANEL_NAME) {
3445
+ ctx.ui.panel.close();
3446
+ return true;
3447
+ }
3448
+ return ctx.ui.panel.open(PANEL_NAME);
3449
+ }
3450
+ if (panelEnabled) {
3451
+ ctx.keymap.layer(() => {
3452
+ return {
3453
+ mode: "global",
3454
+ commands: [{
3455
+ id: "usage-insights.stats",
3456
+ title: "Usage insights",
3457
+ description: "Open the usage statistics panel",
3458
+ group: "Usage insights",
3459
+ bind: false,
3460
+ palette: true,
3461
+ slash: {
3462
+ name: "usage",
3463
+ aliases: ["insights"],
3464
+ arguments: true
3465
+ },
3466
+ run: (input) => {
3467
+ if (!open(input)) {
3468
+ ctx.ui.toast.show({
3469
+ message: "Open a session first, then run /usage",
3470
+ variant: "warning"
3471
+ });
3472
+ }
3473
+ }
3474
+ }]
3475
+ };
3476
+ });
3477
+ }
3478
+ const disposeEvents = [
3479
+ ctx.data.on("session.step.ended", () => bumpRefresh()),
3480
+ ctx.data.on("session.execution.succeeded", () => bumpRefresh()),
3481
+ ctx.data.on("session.execution.failed", () => bumpRefresh()),
3482
+ ctx.data.on("session.execution.interrupted", () => bumpRefresh()),
3483
+ ctx.data.on("session.compaction.ended", () => bumpRefresh())
3484
+ ];
3485
+ __test.line = lineFor;
3486
+ __test.lineText = (sessionID) => lineText(lineParts(lineFor(sessionID), {
3487
+ labels: options.labels,
3488
+ fields: options.fields
3489
+ }));
3490
+ __test.open = open;
3491
+ __test.state = () => {
3492
+ const current = sanitizeSettings(settings);
3493
+ return {
3494
+ view: current.view,
3495
+ range: current.range
3496
+ };
3497
+ };
3498
+ return () => {
3499
+ if (refreshTimer !== undefined)
3500
+ clearTimeout(refreshTimer);
3501
+ for (const dispose of disposeEvents)
3502
+ dispose();
3503
+ disposeLine();
3504
+ disposePanel();
3505
+ disposeSidebar();
3506
+ database?.close();
3507
+ };
3508
+ }
3509
+ });
3510
+ export {
3511
+ __test,
3512
+ tui_default as default
3513
+ };
3514
+
3515
+ //# debugId=6DDD20AEAE1B401D64756E2164756E21