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/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/core.d.ts +259 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1227 -0
- package/dist/index.js.map +16 -0
- package/dist/line.d.ts +71 -0
- package/dist/panel-build.d.ts +46 -0
- package/dist/panel-data.d.ts +216 -0
- package/dist/panel.d.ts +39 -0
- package/dist/recorder.d.ts +69 -0
- package/dist/sidebar.d.ts +25 -0
- package/dist/stats/aggregates.d.ts +283 -0
- package/dist/stats/db.d.ts +99 -0
- package/dist/stats/maintenance.d.ts +49 -0
- package/dist/stats/paths.d.ts +35 -0
- package/dist/stats/queries.d.ts +239 -0
- package/dist/text.d.ts +25 -0
- package/dist/tui.d.ts +53 -0
- package/dist/tui.js +3515 -0
- package/dist/tui.js.map +22 -0
- package/package.json +84 -4
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
|