@agimon-ai/po-log 0.0.0-alpha.0
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 +66 -0
- package/dist/apiContracts.cjs +164 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +159 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/extensions/server.cjs +1034 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +1006 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +1542 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index-CTZMUfTZ.d.mts +185 -0
- package/dist/index-CTZMUfTZ.d.mts.map +1 -0
- package/dist/index.cjs +13 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/metrics-DywCpDJ_.mjs +508 -0
- package/dist/metrics-DywCpDJ_.mjs.map +1 -0
- package/dist/metrics-jCZe13Xg.cjs +573 -0
- package/dist/metrics-jCZe13Xg.cjs.map +1 -0
- package/dist/metrics.cjs +13 -0
- package/dist/metrics.d.mts +2 -0
- package/dist/metrics.mjs +2 -0
- package/dist/metricsSource-DXZLawQu.mjs +130 -0
- package/dist/metricsSource-DXZLawQu.mjs.map +1 -0
- package/dist/metricsSource-DdwQNzoY.cjs +135 -0
- package/dist/metricsSource-DdwQNzoY.cjs.map +1 -0
- package/dist/metricsSource-cZnErdv4.d.mts +51 -0
- package/dist/metricsSource-cZnErdv4.d.mts.map +1 -0
- package/dist/metricsSource.cjs +3 -0
- package/dist/metricsSource.d.mts +18 -0
- package/dist/metricsSource.d.mts.map +1 -0
- package/dist/metricsSource.mjs +2 -0
- package/llms.txt +7 -0
- package/package.json +140 -0
- package/src/prompts/po-debug-agent/SKILL.md +28 -0
|
@@ -0,0 +1,1542 @@
|
|
|
1
|
+
import { apiResponse, createApiClient, defineApiRoutes, defineSettingsPanel, defineToolRenderer, defineWebPlugin, sessionApiAddress } from "@agimon-ai/po-core/web";
|
|
2
|
+
import { Button, EmptyState, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, MessageLines, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Spinner, collapseLines, toolTone } from "@agimon-ai/po-web-components";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { sealedTransport } from "@agimon-ai/po-web-security/browser";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/types/webMetrics.ts
|
|
7
|
+
/**
|
|
8
|
+
* The dimensions the page offers.
|
|
9
|
+
*
|
|
10
|
+
* log-sink-mcp also groups by 'workflow-run', 'workflow-name', 'job' and
|
|
11
|
+
* 'step'. Those are omitted on purpose: no Po package emits the matching
|
|
12
|
+
* attributes today, so offering them would draw an empty chart and read as a
|
|
13
|
+
* bug rather than as a gap.
|
|
14
|
+
*/
|
|
15
|
+
const METRICS_DIMENSIONS = [
|
|
16
|
+
"session",
|
|
17
|
+
"agent",
|
|
18
|
+
"model",
|
|
19
|
+
"provider"
|
|
20
|
+
];
|
|
21
|
+
const METRICS_PERIODS = [
|
|
22
|
+
"day",
|
|
23
|
+
"week",
|
|
24
|
+
"month",
|
|
25
|
+
"all"
|
|
26
|
+
];
|
|
27
|
+
const METRICS_QUERY_PARAMS = {
|
|
28
|
+
dimension: "dimension",
|
|
29
|
+
period: "period",
|
|
30
|
+
/** Narrows the whole report to one value of the current dimension. */
|
|
31
|
+
focus: "focus"
|
|
32
|
+
};
|
|
33
|
+
function isMetricsUnavailable(response) {
|
|
34
|
+
return "unavailable" in response;
|
|
35
|
+
}
|
|
36
|
+
function isIssuesUnavailable(response) {
|
|
37
|
+
return "unavailable" in response;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/types/apiRoutes.ts
|
|
41
|
+
/**
|
|
42
|
+
* This package's routes, as data.
|
|
43
|
+
*
|
|
44
|
+
* No scope and no base path. The build reads both off
|
|
45
|
+
* `src/extensions/(backend)/api/log/`, so the mount is stated once, by the
|
|
46
|
+
* folder that creates it. They used to be written here, in the contract, and
|
|
47
|
+
* again inside every URL the page built, and the copies were free to disagree.
|
|
48
|
+
*
|
|
49
|
+
* This is the one file the routes, the Hono app and the browser all read, so a
|
|
50
|
+
* path cannot move on one side alone.
|
|
51
|
+
*/
|
|
52
|
+
var apiRoutes_default = defineApiRoutes({
|
|
53
|
+
metrics: {
|
|
54
|
+
method: "GET",
|
|
55
|
+
path: "/metrics",
|
|
56
|
+
query: [
|
|
57
|
+
METRICS_QUERY_PARAMS.dimension,
|
|
58
|
+
METRICS_QUERY_PARAMS.period,
|
|
59
|
+
METRICS_QUERY_PARAMS.focus
|
|
60
|
+
],
|
|
61
|
+
response: apiResponse()
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Separate from the report because the hub answers it from a subprocess: the
|
|
65
|
+
* running daemon exposes no issues route, so folding it in would make every
|
|
66
|
+
* refresh wait on the slowest transport.
|
|
67
|
+
*/
|
|
68
|
+
issues: {
|
|
69
|
+
method: "GET",
|
|
70
|
+
path: "/issues",
|
|
71
|
+
query: [METRICS_QUERY_PARAMS.focus],
|
|
72
|
+
response: apiResponse()
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const api = createApiClient(apiRoutes_default, {
|
|
76
|
+
scopes: [
|
|
77
|
+
"global",
|
|
78
|
+
"workspace",
|
|
79
|
+
"session"
|
|
80
|
+
],
|
|
81
|
+
basePath: "log",
|
|
82
|
+
transport: (input, init) => sealedTransport.fetch(input, init),
|
|
83
|
+
sessionAddress: sessionApiAddress
|
|
84
|
+
});
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/extensions/(frontend)/setting/_lib/metricsApi.ts
|
|
87
|
+
/**
|
|
88
|
+
* The page's half of this package's metrics API. The only place the cockpit
|
|
89
|
+
* talks to the hub for metrics, so if the transport ever changes, it changes
|
|
90
|
+
* here alone.
|
|
91
|
+
*
|
|
92
|
+
* These are adapters now, not a transport. The generated client owns the URL
|
|
93
|
+
* and the sealed transport with it, so nothing here spells a route or reaches
|
|
94
|
+
* for `fetch`; a plugin calling `fetch` directly would send plaintext to the
|
|
95
|
+
* tunnel's relay. What stays is this package's own vocabulary: which of the
|
|
96
|
+
* empty states a reader is shown, and which answers are states rather than
|
|
97
|
+
* failures.
|
|
98
|
+
*/
|
|
99
|
+
const UNREACHABLE = "The cockpit hub is unreachable.";
|
|
100
|
+
/**
|
|
101
|
+
* A hub with no package APIs mounted serves the SPA shell for this route, so
|
|
102
|
+
* the answer is a 200 of HTML rather than an error status. The client parses no
|
|
103
|
+
* JSON out of it and reports no body at all, which is read here as an
|
|
104
|
+
* uninstalled feature, because that is what it is.
|
|
105
|
+
*/
|
|
106
|
+
const NO_API_DETAIL = "This cockpit is running a bundle without the log package API, so there are no metrics to read.";
|
|
107
|
+
const NO_API = {
|
|
108
|
+
unavailable: "no-api",
|
|
109
|
+
detail: NO_API_DETAIL
|
|
110
|
+
};
|
|
111
|
+
function isRecord(value) {
|
|
112
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* What the hub's answer means, before either route reads its own shape out of
|
|
116
|
+
* it.
|
|
117
|
+
*
|
|
118
|
+
* `status: 0` is the transport never answering, which covers both a dead hub
|
|
119
|
+
* and a caller that aborted. The client cannot tell those apart, so the signal
|
|
120
|
+
* is asked: an abort is the caller replacing this request, and reporting it
|
|
121
|
+
* would overwrite whatever the replacement renders.
|
|
122
|
+
*/
|
|
123
|
+
function outcomeOf(result, signal) {
|
|
124
|
+
if (result.status === 0) return { error: signal?.aborted === true ? "" : UNREACHABLE };
|
|
125
|
+
const status = `The hub answered ${String(result.status)}.`;
|
|
126
|
+
if (result.data === void 0) return result.ok ? { noApi: true } : { error: status };
|
|
127
|
+
if (!isRecord(result.data)) return { error: "The hub returned a response the cockpit could not read." };
|
|
128
|
+
if (!result.ok) return { error: result.error === "" ? status : result.error };
|
|
129
|
+
return { body: result.data };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Reads one report.
|
|
133
|
+
*
|
|
134
|
+
* A missing sink is not an error here: the route answers it as an
|
|
135
|
+
* `unavailable` body, so the page can name which of the empty states it is in
|
|
136
|
+
* rather than showing one generic failure.
|
|
137
|
+
*/
|
|
138
|
+
async function fetchMetrics(dimension, period, focus, signal) {
|
|
139
|
+
const outcome = outcomeOf(await api.global.metrics({
|
|
140
|
+
query: {
|
|
141
|
+
[METRICS_QUERY_PARAMS.dimension]: dimension,
|
|
142
|
+
[METRICS_QUERY_PARAMS.period]: period,
|
|
143
|
+
[METRICS_QUERY_PARAMS.focus]: focus === "" ? void 0 : focus
|
|
144
|
+
},
|
|
145
|
+
...signal === void 0 ? {} : { signal }
|
|
146
|
+
}), signal);
|
|
147
|
+
if ("error" in outcome) return { error: outcome.error };
|
|
148
|
+
if ("noApi" in outcome) return { report: NO_API };
|
|
149
|
+
const report = outcome.body;
|
|
150
|
+
if (!isMetricsUnavailable(report) && !Array.isArray(report.groups)) return { error: "The hub returned a report the cockpit could not read." };
|
|
151
|
+
return { report };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Reads the detail behind the issue count.
|
|
155
|
+
*
|
|
156
|
+
* Separate call because the hub answers it from a subprocess: folding it into
|
|
157
|
+
* the report would make every refresh wait on the slowest transport.
|
|
158
|
+
*/
|
|
159
|
+
async function fetchIssues(focus, signal, window = {}) {
|
|
160
|
+
const outcome = outcomeOf(await api.global.issues({
|
|
161
|
+
query: {
|
|
162
|
+
[METRICS_QUERY_PARAMS.focus]: focus === "" ? void 0 : focus,
|
|
163
|
+
...window
|
|
164
|
+
},
|
|
165
|
+
...signal === void 0 ? {} : { signal }
|
|
166
|
+
}), signal);
|
|
167
|
+
if ("error" in outcome) return { error: outcome.error };
|
|
168
|
+
if ("noApi" in outcome) return { issues: {
|
|
169
|
+
unavailable: "no-api",
|
|
170
|
+
detail: NO_API_DETAIL
|
|
171
|
+
} };
|
|
172
|
+
return { issues: outcome.body };
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/extensions/(frontend)/setting/_lib/chartScale.ts
|
|
176
|
+
/**
|
|
177
|
+
* The arithmetic behind the charts, kept out of the components so it can be
|
|
178
|
+
* tested as plain functions rather than through a render.
|
|
179
|
+
*
|
|
180
|
+
* There is no charting library in the cockpit bundle and the plugin import
|
|
181
|
+
* allowlist would not admit one, so these are the primitives the SVG is drawn
|
|
182
|
+
* from. They are deliberately small: a bar length and a tick set, nothing that
|
|
183
|
+
* pretends to be a plotting engine.
|
|
184
|
+
*/
|
|
185
|
+
/** Bar length as a fraction of the track, guarding the all-zero series. */
|
|
186
|
+
function barFraction(value, max) {
|
|
187
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
188
|
+
if (!Number.isFinite(max) || max <= 0) return 0;
|
|
189
|
+
return Math.min(1, value / max);
|
|
190
|
+
}
|
|
191
|
+
/** The largest value in a series, or 0 for an empty one. */
|
|
192
|
+
function seriesMax(values) {
|
|
193
|
+
return values.reduce((highest, value) => Number.isFinite(value) && value > highest ? value : highest, 0);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Compact token counts. Charts label axes and bars in a few characters, and a
|
|
197
|
+
* raw nine-digit total makes every row the same illegible width.
|
|
198
|
+
*/
|
|
199
|
+
function formatTokens(value) {
|
|
200
|
+
if (!Number.isFinite(value)) return "Not reported";
|
|
201
|
+
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
202
|
+
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
|
|
203
|
+
return String(Math.round(value));
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/extensions/(frontend)/setting/_components/charts/GroupBars.tsx
|
|
207
|
+
const PREVIEW_GROUPS = 5;
|
|
208
|
+
/** Rank recorded consumers; an unknown attribution is not a selectable model. */
|
|
209
|
+
function GroupBars({ groups, totalTokens, focus, onFocus }) {
|
|
210
|
+
const [expanded, setExpanded] = useState(false);
|
|
211
|
+
const sorted = [...groups].sort((a, b) => b.totalTokens - a.totalTokens || a.key.localeCompare(b.key));
|
|
212
|
+
const rows = expanded ? sorted : sorted.slice(0, PREVIEW_GROUPS);
|
|
213
|
+
const max = totalTokens ?? seriesMax(groups.map((group) => group.totalTokens));
|
|
214
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
215
|
+
className: "flex flex-col gap-3",
|
|
216
|
+
children: [
|
|
217
|
+
groups.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
218
|
+
className: "rounded-md border border-dashed border-doom-border p-5 text-sm text-doom-dim",
|
|
219
|
+
children: "No grouped usage was recorded for this selection."
|
|
220
|
+
}) : null,
|
|
221
|
+
/* @__PURE__ */ jsx("ul", {
|
|
222
|
+
className: "flex flex-col gap-1",
|
|
223
|
+
"data-testid": "metrics-group-bars",
|
|
224
|
+
children: rows.map((group) => {
|
|
225
|
+
const selected = focus === group.key;
|
|
226
|
+
const unknown = group.key === "unknown" || group.key === "";
|
|
227
|
+
const canFocus = onFocus !== void 0 && !unknown;
|
|
228
|
+
const percentage = barFraction(group.totalTokens, max) * 100;
|
|
229
|
+
const row = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
230
|
+
/* @__PURE__ */ jsxs("span", {
|
|
231
|
+
className: "flex items-start justify-between gap-3",
|
|
232
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
233
|
+
className: "min-w-0 break-all text-left font-medium text-doom-hi",
|
|
234
|
+
title: group.key,
|
|
235
|
+
children: unknown ? "Unattributed" : group.key
|
|
236
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
237
|
+
className: "shrink-0 text-right font-semibold tabular-nums text-doom-hi",
|
|
238
|
+
title: String(group.totalTokens),
|
|
239
|
+
children: formatTokens(group.totalTokens)
|
|
240
|
+
})]
|
|
241
|
+
}),
|
|
242
|
+
/* @__PURE__ */ jsx("span", {
|
|
243
|
+
"aria-hidden": "true",
|
|
244
|
+
className: "mt-2 block h-1.5 overflow-hidden rounded-full bg-doom-border",
|
|
245
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
246
|
+
className: `block h-full rounded-full ${unknown ? "bg-doom-yellow" : "bg-doom-blue"}`,
|
|
247
|
+
style: { width: `${percentage}%` }
|
|
248
|
+
})
|
|
249
|
+
}),
|
|
250
|
+
/* @__PURE__ */ jsxs("span", {
|
|
251
|
+
className: "mt-2 flex flex-wrap items-center justify-between gap-2 text-sm text-doom-dim",
|
|
252
|
+
children: [/* @__PURE__ */ jsx("span", { children: totalTokens !== void 0 && max > 0 ? `${percentage.toFixed(1)}% of total` : "Relative token volume" }), /* @__PURE__ */ jsxs("span", {
|
|
253
|
+
className: group.issueCount > 0 || group.failed ? "text-doom-yellow" : "text-doom-dim",
|
|
254
|
+
children: [
|
|
255
|
+
group.failed ? "Failed · " : "",
|
|
256
|
+
formatTokens(group.issueCount),
|
|
257
|
+
" ",
|
|
258
|
+
group.issueCount === 1 ? "issue" : "issues"
|
|
259
|
+
]
|
|
260
|
+
})]
|
|
261
|
+
})
|
|
262
|
+
] });
|
|
263
|
+
return /* @__PURE__ */ jsx("li", { children: canFocus ? /* @__PURE__ */ jsx("button", {
|
|
264
|
+
type: "button",
|
|
265
|
+
onClick: () => onFocus?.(selected ? "" : group.key),
|
|
266
|
+
"aria-pressed": selected,
|
|
267
|
+
"aria-label": `Filter to ${group.key}`,
|
|
268
|
+
"data-testid": `metrics-group-${group.key}`,
|
|
269
|
+
className: `block w-full rounded-md p-3 text-base hover:bg-doom-tint-blue focus-visible:outline focus-visible:outline-1 focus-visible:outline-doom-blue ${selected ? "bg-doom-tint-blue" : ""}`,
|
|
270
|
+
children: row
|
|
271
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
272
|
+
className: "rounded-md p-3 text-base",
|
|
273
|
+
children: row
|
|
274
|
+
}) }, group.key);
|
|
275
|
+
})
|
|
276
|
+
}),
|
|
277
|
+
groups.length > PREVIEW_GROUPS ? /* @__PURE__ */ jsx(Button, {
|
|
278
|
+
variant: "ghost",
|
|
279
|
+
size: "sm",
|
|
280
|
+
onClick: () => setExpanded(!expanded),
|
|
281
|
+
"aria-expanded": expanded,
|
|
282
|
+
children: expanded ? "Show top 5" : `Show all ${groups.length} returned groups`
|
|
283
|
+
}) : null
|
|
284
|
+
]
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/extensions/(frontend)/setting/_components/charts/TimelineChart.tsx
|
|
289
|
+
const SERIES = [
|
|
290
|
+
{
|
|
291
|
+
key: "totalTokens",
|
|
292
|
+
label: "Total"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
key: "inputTokens",
|
|
296
|
+
label: "Input"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
key: "outputTokens",
|
|
300
|
+
label: "Output"
|
|
301
|
+
}
|
|
302
|
+
];
|
|
303
|
+
const PLOT_WIDTH = 600;
|
|
304
|
+
const PLOT_HEIGHT = 160;
|
|
305
|
+
const MAX_COLUMN_WIDTH = 48;
|
|
306
|
+
/** Plot the sink's buckets without filling missing history or stacking counters. */
|
|
307
|
+
function TimelineChart({ buckets, bucketUnit, onShorterPeriod }) {
|
|
308
|
+
const [series, setSeries] = useState("totalTokens");
|
|
309
|
+
const [activeLabel, setActiveLabel] = useState("");
|
|
310
|
+
const active = buckets.find((bucket) => bucket.label === activeLabel) ?? buckets.at(-1);
|
|
311
|
+
const max = seriesMax(buckets.map((bucket) => bucket[series]));
|
|
312
|
+
const unit = bucketUnit ? `${bucketUnit} ` : "";
|
|
313
|
+
const slot = PLOT_WIDTH / Math.max(1, buckets.length);
|
|
314
|
+
const width = Math.min(MAX_COLUMN_WIDTH, slot * .65);
|
|
315
|
+
if (buckets.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
316
|
+
className: "rounded-md border border-dashed border-doom-border p-5 text-sm text-doom-dim",
|
|
317
|
+
"data-testid": "metrics-timeline-chart",
|
|
318
|
+
children: "No timeline buckets were recorded for this selection."
|
|
319
|
+
});
|
|
320
|
+
if (buckets.length === 1 && active !== void 0) return /* @__PURE__ */ jsx("div", {
|
|
321
|
+
className: "flex flex-col gap-4",
|
|
322
|
+
"data-testid": "metrics-timeline-chart",
|
|
323
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
324
|
+
className: "rounded-md border border-doom-border bg-doom-deep p-5",
|
|
325
|
+
children: [
|
|
326
|
+
/* @__PURE__ */ jsx("p", {
|
|
327
|
+
className: "text-sm text-doom-dim",
|
|
328
|
+
children: active.label
|
|
329
|
+
}),
|
|
330
|
+
/* @__PURE__ */ jsxs("p", {
|
|
331
|
+
className: "mt-2 text-lg font-semibold tabular-nums text-doom-blue",
|
|
332
|
+
title: String(active.totalTokens),
|
|
333
|
+
children: [formatTokens(active.totalTokens), " tokens"]
|
|
334
|
+
}),
|
|
335
|
+
/* @__PURE__ */ jsxs("p", {
|
|
336
|
+
className: "mt-3 text-sm leading-relaxed text-doom-dim",
|
|
337
|
+
children: [
|
|
338
|
+
"Only one ",
|
|
339
|
+
unit,
|
|
340
|
+
"bucket was recorded. There is not enough history to show a trend."
|
|
341
|
+
]
|
|
342
|
+
}),
|
|
343
|
+
/* @__PURE__ */ jsx("p", {
|
|
344
|
+
className: "mt-2 text-sm text-doom-dim",
|
|
345
|
+
children: "For more detail, pick a shorter period when one is available."
|
|
346
|
+
}),
|
|
347
|
+
onShorterPeriod === void 0 ? null : /* @__PURE__ */ jsx(Button, {
|
|
348
|
+
variant: "ghost",
|
|
349
|
+
size: "sm",
|
|
350
|
+
className: "mt-3",
|
|
351
|
+
onClick: onShorterPeriod,
|
|
352
|
+
children: "View hourly detail"
|
|
353
|
+
})
|
|
354
|
+
]
|
|
355
|
+
})
|
|
356
|
+
});
|
|
357
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
358
|
+
className: "flex flex-col gap-3",
|
|
359
|
+
"data-testid": "metrics-timeline-chart",
|
|
360
|
+
children: [
|
|
361
|
+
/* @__PURE__ */ jsxs("div", {
|
|
362
|
+
className: "flex flex-wrap items-center justify-between gap-2",
|
|
363
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
364
|
+
className: "flex gap-1",
|
|
365
|
+
role: "group",
|
|
366
|
+
"aria-label": "Timeline token counter",
|
|
367
|
+
children: SERIES.map((item) => /* @__PURE__ */ jsx(Button, {
|
|
368
|
+
size: "sm",
|
|
369
|
+
variant: series === item.key ? "primary" : "ghost",
|
|
370
|
+
"aria-pressed": series === item.key,
|
|
371
|
+
onClick: () => setSeries(item.key),
|
|
372
|
+
children: item.label
|
|
373
|
+
}, item.key))
|
|
374
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
375
|
+
className: "text-sm text-doom-dim",
|
|
376
|
+
children: [
|
|
377
|
+
buckets.length,
|
|
378
|
+
" recorded ",
|
|
379
|
+
unit,
|
|
380
|
+
"buckets"
|
|
381
|
+
]
|
|
382
|
+
})]
|
|
383
|
+
}),
|
|
384
|
+
/* @__PURE__ */ jsxs("div", {
|
|
385
|
+
className: "flex gap-3",
|
|
386
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
387
|
+
"aria-hidden": "true",
|
|
388
|
+
className: "flex w-12 shrink-0 flex-col justify-between text-right text-sm tabular-nums text-doom-dim",
|
|
389
|
+
children: [
|
|
390
|
+
/* @__PURE__ */ jsx("span", { children: formatTokens(max) }),
|
|
391
|
+
/* @__PURE__ */ jsx("span", { children: formatTokens(max / 2) }),
|
|
392
|
+
/* @__PURE__ */ jsx("span", { children: "0" })
|
|
393
|
+
]
|
|
394
|
+
}), /* @__PURE__ */ jsxs("svg", {
|
|
395
|
+
viewBox: `0 0 ${PLOT_WIDTH} ${PLOT_HEIGHT}`,
|
|
396
|
+
preserveAspectRatio: "none",
|
|
397
|
+
className: "h-40 min-w-0 flex-1 overflow-visible",
|
|
398
|
+
role: "group",
|
|
399
|
+
"aria-label": `tokens per bucket, peak ${formatTokens(max)}`,
|
|
400
|
+
children: [[
|
|
401
|
+
0,
|
|
402
|
+
PLOT_HEIGHT / 2,
|
|
403
|
+
PLOT_HEIGHT
|
|
404
|
+
].map((y) => /* @__PURE__ */ jsx("line", {
|
|
405
|
+
x1: "0",
|
|
406
|
+
x2: PLOT_WIDTH,
|
|
407
|
+
y1: y,
|
|
408
|
+
y2: y,
|
|
409
|
+
className: "text-doom-border",
|
|
410
|
+
stroke: "currentColor",
|
|
411
|
+
vectorEffect: "non-scaling-stroke"
|
|
412
|
+
}, y)), buckets.map((bucket, index) => {
|
|
413
|
+
const height = barFraction(bucket[series], max) * PLOT_HEIGHT;
|
|
414
|
+
const label = `${bucket.label}: ${formatTokens(bucket[series])} ${SERIES.find((item) => item.key === series)?.label.toLowerCase()} tokens`;
|
|
415
|
+
return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("rect", {
|
|
416
|
+
x: index * slot + (slot - width) / 2,
|
|
417
|
+
y: PLOT_HEIGHT - height,
|
|
418
|
+
width,
|
|
419
|
+
height,
|
|
420
|
+
rx: "2",
|
|
421
|
+
fill: "currentColor",
|
|
422
|
+
className: active?.label === bucket.label ? "text-doom-cyan" : "text-doom-blue"
|
|
423
|
+
}), /* @__PURE__ */ jsx("rect", {
|
|
424
|
+
x: index * slot,
|
|
425
|
+
y: "0",
|
|
426
|
+
width: slot,
|
|
427
|
+
height: PLOT_HEIGHT,
|
|
428
|
+
fill: "transparent",
|
|
429
|
+
tabIndex: 0,
|
|
430
|
+
role: "button",
|
|
431
|
+
"aria-label": label,
|
|
432
|
+
"aria-pressed": active?.label === bucket.label,
|
|
433
|
+
onMouseEnter: () => setActiveLabel(bucket.label),
|
|
434
|
+
onFocus: () => setActiveLabel(bucket.label),
|
|
435
|
+
onClick: () => setActiveLabel(bucket.label),
|
|
436
|
+
onKeyDown: (event) => {
|
|
437
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
438
|
+
event.preventDefault();
|
|
439
|
+
setActiveLabel(bucket.label);
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
className: "cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-doom-cyan",
|
|
443
|
+
children: /* @__PURE__ */ jsx("title", { children: label })
|
|
444
|
+
})] }, bucket.label);
|
|
445
|
+
})]
|
|
446
|
+
})]
|
|
447
|
+
}),
|
|
448
|
+
/* @__PURE__ */ jsxs("div", {
|
|
449
|
+
className: "ml-15 flex justify-between gap-3 text-sm text-doom-dim",
|
|
450
|
+
children: [/* @__PURE__ */ jsx("span", { children: buckets[0]?.label }), /* @__PURE__ */ jsx("span", {
|
|
451
|
+
className: "text-right",
|
|
452
|
+
children: buckets.at(-1)?.label
|
|
453
|
+
})]
|
|
454
|
+
}),
|
|
455
|
+
/* @__PURE__ */ jsxs("div", {
|
|
456
|
+
className: "flex flex-wrap justify-between gap-2 rounded-md bg-doom-deep px-3 py-2 text-sm tabular-nums",
|
|
457
|
+
"aria-live": "polite",
|
|
458
|
+
"aria-atomic": "true",
|
|
459
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
460
|
+
className: "text-doom-dim",
|
|
461
|
+
children: active?.label
|
|
462
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
463
|
+
className: "text-doom-hi",
|
|
464
|
+
children: [
|
|
465
|
+
formatTokens(active?.[series] ?? NaN),
|
|
466
|
+
" ",
|
|
467
|
+
SERIES.find((item) => item.key === series)?.label.toLowerCase(),
|
|
468
|
+
" tokens"
|
|
469
|
+
]
|
|
470
|
+
})]
|
|
471
|
+
}),
|
|
472
|
+
max === 0 ? /* @__PURE__ */ jsxs("p", {
|
|
473
|
+
className: "text-sm text-doom-dim",
|
|
474
|
+
children: [
|
|
475
|
+
"Recorded buckets contain zero ",
|
|
476
|
+
SERIES.find((item) => item.key === series)?.label.toLowerCase(),
|
|
477
|
+
" tokens."
|
|
478
|
+
]
|
|
479
|
+
}) : null,
|
|
480
|
+
/* @__PURE__ */ jsxs("details", {
|
|
481
|
+
className: "text-sm text-doom-dim",
|
|
482
|
+
children: [/* @__PURE__ */ jsx("summary", {
|
|
483
|
+
className: "cursor-pointer rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-doom-blue",
|
|
484
|
+
children: "View exact bucket values"
|
|
485
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
486
|
+
className: "mt-3 overflow-x-auto",
|
|
487
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
488
|
+
className: "w-full text-right tabular-nums",
|
|
489
|
+
children: [
|
|
490
|
+
/* @__PURE__ */ jsx("caption", {
|
|
491
|
+
className: "sr-only",
|
|
492
|
+
children: "Independent token counters by recorded bucket"
|
|
493
|
+
}),
|
|
494
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
495
|
+
/* @__PURE__ */ jsx("th", {
|
|
496
|
+
scope: "col",
|
|
497
|
+
className: "py-2 text-left",
|
|
498
|
+
children: "Bucket"
|
|
499
|
+
}),
|
|
500
|
+
/* @__PURE__ */ jsx("th", {
|
|
501
|
+
scope: "col",
|
|
502
|
+
className: "pl-3",
|
|
503
|
+
children: "Total"
|
|
504
|
+
}),
|
|
505
|
+
/* @__PURE__ */ jsx("th", {
|
|
506
|
+
scope: "col",
|
|
507
|
+
className: "pl-3",
|
|
508
|
+
children: "Input"
|
|
509
|
+
}),
|
|
510
|
+
/* @__PURE__ */ jsx("th", {
|
|
511
|
+
scope: "col",
|
|
512
|
+
className: "pl-3",
|
|
513
|
+
children: "Output"
|
|
514
|
+
})
|
|
515
|
+
] }) }),
|
|
516
|
+
/* @__PURE__ */ jsx("tbody", { children: buckets.map((bucket) => /* @__PURE__ */ jsxs("tr", {
|
|
517
|
+
className: "border-t border-doom-border",
|
|
518
|
+
children: [/* @__PURE__ */ jsx("th", {
|
|
519
|
+
scope: "row",
|
|
520
|
+
className: "py-2 text-left font-normal",
|
|
521
|
+
children: bucket.label
|
|
522
|
+
}), SERIES.map((item) => /* @__PURE__ */ jsx("td", {
|
|
523
|
+
className: "pl-3",
|
|
524
|
+
children: Number.isFinite(bucket[item.key]) ? bucket[item.key].toLocaleString("en-US") : "Not reported"
|
|
525
|
+
}, item.key))]
|
|
526
|
+
}, bucket.label)) })
|
|
527
|
+
]
|
|
528
|
+
})
|
|
529
|
+
})]
|
|
530
|
+
})
|
|
531
|
+
]
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/types/issueGrouping.ts
|
|
536
|
+
/**
|
|
537
|
+
* A key that separates problems a reader would act on separately.
|
|
538
|
+
*
|
|
539
|
+
* The sink's fingerprint is the primary key, but it can be empty, and several
|
|
540
|
+
* distinct bash failures share 'tool_failure|pi|bash||pi.tool_result' because
|
|
541
|
+
* the record name is all they carry. Folding the detail in keeps two different
|
|
542
|
+
* failures apart while still merging repeats of the same one.
|
|
543
|
+
*/
|
|
544
|
+
function groupingKey(sample) {
|
|
545
|
+
return `${sample.fingerprint === "" ? `${sample.category}|${sample.message}` : sample.fingerprint}|${sample.detail}`;
|
|
546
|
+
}
|
|
547
|
+
/** Incidents merged by problem and ranked by how often each happened. */
|
|
548
|
+
function groupIssues(samples) {
|
|
549
|
+
const groups = /* @__PURE__ */ new Map();
|
|
550
|
+
for (const sample of samples) {
|
|
551
|
+
const key = groupingKey(sample);
|
|
552
|
+
const existing = groups.get(key);
|
|
553
|
+
if (existing === void 0) {
|
|
554
|
+
groups.set(key, {
|
|
555
|
+
key,
|
|
556
|
+
occurrences: sample.occurrenceCount,
|
|
557
|
+
category: sample.category,
|
|
558
|
+
detail: sample.detail,
|
|
559
|
+
tool: sample.tool,
|
|
560
|
+
errorType: sample.errorType,
|
|
561
|
+
agentName: sample.agentName,
|
|
562
|
+
model: sample.model,
|
|
563
|
+
statusCode: sample.statusCode,
|
|
564
|
+
lastSeen: sample.timestamp,
|
|
565
|
+
members: [sample]
|
|
566
|
+
});
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
existing.occurrences += sample.occurrenceCount;
|
|
570
|
+
existing.members.push(sample);
|
|
571
|
+
if (sample.timestamp > existing.lastSeen) existing.lastSeen = sample.timestamp;
|
|
572
|
+
existing.tool ??= sample.tool;
|
|
573
|
+
existing.errorType ??= sample.errorType;
|
|
574
|
+
existing.agentName ??= sample.agentName;
|
|
575
|
+
existing.model ??= sample.model;
|
|
576
|
+
existing.statusCode ??= sample.statusCode;
|
|
577
|
+
}
|
|
578
|
+
return [...groups.values()].sort((left, right) => right.occurrences - left.occurrences || left.detail.localeCompare(right.detail));
|
|
579
|
+
}
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/extensions/(frontend)/setting/_components/IssuesDetail.tsx
|
|
582
|
+
/**
|
|
583
|
+
* What is actually going wrong, ranked by how often.
|
|
584
|
+
*
|
|
585
|
+
* A count of 69 issues tells nobody what to change. The same 69 collapsed into
|
|
586
|
+
* "this spawn failed 20 times, this hook failed 3" is a work list, so the
|
|
587
|
+
* ranked bar is the primary view here and the totals are context beside it.
|
|
588
|
+
*
|
|
589
|
+
* Each bar expands to the incidents behind it, because the row states the
|
|
590
|
+
* problem and the reader still needs the session, model, and timestamps to go
|
|
591
|
+
* and look at one.
|
|
592
|
+
*/
|
|
593
|
+
function countRows(counts) {
|
|
594
|
+
return Object.entries(counts).sort(([, left], [, right]) => right - left);
|
|
595
|
+
}
|
|
596
|
+
/** A short, human label for a problem; the detail can be a whole command line. */
|
|
597
|
+
function titleOf(group) {
|
|
598
|
+
if (group.errorType !== null) return group.errorType;
|
|
599
|
+
if (group.tool !== null) return `${group.tool} failed`;
|
|
600
|
+
return group.category;
|
|
601
|
+
}
|
|
602
|
+
function IssueRow({ group, max, tools }) {
|
|
603
|
+
const [open, setOpen] = useState(false);
|
|
604
|
+
const width = `${(barFraction(group.occurrences, max) * 100).toFixed(2)}%`;
|
|
605
|
+
const calls = tools.find((tool) => tool.name === group.tool)?.calls;
|
|
606
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
607
|
+
className: "flex flex-col rounded-md border border-doom-border",
|
|
608
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
609
|
+
className: "relative",
|
|
610
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
611
|
+
"aria-hidden": "true",
|
|
612
|
+
className: "absolute bottom-0 left-0 h-1 rounded-xs bg-doom-yellow/40",
|
|
613
|
+
style: { width }
|
|
614
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
615
|
+
type: "button",
|
|
616
|
+
onClick: () => setOpen(!open),
|
|
617
|
+
"aria-expanded": open,
|
|
618
|
+
"data-testid": `metrics-issue-${group.key}`,
|
|
619
|
+
className: "relative flex min-h-10 w-full flex-wrap items-start gap-3 rounded-md p-3 text-left text-base hover:bg-doom-tint-blue focus-visible:outline focus-visible:outline-1 focus-visible:outline-doom-blue",
|
|
620
|
+
children: [
|
|
621
|
+
/* @__PURE__ */ jsx("span", {
|
|
622
|
+
className: "w-8 shrink-0 text-right font-semibold text-doom-yellow",
|
|
623
|
+
children: group.occurrences
|
|
624
|
+
}),
|
|
625
|
+
/* @__PURE__ */ jsx("span", {
|
|
626
|
+
className: "w-24 shrink-0 break-words font-medium text-doom-hi",
|
|
627
|
+
children: titleOf(group)
|
|
628
|
+
}),
|
|
629
|
+
/* @__PURE__ */ jsx("span", {
|
|
630
|
+
className: "min-w-40 flex-1 break-words text-doom-dim",
|
|
631
|
+
children: group.detail
|
|
632
|
+
}),
|
|
633
|
+
calls === void 0 ? null : /* @__PURE__ */ jsxs("span", {
|
|
634
|
+
className: "shrink-0 text-doom-dim",
|
|
635
|
+
children: [calls, " token samples"]
|
|
636
|
+
})
|
|
637
|
+
]
|
|
638
|
+
})]
|
|
639
|
+
}), open ? /* @__PURE__ */ jsxs("div", {
|
|
640
|
+
className: "flex flex-col gap-1 px-3 py-3 text-base",
|
|
641
|
+
"data-testid": `metrics-issue-body-${group.key}`,
|
|
642
|
+
children: [
|
|
643
|
+
/* @__PURE__ */ jsxs("div", {
|
|
644
|
+
className: "flex flex-wrap gap-x-3 gap-y-1 text-sm text-doom-dim",
|
|
645
|
+
children: [
|
|
646
|
+
/* @__PURE__ */ jsxs("span", { children: ["category ", group.category] }),
|
|
647
|
+
group.tool === null ? null : /* @__PURE__ */ jsxs("span", { children: ["tool ", group.tool] }),
|
|
648
|
+
group.errorType === null ? null : /* @__PURE__ */ jsxs("span", { children: ["error ", group.errorType] }),
|
|
649
|
+
group.statusCode === null ? null : /* @__PURE__ */ jsxs("span", { children: ["status ", group.statusCode] }),
|
|
650
|
+
group.agentName === null ? null : /* @__PURE__ */ jsxs("span", { children: ["agent ", group.agentName] }),
|
|
651
|
+
group.model === null ? null : /* @__PURE__ */ jsxs("span", { children: ["model ", group.model] }),
|
|
652
|
+
/* @__PURE__ */ jsxs("span", { children: ["last seen ", group.lastSeen] })
|
|
653
|
+
]
|
|
654
|
+
}),
|
|
655
|
+
/* @__PURE__ */ jsx("span", {
|
|
656
|
+
className: "break-words text-doom-dim",
|
|
657
|
+
children: group.detail
|
|
658
|
+
}),
|
|
659
|
+
/* @__PURE__ */ jsx("ul", {
|
|
660
|
+
className: "flex flex-col gap-1 text-sm text-doom-dim",
|
|
661
|
+
children: group.members.map((member, index) => /* @__PURE__ */ jsxs("li", {
|
|
662
|
+
className: "flex flex-wrap gap-x-2",
|
|
663
|
+
children: [
|
|
664
|
+
/* @__PURE__ */ jsx("span", { children: member.timestamp }),
|
|
665
|
+
/* @__PURE__ */ jsx("span", { children: member.level }),
|
|
666
|
+
/* @__PURE__ */ jsxs("span", { children: [member.occurrenceCount, "×"] }),
|
|
667
|
+
/* @__PURE__ */ jsx("span", {
|
|
668
|
+
className: "min-w-0 truncate",
|
|
669
|
+
children: member.message
|
|
670
|
+
})
|
|
671
|
+
]
|
|
672
|
+
}, `${member.timestamp}-${String(index)}`))
|
|
673
|
+
})
|
|
674
|
+
]
|
|
675
|
+
}) : null]
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function IssuesDetail({ view, tools, summaryCount }) {
|
|
679
|
+
const groups = groupIssues(view.samples);
|
|
680
|
+
const max = groups[0]?.occurrences ?? 0;
|
|
681
|
+
const callsByTool = new Map(tools.map((tool) => [tool.name, tool.calls]));
|
|
682
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
683
|
+
className: "flex flex-col gap-3",
|
|
684
|
+
children: [
|
|
685
|
+
summaryCount !== void 0 && summaryCount !== view.totalIssues ? /* @__PURE__ */ jsxs("p", {
|
|
686
|
+
className: "rounded-md border border-doom-edge-yellow bg-doom-tint-yellow p-3 text-sm leading-relaxed text-doom-yellow",
|
|
687
|
+
role: "status",
|
|
688
|
+
"data-testid": "metrics-issue-count-mismatch",
|
|
689
|
+
children: [
|
|
690
|
+
"The summary reports ",
|
|
691
|
+
summaryCount,
|
|
692
|
+
" ",
|
|
693
|
+
summaryCount === 1 ? "issue" : "issues",
|
|
694
|
+
"; incident analysis returned",
|
|
695
|
+
" ",
|
|
696
|
+
view.totalIssues,
|
|
697
|
+
". The counts differ, so these details should not be treated as a complete explanation of the summary."
|
|
698
|
+
]
|
|
699
|
+
}) : null,
|
|
700
|
+
/* @__PURE__ */ jsxs("span", {
|
|
701
|
+
className: "text-sm text-doom-dim",
|
|
702
|
+
children: [
|
|
703
|
+
/* @__PURE__ */ jsx("span", {
|
|
704
|
+
className: "text-doom-hi",
|
|
705
|
+
children: view.totalIssues
|
|
706
|
+
}),
|
|
707
|
+
" occurrences of",
|
|
708
|
+
" ",
|
|
709
|
+
/* @__PURE__ */ jsx("span", {
|
|
710
|
+
className: "text-doom-hi",
|
|
711
|
+
children: groups.length
|
|
712
|
+
}),
|
|
713
|
+
" distinct problems in the returned samples, most frequent first"
|
|
714
|
+
]
|
|
715
|
+
}),
|
|
716
|
+
groups.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
717
|
+
className: "rounded-md border border-dashed border-doom-border p-4 text-sm text-doom-dim",
|
|
718
|
+
children: view.totalIssues === 0 ? "No issues were detected in the returned analysis. This does not establish coverage of unrecorded activity." : "The analysis counted issues but returned no incident samples."
|
|
719
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
720
|
+
className: "flex flex-col gap-2",
|
|
721
|
+
"data-testid": "metrics-issue-groups",
|
|
722
|
+
children: groups.map((group) => /* @__PURE__ */ jsx(IssueRow, {
|
|
723
|
+
group,
|
|
724
|
+
max,
|
|
725
|
+
tools
|
|
726
|
+
}, group.key))
|
|
727
|
+
}),
|
|
728
|
+
Object.keys(view.byTool).length === 0 ? null : /* @__PURE__ */ jsxs("div", {
|
|
729
|
+
className: "flex flex-col gap-2 overflow-x-auto",
|
|
730
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
731
|
+
className: "text-base font-semibold text-doom-hi",
|
|
732
|
+
children: "Failures by tool"
|
|
733
|
+
}), /* @__PURE__ */ jsxs("table", {
|
|
734
|
+
className: "w-full text-sm",
|
|
735
|
+
"data-testid": "metrics-issues-tools",
|
|
736
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
|
|
737
|
+
className: "text-sm text-doom-dim",
|
|
738
|
+
children: [
|
|
739
|
+
/* @__PURE__ */ jsx("th", {
|
|
740
|
+
scope: "col",
|
|
741
|
+
className: "py-2 text-left font-normal",
|
|
742
|
+
children: "Tool"
|
|
743
|
+
}),
|
|
744
|
+
/* @__PURE__ */ jsx("th", {
|
|
745
|
+
scope: "col",
|
|
746
|
+
className: "text-right font-normal",
|
|
747
|
+
children: "Failures"
|
|
748
|
+
}),
|
|
749
|
+
/* @__PURE__ */ jsx("th", {
|
|
750
|
+
scope: "col",
|
|
751
|
+
className: "text-right font-normal",
|
|
752
|
+
children: "Attribution context"
|
|
753
|
+
})
|
|
754
|
+
]
|
|
755
|
+
}) }), /* @__PURE__ */ jsx("tbody", { children: countRows(view.byTool).map(([name, failures]) => {
|
|
756
|
+
const calls = callsByTool.get(name);
|
|
757
|
+
return /* @__PURE__ */ jsxs("tr", {
|
|
758
|
+
className: "border-b border-doom-border/40",
|
|
759
|
+
children: [
|
|
760
|
+
/* @__PURE__ */ jsx("td", {
|
|
761
|
+
className: "min-w-0 break-all py-2 text-doom-dim",
|
|
762
|
+
children: name
|
|
763
|
+
}),
|
|
764
|
+
/* @__PURE__ */ jsx("td", {
|
|
765
|
+
className: "w-16 py-2 text-right text-doom-red",
|
|
766
|
+
children: failures
|
|
767
|
+
}),
|
|
768
|
+
/* @__PURE__ */ jsx("td", {
|
|
769
|
+
className: "w-28 py-2 text-right text-doom-dim",
|
|
770
|
+
children: calls === void 0 ? "no token samples" : `${String(calls)} token samples`
|
|
771
|
+
})
|
|
772
|
+
]
|
|
773
|
+
}, name);
|
|
774
|
+
}) })]
|
|
775
|
+
})]
|
|
776
|
+
}),
|
|
777
|
+
/* @__PURE__ */ jsxs("div", {
|
|
778
|
+
className: "flex flex-wrap gap-x-4 gap-y-1 text-sm text-doom-dim",
|
|
779
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
780
|
+
className: "text-sm font-semibold text-doom-dim",
|
|
781
|
+
children: "by category"
|
|
782
|
+
}), countRows(view.byCategory).map(([name, count]) => /* @__PURE__ */ jsxs("span", { children: [
|
|
783
|
+
name,
|
|
784
|
+
" ",
|
|
785
|
+
/* @__PURE__ */ jsx("span", {
|
|
786
|
+
className: "text-doom-hi",
|
|
787
|
+
children: count
|
|
788
|
+
})
|
|
789
|
+
] }, name))]
|
|
790
|
+
})
|
|
791
|
+
]
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region src/extensions/(frontend)/setting/_components/IssuesSection.tsx
|
|
796
|
+
/** Issue analysis is an explicit read, not another request on every metrics refresh. */
|
|
797
|
+
function IssuesSection({ tools, focus, count, startTime, endTime, broaderScope = false }) {
|
|
798
|
+
const [view, setView] = useState();
|
|
799
|
+
const [message, setMessage] = useState("");
|
|
800
|
+
const [loading, setLoading] = useState(false);
|
|
801
|
+
const [open, setOpen] = useState(false);
|
|
802
|
+
const pending = useRef(void 0);
|
|
803
|
+
const detailsId = useId();
|
|
804
|
+
useEffect(() => {
|
|
805
|
+
pending.current?.abort();
|
|
806
|
+
setView(void 0);
|
|
807
|
+
setMessage("");
|
|
808
|
+
setLoading(false);
|
|
809
|
+
setOpen(false);
|
|
810
|
+
return () => pending.current?.abort();
|
|
811
|
+
}, [
|
|
812
|
+
focus,
|
|
813
|
+
startTime,
|
|
814
|
+
endTime
|
|
815
|
+
]);
|
|
816
|
+
const load = () => {
|
|
817
|
+
pending.current?.abort();
|
|
818
|
+
const controller = new AbortController();
|
|
819
|
+
pending.current = controller;
|
|
820
|
+
setOpen(true);
|
|
821
|
+
setLoading(true);
|
|
822
|
+
setMessage("");
|
|
823
|
+
setView(void 0);
|
|
824
|
+
fetchIssues(focus, controller.signal, {
|
|
825
|
+
startTime,
|
|
826
|
+
endTime
|
|
827
|
+
}).then((result) => {
|
|
828
|
+
if (controller.signal.aborted) return;
|
|
829
|
+
setLoading(false);
|
|
830
|
+
if ("error" in result) setMessage(result.error || "Issue details could not be loaded.");
|
|
831
|
+
else if (isIssuesUnavailable(result.issues)) setMessage(result.issues.detail);
|
|
832
|
+
else setView(result.issues);
|
|
833
|
+
}).catch((error) => {
|
|
834
|
+
if (controller.signal.aborted) return;
|
|
835
|
+
setLoading(false);
|
|
836
|
+
setMessage(error instanceof Error ? error.message : "Issue details could not be loaded.");
|
|
837
|
+
});
|
|
838
|
+
};
|
|
839
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
840
|
+
className: "flex min-w-0 flex-col gap-3 rounded-lg border border-doom-border bg-doom-panel p-4",
|
|
841
|
+
"data-testid": "metrics-issues",
|
|
842
|
+
children: [
|
|
843
|
+
/* @__PURE__ */ jsxs("div", {
|
|
844
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
845
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("h3", {
|
|
846
|
+
className: "text-lg font-semibold text-doom-hi",
|
|
847
|
+
children: ["Issues to investigate", count === void 0 ? "" : ` · ${count} ${count === 1 ? "issue" : "issues"}`]
|
|
848
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
849
|
+
className: "mt-1 text-sm text-doom-dim",
|
|
850
|
+
children: "Inspect recurring failures and the evidence behind them."
|
|
851
|
+
})] }), /* @__PURE__ */ jsxs("div", {
|
|
852
|
+
className: "flex items-center gap-2",
|
|
853
|
+
children: [
|
|
854
|
+
loading ? /* @__PURE__ */ jsx(Spinner, { label: "Reading issue details" }) : null,
|
|
855
|
+
/* @__PURE__ */ jsx(Button, {
|
|
856
|
+
variant: "outline",
|
|
857
|
+
size: "md",
|
|
858
|
+
disabled: loading,
|
|
859
|
+
onClick: load,
|
|
860
|
+
"aria-expanded": open,
|
|
861
|
+
"aria-controls": detailsId,
|
|
862
|
+
"data-testid": open ? "metrics-issues-reload" : "metrics-issues-open",
|
|
863
|
+
children: open ? "Reload details" : "Inspect issues"
|
|
864
|
+
}),
|
|
865
|
+
open ? /* @__PURE__ */ jsx(Button, {
|
|
866
|
+
variant: "ghost",
|
|
867
|
+
size: "md",
|
|
868
|
+
onClick: () => {
|
|
869
|
+
pending.current?.abort();
|
|
870
|
+
setLoading(false);
|
|
871
|
+
setOpen(false);
|
|
872
|
+
},
|
|
873
|
+
"aria-controls": detailsId,
|
|
874
|
+
children: "Hide"
|
|
875
|
+
}) : null
|
|
876
|
+
]
|
|
877
|
+
})]
|
|
878
|
+
}),
|
|
879
|
+
/* @__PURE__ */ jsxs("p", {
|
|
880
|
+
className: "text-sm leading-relaxed text-doom-dim",
|
|
881
|
+
children: [
|
|
882
|
+
"Details are loaded on demand.",
|
|
883
|
+
" ",
|
|
884
|
+
startTime === void 0 ? endTime === void 0 ? "This hub did not report a time window; details cover all available history." : "Details cover available history up to report time; no start bound was supplied." : "Details use the report’s recorded time window.",
|
|
885
|
+
" ",
|
|
886
|
+
broaderScope ? "LogSink cannot filter issue details by model, provider or agent, so this analysis covers all groups in that window." : focus === void 0 ? "Analysis covers all groups." : "Filtered to this session."
|
|
887
|
+
]
|
|
888
|
+
}),
|
|
889
|
+
/* @__PURE__ */ jsxs("div", {
|
|
890
|
+
id: detailsId,
|
|
891
|
+
hidden: !open,
|
|
892
|
+
"aria-busy": loading,
|
|
893
|
+
children: [
|
|
894
|
+
message === "" ? null : /* @__PURE__ */ jsx("p", {
|
|
895
|
+
role: "alert",
|
|
896
|
+
className: "rounded-md border border-doom-edge-yellow bg-doom-tint-yellow p-3 text-sm text-doom-yellow",
|
|
897
|
+
"data-testid": "metrics-issues-message",
|
|
898
|
+
children: message
|
|
899
|
+
}),
|
|
900
|
+
loading ? /* @__PURE__ */ jsx("p", {
|
|
901
|
+
role: "status",
|
|
902
|
+
className: "py-3 text-sm text-doom-dim",
|
|
903
|
+
children: "Reading recorded incidents..."
|
|
904
|
+
}) : null,
|
|
905
|
+
view === void 0 ? null : /* @__PURE__ */ jsx(IssuesDetail, {
|
|
906
|
+
view,
|
|
907
|
+
tools: broaderScope ? [] : tools,
|
|
908
|
+
summaryCount: broaderScope ? void 0 : count
|
|
909
|
+
})
|
|
910
|
+
]
|
|
911
|
+
})
|
|
912
|
+
]
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
//#endregion
|
|
916
|
+
//#region src/extensions/(frontend)/setting/_components/MetricsSummary.tsx
|
|
917
|
+
/** Independent counters, never a stacked breakdown of the provider total. */
|
|
918
|
+
function MetricsSummary({ totals }) {
|
|
919
|
+
const cards = [
|
|
920
|
+
{
|
|
921
|
+
label: "Total tokens",
|
|
922
|
+
value: totals.totalTokens,
|
|
923
|
+
detail: "Provider-reported total",
|
|
924
|
+
color: "text-doom-blue"
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
label: "Input tokens",
|
|
928
|
+
value: totals.inputTokens,
|
|
929
|
+
detail: "Reported input counter",
|
|
930
|
+
color: "text-doom-hi"
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
label: "Output tokens",
|
|
934
|
+
value: totals.outputTokens,
|
|
935
|
+
detail: "Reported output counter",
|
|
936
|
+
color: "text-doom-hi"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
label: "Recorded issues",
|
|
940
|
+
value: totals.issueCount,
|
|
941
|
+
detail: "Detected in this selection",
|
|
942
|
+
color: totals.issueCount > 0 ? "text-doom-yellow" : "text-doom-hi"
|
|
943
|
+
}
|
|
944
|
+
];
|
|
945
|
+
return /* @__PURE__ */ jsx("dl", {
|
|
946
|
+
className: "grid grid-cols-2 gap-3 @3xl:grid-cols-4",
|
|
947
|
+
"data-testid": "metrics-summary",
|
|
948
|
+
children: cards.map((card) => /* @__PURE__ */ jsxs("div", {
|
|
949
|
+
className: "min-w-0 rounded-lg border border-doom-border bg-doom-panel p-4",
|
|
950
|
+
children: [
|
|
951
|
+
/* @__PURE__ */ jsx("dt", {
|
|
952
|
+
className: "text-sm text-doom-dim",
|
|
953
|
+
children: card.label
|
|
954
|
+
}),
|
|
955
|
+
/* @__PURE__ */ jsx("dd", {
|
|
956
|
+
className: `mt-2 text-lg font-semibold tabular-nums ${card.color}`,
|
|
957
|
+
title: Number.isFinite(card.value) ? card.value.toLocaleString("en-US") : "Not reported",
|
|
958
|
+
children: formatTokens(card.value)
|
|
959
|
+
}),
|
|
960
|
+
/* @__PURE__ */ jsx("dd", {
|
|
961
|
+
className: "mt-1 text-sm text-doom-dim",
|
|
962
|
+
children: card.detail
|
|
963
|
+
})
|
|
964
|
+
]
|
|
965
|
+
}, card.label))
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region src/extensions/(frontend)/setting/_components/ToolMetrics.tsx
|
|
970
|
+
/** The sink attributes a whole turn to its tools. These are samples, not bills. */
|
|
971
|
+
function ToolMetrics({ tools }) {
|
|
972
|
+
const [sort, setSort] = useState("p90TotalTokens");
|
|
973
|
+
const rows = [...tools].sort((a, b) => b[sort] - a[sort] || a.name.localeCompare(b.name));
|
|
974
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
975
|
+
className: "flex min-w-0 flex-col gap-4 rounded-lg border border-doom-border bg-doom-panel p-4",
|
|
976
|
+
"aria-label": "Tool token samples",
|
|
977
|
+
children: [
|
|
978
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
979
|
+
className: "text-lg font-semibold text-doom-hi",
|
|
980
|
+
children: "Tool token samples"
|
|
981
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
982
|
+
className: "mt-1 text-sm text-doom-dim",
|
|
983
|
+
children: "Which tools participate in token-heavy turns?"
|
|
984
|
+
})] }),
|
|
985
|
+
rows.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
986
|
+
className: "flex flex-1 flex-col justify-center gap-2 rounded-md border border-dashed border-doom-border p-5",
|
|
987
|
+
"data-testid": "metrics-tools-empty",
|
|
988
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
989
|
+
className: "text-base font-semibold text-doom-hi",
|
|
990
|
+
children: "No token-attributed tool calls"
|
|
991
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
992
|
+
className: "text-sm leading-relaxed text-doom-dim",
|
|
993
|
+
children: "Tool execution logs may exist, but this selection has no samples that connect tools to model usage. This does not mean no tools ran."
|
|
994
|
+
})]
|
|
995
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
996
|
+
className: "flex flex-wrap gap-1",
|
|
997
|
+
role: "group",
|
|
998
|
+
"aria-label": "Rank tool samples by",
|
|
999
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
1000
|
+
size: "sm",
|
|
1001
|
+
variant: sort === "p90TotalTokens" ? "primary" : "ghost",
|
|
1002
|
+
"aria-pressed": sort === "p90TotalTokens",
|
|
1003
|
+
onClick: () => setSort("p90TotalTokens"),
|
|
1004
|
+
children: "P90 turn tokens"
|
|
1005
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1006
|
+
size: "sm",
|
|
1007
|
+
variant: sort === "calls" ? "primary" : "ghost",
|
|
1008
|
+
"aria-pressed": sort === "calls",
|
|
1009
|
+
onClick: () => setSort("calls"),
|
|
1010
|
+
children: "Sampled calls"
|
|
1011
|
+
})]
|
|
1012
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1013
|
+
className: "overflow-x-auto",
|
|
1014
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
1015
|
+
className: "w-full text-base tabular-nums",
|
|
1016
|
+
"data-testid": "metrics-tools",
|
|
1017
|
+
children: [
|
|
1018
|
+
/* @__PURE__ */ jsxs("caption", {
|
|
1019
|
+
className: "sr-only",
|
|
1020
|
+
children: ["Tools ranked by ", sort === "calls" ? "sampled calls" : "90th percentile whole-turn tokens"]
|
|
1021
|
+
}),
|
|
1022
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
|
|
1023
|
+
className: "border-b border-doom-border text-sm text-doom-dim",
|
|
1024
|
+
children: [
|
|
1025
|
+
/* @__PURE__ */ jsx("th", {
|
|
1026
|
+
scope: "col",
|
|
1027
|
+
className: "pb-2 text-left font-normal",
|
|
1028
|
+
children: "Tool"
|
|
1029
|
+
}),
|
|
1030
|
+
/* @__PURE__ */ jsx("th", {
|
|
1031
|
+
scope: "col",
|
|
1032
|
+
className: "pb-2 pl-3 text-right font-normal",
|
|
1033
|
+
children: "Sampled calls"
|
|
1034
|
+
}),
|
|
1035
|
+
/* @__PURE__ */ jsx("th", {
|
|
1036
|
+
scope: "col",
|
|
1037
|
+
className: "pb-2 pl-3 text-right font-normal",
|
|
1038
|
+
children: "P90 turn tokens"
|
|
1039
|
+
})
|
|
1040
|
+
]
|
|
1041
|
+
}) }),
|
|
1042
|
+
/* @__PURE__ */ jsx("tbody", { children: rows.map((tool) => /* @__PURE__ */ jsxs("tr", {
|
|
1043
|
+
className: "border-b border-doom-border/40",
|
|
1044
|
+
children: [
|
|
1045
|
+
/* @__PURE__ */ jsx("th", {
|
|
1046
|
+
scope: "row",
|
|
1047
|
+
className: "max-w-48 break-all py-3 text-left font-normal text-doom-hi",
|
|
1048
|
+
children: tool.name
|
|
1049
|
+
}),
|
|
1050
|
+
/* @__PURE__ */ jsx("td", {
|
|
1051
|
+
className: "py-3 pl-3 text-right text-doom-dim",
|
|
1052
|
+
children: formatTokens(tool.calls)
|
|
1053
|
+
}),
|
|
1054
|
+
/* @__PURE__ */ jsx("td", {
|
|
1055
|
+
className: "py-3 pl-3 text-right text-doom-blue",
|
|
1056
|
+
title: String(tool.p90TotalTokens),
|
|
1057
|
+
children: formatTokens(tool.p90TotalTokens)
|
|
1058
|
+
})
|
|
1059
|
+
]
|
|
1060
|
+
}, tool.name)) })
|
|
1061
|
+
]
|
|
1062
|
+
})
|
|
1063
|
+
})] }),
|
|
1064
|
+
/* @__PURE__ */ jsxs("details", {
|
|
1065
|
+
className: "text-sm leading-relaxed text-doom-dim",
|
|
1066
|
+
children: [/* @__PURE__ */ jsx("summary", {
|
|
1067
|
+
className: "cursor-pointer rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-doom-blue",
|
|
1068
|
+
children: "How these samples are counted"
|
|
1069
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1070
|
+
className: "mt-2",
|
|
1071
|
+
children: "This view includes only calls with recorded token attribution; external MCP calls remain in the logs. P90 is the 90th percentile of the whole turn, not each tool's own consumption. Several tools may share a turn, so do not add these values together."
|
|
1072
|
+
})]
|
|
1073
|
+
})
|
|
1074
|
+
]
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/extensions/(frontend)/setting/_components/MetricsReportView.tsx
|
|
1079
|
+
const DIMENSION_LABELS = {
|
|
1080
|
+
session: "session",
|
|
1081
|
+
agent: "agent",
|
|
1082
|
+
model: "model",
|
|
1083
|
+
provider: "provider"
|
|
1084
|
+
};
|
|
1085
|
+
const DIMENSION_NOTES = { session: "Session ids are hashed before export. Select a row to filter usage, not to open a session." };
|
|
1086
|
+
/** The same data-only dashboard is rendered by the stories and the settings panel. */
|
|
1087
|
+
function MetricsReportView({ report, onFocus, onPeriodChange }) {
|
|
1088
|
+
const label = DIMENSION_LABELS[report.dimension];
|
|
1089
|
+
const unattributed = report.groups.filter((group) => group.key === "unknown" || group.key === "").reduce((sum, group) => sum + group.totalTokens, 0);
|
|
1090
|
+
const counters = [
|
|
1091
|
+
{
|
|
1092
|
+
name: "Cache reads",
|
|
1093
|
+
value: report.totals.cachedTokens
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
name: "Reasoning tokens",
|
|
1097
|
+
value: report.totals.reasoningTokens
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
name: "Usage events",
|
|
1101
|
+
value: report.totals.usageEventCount
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
name: "Log and span records",
|
|
1105
|
+
value: report.totals.totalRecords
|
|
1106
|
+
}
|
|
1107
|
+
];
|
|
1108
|
+
const generated = new Date(report.generatedAt);
|
|
1109
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1110
|
+
className: "@container flex w-full min-w-0 flex-col gap-4",
|
|
1111
|
+
"data-testid": "metrics-report",
|
|
1112
|
+
children: [
|
|
1113
|
+
/* @__PURE__ */ jsx(MetricsSummary, { totals: report.totals }),
|
|
1114
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1115
|
+
className: "grid min-w-0 grid-cols-1 gap-4 @3xl:grid-cols-3",
|
|
1116
|
+
children: [/* @__PURE__ */ jsxs("section", {
|
|
1117
|
+
className: "flex min-w-0 flex-col gap-4 rounded-lg border border-doom-border bg-doom-panel p-4 @3xl:col-span-2",
|
|
1118
|
+
"aria-label": "Usage over time",
|
|
1119
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
1120
|
+
className: "text-lg font-semibold text-doom-hi",
|
|
1121
|
+
children: "Usage over time"
|
|
1122
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1123
|
+
className: "mt-1 text-sm text-doom-dim",
|
|
1124
|
+
children: "Recorded tokens, not estimated spend."
|
|
1125
|
+
})] }), /* @__PURE__ */ jsx(TimelineChart, {
|
|
1126
|
+
buckets: report.timeline,
|
|
1127
|
+
bucketUnit: report.bucketUnit,
|
|
1128
|
+
onShorterPeriod: report.period === "day" || onPeriodChange === void 0 ? void 0 : () => onPeriodChange("day")
|
|
1129
|
+
})]
|
|
1130
|
+
}), /* @__PURE__ */ jsxs("section", {
|
|
1131
|
+
className: "flex min-w-0 flex-col gap-4 rounded-lg border border-doom-border bg-doom-panel p-4",
|
|
1132
|
+
"aria-label": "Reported counters",
|
|
1133
|
+
children: [
|
|
1134
|
+
/* @__PURE__ */ jsx("h3", {
|
|
1135
|
+
className: "text-lg font-semibold text-doom-hi",
|
|
1136
|
+
children: "Reported counters"
|
|
1137
|
+
}),
|
|
1138
|
+
/* @__PURE__ */ jsx("dl", {
|
|
1139
|
+
className: "flex flex-col gap-3",
|
|
1140
|
+
children: counters.map((counter) => /* @__PURE__ */ jsxs("div", {
|
|
1141
|
+
className: "flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1 border-b border-doom-border/40 pb-3",
|
|
1142
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
1143
|
+
className: "text-sm text-doom-dim",
|
|
1144
|
+
children: counter.name
|
|
1145
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
1146
|
+
className: "text-base font-semibold tabular-nums text-doom-hi",
|
|
1147
|
+
title: counter.value === void 0 ? "Not reported" : String(counter.value),
|
|
1148
|
+
children: formatTokens(counter.value ?? NaN)
|
|
1149
|
+
})]
|
|
1150
|
+
}, counter.name))
|
|
1151
|
+
}),
|
|
1152
|
+
/* @__PURE__ */ jsx("p", {
|
|
1153
|
+
className: "text-sm leading-relaxed text-doom-dim",
|
|
1154
|
+
children: "Monetary cost is not included in this metrics report."
|
|
1155
|
+
}),
|
|
1156
|
+
/* @__PURE__ */ jsxs("details", {
|
|
1157
|
+
className: "text-sm leading-relaxed text-doom-dim",
|
|
1158
|
+
children: [/* @__PURE__ */ jsx("summary", {
|
|
1159
|
+
className: "cursor-pointer rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-doom-blue",
|
|
1160
|
+
children: "Why the counters may not add up"
|
|
1161
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1162
|
+
className: "mt-2",
|
|
1163
|
+
children: "The total is reported by providers. Input, output, cache reads and reasoning are independent counters and may overlap or be incomplete. They are not a breakdown and may not sum to the total. A gap is not evidence of cache traffic. Missing fields say Not reported; a recorded zero stays zero."
|
|
1164
|
+
})]
|
|
1165
|
+
})
|
|
1166
|
+
]
|
|
1167
|
+
})]
|
|
1168
|
+
}),
|
|
1169
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1170
|
+
className: "grid min-w-0 grid-cols-1 items-start gap-4 @3xl:grid-cols-2",
|
|
1171
|
+
children: [/* @__PURE__ */ jsxs("section", {
|
|
1172
|
+
className: "flex min-w-0 flex-col gap-3 rounded-lg border border-doom-border bg-doom-panel p-4",
|
|
1173
|
+
"aria-label": `Tokens by ${label}`,
|
|
1174
|
+
children: [
|
|
1175
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("h3", {
|
|
1176
|
+
className: "text-lg font-semibold text-doom-hi",
|
|
1177
|
+
children: ["Tokens by ", label]
|
|
1178
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1179
|
+
className: "mt-1 text-sm text-doom-dim",
|
|
1180
|
+
children: "Largest recorded consumers. Select a named row to filter."
|
|
1181
|
+
})] }),
|
|
1182
|
+
unattributed > 0 ? /* @__PURE__ */ jsxs("div", {
|
|
1183
|
+
className: "rounded-md border border-doom-edge-yellow bg-doom-tint-yellow p-3",
|
|
1184
|
+
"data-testid": "metrics-attribution-notice",
|
|
1185
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
1186
|
+
className: "text-sm font-semibold text-doom-yellow",
|
|
1187
|
+
children: [label.charAt(0).toUpperCase() + label.slice(1), " attribution missing"]
|
|
1188
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
1189
|
+
className: "mt-1 text-sm leading-relaxed text-doom-dim",
|
|
1190
|
+
children: [
|
|
1191
|
+
formatTokens(unattributed),
|
|
1192
|
+
" tokens have no ",
|
|
1193
|
+
label,
|
|
1194
|
+
" recorded. They are included in the total, but cannot be assigned to a named ",
|
|
1195
|
+
label,
|
|
1196
|
+
"."
|
|
1197
|
+
]
|
|
1198
|
+
})]
|
|
1199
|
+
}) : null,
|
|
1200
|
+
DIMENSION_NOTES[report.dimension] === void 0 ? null : /* @__PURE__ */ jsx("p", {
|
|
1201
|
+
className: "text-sm leading-relaxed text-doom-dim",
|
|
1202
|
+
children: DIMENSION_NOTES[report.dimension]
|
|
1203
|
+
}),
|
|
1204
|
+
/* @__PURE__ */ jsx(GroupBars, {
|
|
1205
|
+
groups: report.groups,
|
|
1206
|
+
totalTokens: report.totals.totalTokens,
|
|
1207
|
+
focus: report.focus,
|
|
1208
|
+
onFocus
|
|
1209
|
+
}, `${report.dimension}:${report.focus ?? ""}`),
|
|
1210
|
+
/* @__PURE__ */ jsxs("p", {
|
|
1211
|
+
className: "text-sm text-doom-dim",
|
|
1212
|
+
children: [
|
|
1213
|
+
"Showing ",
|
|
1214
|
+
report.groups.length,
|
|
1215
|
+
" of ",
|
|
1216
|
+
formatTokens(report.totals.groupCount),
|
|
1217
|
+
" ",
|
|
1218
|
+
label,
|
|
1219
|
+
" groups. Bars show share of the reported total."
|
|
1220
|
+
]
|
|
1221
|
+
}),
|
|
1222
|
+
report.totals.failedGroups > 0 ? /* @__PURE__ */ jsxs("p", {
|
|
1223
|
+
className: "text-sm text-doom-yellow",
|
|
1224
|
+
children: [
|
|
1225
|
+
report.totals.failedGroups,
|
|
1226
|
+
" of ",
|
|
1227
|
+
report.totals.groupCount,
|
|
1228
|
+
" ",
|
|
1229
|
+
label,
|
|
1230
|
+
"s failed"
|
|
1231
|
+
]
|
|
1232
|
+
}) : null
|
|
1233
|
+
]
|
|
1234
|
+
}), /* @__PURE__ */ jsx(ToolMetrics, { tools: report.tools })]
|
|
1235
|
+
}),
|
|
1236
|
+
/* @__PURE__ */ jsx(IssuesSection, {
|
|
1237
|
+
tools: report.tools,
|
|
1238
|
+
focus: report.dimension === "session" ? report.focus : void 0,
|
|
1239
|
+
count: report.totals.issueCount,
|
|
1240
|
+
startTime: report.startTime,
|
|
1241
|
+
endTime: report.endTime,
|
|
1242
|
+
broaderScope: report.focus !== void 0 && report.dimension !== "session"
|
|
1243
|
+
}, `${report.generatedAt}:${report.dimension}:${report.focus ?? ""}`),
|
|
1244
|
+
/* @__PURE__ */ jsxs("footer", {
|
|
1245
|
+
className: "flex flex-wrap justify-between gap-2 border-t border-doom-border pt-3 text-sm text-doom-dim",
|
|
1246
|
+
"data-testid": "metrics-provenance",
|
|
1247
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
1248
|
+
"Source: LogSink ·",
|
|
1249
|
+
" ",
|
|
1250
|
+
report.transport === "http" ? "HTTP" : report.transport === "worker" ? "database worker" : "transport not reported"
|
|
1251
|
+
] }), /* @__PURE__ */ jsxs("span", {
|
|
1252
|
+
title: report.generatedAt,
|
|
1253
|
+
children: ["Report generated ", Number.isNaN(generated.getTime()) ? report.generatedAt : generated.toLocaleString()]
|
|
1254
|
+
})]
|
|
1255
|
+
})
|
|
1256
|
+
]
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/extensions/(frontend)/setting/_components/MetricsNotice.tsx
|
|
1261
|
+
/**
|
|
1262
|
+
* Everything the page says instead of, or above, a report.
|
|
1263
|
+
*
|
|
1264
|
+
* Kept pure and apart from the panel because these are the states that decide
|
|
1265
|
+
* whether a reader trusts the numbers, and they are the ones a fetching
|
|
1266
|
+
* component makes awkward to assert. The panel decides which state it is in;
|
|
1267
|
+
* this decides how each one reads.
|
|
1268
|
+
*/
|
|
1269
|
+
const EMPTY_TITLES = {
|
|
1270
|
+
"no-sink": "no log sink",
|
|
1271
|
+
"no-data": "nothing recorded yet",
|
|
1272
|
+
"no-api": "metrics not installed",
|
|
1273
|
+
"query-error": "metrics could not be loaded"
|
|
1274
|
+
};
|
|
1275
|
+
function EmptyForReason({ response }) {
|
|
1276
|
+
return /* @__PURE__ */ jsx(EmptyState, {
|
|
1277
|
+
title: EMPTY_TITLES[response.unavailable],
|
|
1278
|
+
description: response.detail,
|
|
1279
|
+
"data-testid": "metrics-empty"
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
function FocusNotice({ requested, applied, dimension, onClear }) {
|
|
1283
|
+
if (requested === "") return null;
|
|
1284
|
+
if (applied === void 0) return /* @__PURE__ */ jsxs("span", {
|
|
1285
|
+
className: "rounded-md border border-doom-edge-yellow bg-doom-tint-yellow p-3 text-sm text-doom-yellow",
|
|
1286
|
+
"data-testid": "metrics-focus-refused",
|
|
1287
|
+
children: [
|
|
1288
|
+
"this log sink does not support narrowing by ",
|
|
1289
|
+
DIMENSION_LABELS[dimension],
|
|
1290
|
+
", so the numbers below are still everything",
|
|
1291
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1292
|
+
variant: "ghost",
|
|
1293
|
+
size: "md",
|
|
1294
|
+
className: "ml-2",
|
|
1295
|
+
onClick: onClear,
|
|
1296
|
+
children: "clear"
|
|
1297
|
+
})
|
|
1298
|
+
]
|
|
1299
|
+
});
|
|
1300
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
1301
|
+
className: "flex flex-wrap items-center gap-2 rounded-md border border-doom-border bg-doom-panel p-3 text-sm text-doom-dim",
|
|
1302
|
+
"data-testid": "metrics-focus",
|
|
1303
|
+
children: [
|
|
1304
|
+
"narrowed to ",
|
|
1305
|
+
/* @__PURE__ */ jsx("span", {
|
|
1306
|
+
className: "min-w-0 break-all text-doom-hi",
|
|
1307
|
+
children: applied
|
|
1308
|
+
}),
|
|
1309
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1310
|
+
variant: "ghost",
|
|
1311
|
+
size: "md",
|
|
1312
|
+
className: "ml-2",
|
|
1313
|
+
onClick: onClear,
|
|
1314
|
+
children: "clear"
|
|
1315
|
+
})
|
|
1316
|
+
]
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
/** First load and filter changes have no report to display yet. */
|
|
1320
|
+
function MetricsLoading() {
|
|
1321
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1322
|
+
className: "flex flex-col gap-4",
|
|
1323
|
+
role: "status",
|
|
1324
|
+
"aria-live": "polite",
|
|
1325
|
+
"data-testid": "metrics-loading",
|
|
1326
|
+
children: [
|
|
1327
|
+
/* @__PURE__ */ jsx("span", {
|
|
1328
|
+
className: "text-sm text-doom-dim",
|
|
1329
|
+
children: "Loading recorded metrics..."
|
|
1330
|
+
}),
|
|
1331
|
+
/* @__PURE__ */ jsx("div", {
|
|
1332
|
+
"aria-hidden": "true",
|
|
1333
|
+
className: "grid grid-cols-2 gap-3 @3xl:grid-cols-4",
|
|
1334
|
+
children: [
|
|
1335
|
+
0,
|
|
1336
|
+
1,
|
|
1337
|
+
2,
|
|
1338
|
+
3
|
|
1339
|
+
].map((index) => /* @__PURE__ */ jsx("div", { className: "h-24 animate-pulse rounded-lg border border-doom-border bg-doom-panel motion-reduce:animate-none" }, index))
|
|
1340
|
+
}),
|
|
1341
|
+
/* @__PURE__ */ jsx("div", {
|
|
1342
|
+
"aria-hidden": "true",
|
|
1343
|
+
className: "h-48 animate-pulse rounded-lg border border-doom-border bg-doom-panel motion-reduce:animate-none"
|
|
1344
|
+
})
|
|
1345
|
+
]
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
//#endregion
|
|
1349
|
+
//#region src/extensions/(frontend)/setting/_components/MetricsPanel.tsx
|
|
1350
|
+
const PERIOD_LABELS = {
|
|
1351
|
+
day: "Past day",
|
|
1352
|
+
week: "Past week",
|
|
1353
|
+
month: "Past month",
|
|
1354
|
+
all: "All history"
|
|
1355
|
+
};
|
|
1356
|
+
/** Only render a response under the controls that requested it. */
|
|
1357
|
+
function MetricsPanel(_props) {
|
|
1358
|
+
const [dimension, setDimension] = useState("model");
|
|
1359
|
+
const [period, setPeriod] = useState("week");
|
|
1360
|
+
const [focus, setFocus] = useState("");
|
|
1361
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
1362
|
+
const [completed, setCompleted] = useState();
|
|
1363
|
+
const requestKey = JSON.stringify([
|
|
1364
|
+
dimension,
|
|
1365
|
+
period,
|
|
1366
|
+
focus,
|
|
1367
|
+
refreshKey
|
|
1368
|
+
]);
|
|
1369
|
+
useEffect(() => {
|
|
1370
|
+
const controller = new AbortController();
|
|
1371
|
+
fetchMetrics(dimension, period, focus, controller.signal).then((result) => {
|
|
1372
|
+
if (!controller.signal.aborted) setCompleted({
|
|
1373
|
+
key: requestKey,
|
|
1374
|
+
result
|
|
1375
|
+
});
|
|
1376
|
+
}).catch((error) => {
|
|
1377
|
+
if (!controller.signal.aborted) setCompleted({
|
|
1378
|
+
key: requestKey,
|
|
1379
|
+
result: { error: error instanceof Error ? error.message : "Metrics could not be loaded." }
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
return () => controller.abort();
|
|
1383
|
+
}, [
|
|
1384
|
+
dimension,
|
|
1385
|
+
period,
|
|
1386
|
+
focus,
|
|
1387
|
+
requestKey
|
|
1388
|
+
]);
|
|
1389
|
+
const result = completed?.key === requestKey ? completed.result : void 0;
|
|
1390
|
+
const loading = result === void 0;
|
|
1391
|
+
const response = result !== void 0 && "report" in result ? result.report : void 0;
|
|
1392
|
+
const report = response === void 0 || isMetricsUnavailable(response) ? void 0 : response;
|
|
1393
|
+
const error = result !== void 0 && "error" in result ? result.error : "";
|
|
1394
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1395
|
+
className: "@container flex w-full min-w-0 flex-col gap-4",
|
|
1396
|
+
"data-testid": "metrics-panel",
|
|
1397
|
+
"aria-busy": loading,
|
|
1398
|
+
children: [
|
|
1399
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1400
|
+
className: "flex min-w-0 flex-wrap items-end gap-3 rounded-lg border border-doom-border bg-doom-panel p-3",
|
|
1401
|
+
children: [
|
|
1402
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1403
|
+
className: "flex flex-col gap-2",
|
|
1404
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1405
|
+
className: "text-base font-medium text-doom-text",
|
|
1406
|
+
children: "Group by"
|
|
1407
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
1408
|
+
value: dimension,
|
|
1409
|
+
onValueChange: (next) => {
|
|
1410
|
+
setFocus("");
|
|
1411
|
+
setDimension(next);
|
|
1412
|
+
},
|
|
1413
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
1414
|
+
"aria-label": "Group metrics by",
|
|
1415
|
+
"data-testid": "metrics-dimension",
|
|
1416
|
+
className: "w-40 ",
|
|
1417
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
1418
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children: METRICS_DIMENSIONS.map((candidate) => /* @__PURE__ */ jsx(SelectItem, {
|
|
1419
|
+
value: candidate,
|
|
1420
|
+
children: DIMENSION_LABELS[candidate]
|
|
1421
|
+
}, candidate)) })]
|
|
1422
|
+
})]
|
|
1423
|
+
}),
|
|
1424
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1425
|
+
className: "flex flex-col gap-2",
|
|
1426
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1427
|
+
className: "text-base font-medium text-doom-text",
|
|
1428
|
+
children: "Period"
|
|
1429
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
1430
|
+
value: period,
|
|
1431
|
+
onValueChange: (next) => setPeriod(next),
|
|
1432
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
1433
|
+
"aria-label": "Metrics period",
|
|
1434
|
+
"data-testid": "metrics-period",
|
|
1435
|
+
className: "w-40 ",
|
|
1436
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
1437
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children: METRICS_PERIODS.map((candidate) => /* @__PURE__ */ jsx(SelectItem, {
|
|
1438
|
+
value: candidate,
|
|
1439
|
+
children: PERIOD_LABELS[candidate]
|
|
1440
|
+
}, candidate)) })]
|
|
1441
|
+
})]
|
|
1442
|
+
}),
|
|
1443
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1444
|
+
variant: "outline",
|
|
1445
|
+
size: "md",
|
|
1446
|
+
className: "ml-auto",
|
|
1447
|
+
onClick: () => setRefreshKey((current) => current + 1),
|
|
1448
|
+
disabled: loading,
|
|
1449
|
+
"data-testid": "metrics-refresh",
|
|
1450
|
+
children: loading ? "Loading..." : "Refresh"
|
|
1451
|
+
})
|
|
1452
|
+
]
|
|
1453
|
+
}),
|
|
1454
|
+
loading ? /* @__PURE__ */ jsx(MetricsLoading, {}) : null,
|
|
1455
|
+
error === "" ? null : /* @__PURE__ */ jsxs("p", {
|
|
1456
|
+
role: "alert",
|
|
1457
|
+
className: "rounded-lg border border-doom-edge-red bg-doom-tint-red p-4 text-sm text-doom-red",
|
|
1458
|
+
"data-testid": "metrics-error",
|
|
1459
|
+
children: [error, " Use Refresh to try again."]
|
|
1460
|
+
}),
|
|
1461
|
+
response !== void 0 && isMetricsUnavailable(response) ? /* @__PURE__ */ jsx(EmptyForReason, { response }) : null,
|
|
1462
|
+
report === void 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FocusNotice, {
|
|
1463
|
+
requested: focus,
|
|
1464
|
+
applied: report.focus,
|
|
1465
|
+
dimension: report.dimension,
|
|
1466
|
+
onClear: () => setFocus("")
|
|
1467
|
+
}), /* @__PURE__ */ jsx(MetricsReportView, {
|
|
1468
|
+
report,
|
|
1469
|
+
onFocus: setFocus,
|
|
1470
|
+
onPeriodChange: setPeriod
|
|
1471
|
+
})] })
|
|
1472
|
+
]
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
//#endregion
|
|
1476
|
+
//#region src/extensions/(frontend)/setting/metrics.panel.web.tsx
|
|
1477
|
+
var metrics_panel_web_default = defineSettingsPanel({
|
|
1478
|
+
label: "metrics",
|
|
1479
|
+
detail: "recorded usage, token attribution and issues",
|
|
1480
|
+
component: MetricsPanel
|
|
1481
|
+
});
|
|
1482
|
+
//#endregion
|
|
1483
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/_components/AgentDiagnosticCard.tsx
|
|
1484
|
+
function AgentDiagnosticCard({ toolName, output, running, isError }) {
|
|
1485
|
+
const lines = output.split("\n").filter(Boolean).map((text) => ({
|
|
1486
|
+
text,
|
|
1487
|
+
tone: "text"
|
|
1488
|
+
}));
|
|
1489
|
+
return /* @__PURE__ */ jsx(MessageItem, {
|
|
1490
|
+
tone: toolTone({
|
|
1491
|
+
running,
|
|
1492
|
+
isError
|
|
1493
|
+
}),
|
|
1494
|
+
expandable: lines.length > 12,
|
|
1495
|
+
children: ({ expanded }) => {
|
|
1496
|
+
const view = collapseLines(lines, 12, expanded);
|
|
1497
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MessageItemHeader, {
|
|
1498
|
+
title: toolName,
|
|
1499
|
+
children: "Captured agent evidence"
|
|
1500
|
+
}), /* @__PURE__ */ jsxs(MessageItemBody, { children: [
|
|
1501
|
+
/* @__PURE__ */ jsx(MessageLines, { lines: view.shown }),
|
|
1502
|
+
view.hidden > 0 ? /* @__PURE__ */ jsxs(MessageItemStatus, {
|
|
1503
|
+
expands: true,
|
|
1504
|
+
children: [view.hidden, " more lines"]
|
|
1505
|
+
}) : null,
|
|
1506
|
+
running ? /* @__PURE__ */ jsx(MessageItemStatus, {
|
|
1507
|
+
tone: "running",
|
|
1508
|
+
children: "Reading session telemetry"
|
|
1509
|
+
}) : null,
|
|
1510
|
+
isError ? /* @__PURE__ */ jsx(MessageItemStatus, {
|
|
1511
|
+
tone: "error",
|
|
1512
|
+
children: "Diagnostic unavailable"
|
|
1513
|
+
}) : null
|
|
1514
|
+
] })] });
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
//#endregion
|
|
1519
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/diagnose-agent.web.tsx
|
|
1520
|
+
var diagnose_agent_web_default = defineToolRenderer({
|
|
1521
|
+
tools: ["diagnose_agent"],
|
|
1522
|
+
message: AgentDiagnosticCard
|
|
1523
|
+
});
|
|
1524
|
+
//#endregion
|
|
1525
|
+
//#region generated/web.ts
|
|
1526
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
1527
|
+
...identity,
|
|
1528
|
+
...value
|
|
1529
|
+
};
|
|
1530
|
+
const webPlugin = defineWebPlugin({
|
|
1531
|
+
id: "log",
|
|
1532
|
+
global: { get settingsPanels() {
|
|
1533
|
+
return [via({ id: "metrics" }, metrics_panel_web_default)];
|
|
1534
|
+
} },
|
|
1535
|
+
session: { get toolRenderers() {
|
|
1536
|
+
return [via({ tools: ["diagnose_agent"] }, diagnose_agent_web_default)];
|
|
1537
|
+
} }
|
|
1538
|
+
});
|
|
1539
|
+
//#endregion
|
|
1540
|
+
export { webPlugin };
|
|
1541
|
+
|
|
1542
|
+
//# sourceMappingURL=web.mjs.map
|