@tonyclaw/agent-inspector 3.0.22 → 3.0.24
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/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-CaDAbX9q.js → CompareDrawer-BC52FYwn.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-WuChOrSS.js +127 -0
- package/.output/public/assets/{ReplayDialog-C5kb1Bj4.js → ReplayDialog-BcKsarKt.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CbPNLnlL.js → RequestAnatomy-DR0uGpCj.js} +1 -1
- package/.output/public/assets/{ResponseView-DyzW0Zfo.js → ResponseView-kxAAPfmn.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-Cl8zJu0w.js → StreamingChunkSequence-YQR0PjQj.js} +1 -1
- package/.output/public/assets/{_sessionId-BeN8-5Ho.js → _sessionId-DRl-Y9om.js} +1 -1
- package/.output/public/assets/{index-BCHWOmF2.js → index-CQbrc-hi.js} +71 -7
- package/.output/public/assets/{index-DcFtdKF3.js → index-CcSIBI4L.js} +1 -1
- package/.output/public/assets/index-DQeXi4Rv.css +1 -0
- package/.output/public/assets/{index-Pz9T7cS9.js → index-ZrMgyEZV.js} +1 -1
- package/.output/public/assets/{json-viewer-PNvFrOUw.js → json-viewer-CS5pwnEA.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +266 -234
- package/.output/server/{_sessionId-CIvbZsmt.mjs → _sessionId-BLk2xPkR.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CtWPZP5T.mjs → CompareDrawer-Cw7Dseo3.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-Dnm-Hup3.mjs → ProxyViewerContainer-D8SB0zyQ.mjs} +1124 -877
- package/.output/server/_ssr/{ReplayDialog-BYii7Q2m.mjs → ReplayDialog-B-YMhZkE.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-CC6brPZr.mjs → RequestAnatomy-a9d8UTYZ.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-CwwkL_0d.mjs → ResponseView-PzaZS4CN.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-CFsJzBLS.mjs → StreamingChunkSequence-C21_TCXk.mjs} +2 -2
- package/.output/server/_ssr/{index-B-Mm06YB.mjs → index-CrCeNP7H.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-D2MGmYZ0.mjs → json-viewer-Di2gxs9H.mjs} +3 -3
- package/.output/server/_ssr/{router-BwvN4eYE.mjs → router-BJzdUbcH.mjs} +296 -91
- package/.output/server/{_tanstack-start-manifest_v-Dpk8cafb.mjs → _tanstack-start-manifest_v-CGBTdAYC.mjs} +1 -1
- package/.output/server/index.mjs +64 -64
- package/package.json +6 -2
- package/src/components/ProxyViewer.tsx +136 -460
- package/src/components/ProxyViewerContainer.tsx +5 -47
- package/src/components/alerts/AlertsDialog.tsx +6 -2
- package/src/components/providers/ProviderCard.tsx +20 -579
- package/src/components/providers/ProviderTestResultsView.tsx +732 -0
- package/src/components/proxy-viewer/LogEntry.tsx +5 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +7 -0
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +33 -36
- package/src/components/proxy-viewer/TurnGroup.tsx +1 -5
- package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +46 -0
- package/src/components/proxy-viewer/proxyViewerLogic.ts +469 -0
- package/src/lib/themeMode.ts +45 -0
- package/src/lib/uiScale.ts +112 -0
- package/src/lib/useThemeMode.ts +40 -0
- package/src/lib/useUiScale.ts +52 -0
- package/src/routes/__root.tsx +78 -1
- package/styles/globals.css +305 -23
- package/.output/public/assets/ProxyViewerContainer-7ugZIMW0.js +0 -127
- package/.output/public/assets/index-SqCROi0s.css +0 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import type { CapturedLog } from "../../contracts";
|
|
2
|
+
import type { CaptureMode, TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
3
|
+
import { extractStopReason, type StopReason } from "../../lib/stopReason";
|
|
4
|
+
import { formatTimestampRange } from "../../lib/timeDisplay";
|
|
5
|
+
import { formatTokens } from "../../lib/utils";
|
|
6
|
+
import { buildTurnGroups } from "./viewerState";
|
|
7
|
+
|
|
8
|
+
export type ExportActionVisibility = {
|
|
9
|
+
redacted: boolean;
|
|
10
|
+
raw: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type SessionContextScope = {
|
|
14
|
+
label: string;
|
|
15
|
+
logs: CapturedLog[];
|
|
16
|
+
showBackLink: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type SessionSlateStatus = "rolling" | "clear" | "watch" | "failed";
|
|
20
|
+
export type SessionSlateMetricTone = "neutral" | "cool" | "watch" | "danger";
|
|
21
|
+
|
|
22
|
+
export type ConversationLogs = {
|
|
23
|
+
logs: CapturedLog[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SessionSlateStats = {
|
|
27
|
+
status: SessionSlateStatus;
|
|
28
|
+
statusLabel: string;
|
|
29
|
+
modelLabel: string;
|
|
30
|
+
providerLabel: string;
|
|
31
|
+
requests: number;
|
|
32
|
+
turns: number;
|
|
33
|
+
failures: number;
|
|
34
|
+
pending: number;
|
|
35
|
+
slow: number;
|
|
36
|
+
lengthStops: number;
|
|
37
|
+
timeRange: string | null;
|
|
38
|
+
tokenLabel: string;
|
|
39
|
+
finishLabel: string;
|
|
40
|
+
finishDetail: string;
|
|
41
|
+
finishTone: SessionSlateMetricTone;
|
|
42
|
+
outcomeLabel: string;
|
|
43
|
+
outcomeDetail: string;
|
|
44
|
+
outcomeTone: SessionSlateMetricTone;
|
|
45
|
+
slowestLabel: string;
|
|
46
|
+
slowestDetail: string;
|
|
47
|
+
heaviestDetail: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type PaginationStatusControls = {
|
|
51
|
+
isLoading: boolean;
|
|
52
|
+
total: number | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type FinishSummary = {
|
|
56
|
+
label: string;
|
|
57
|
+
detail: string;
|
|
58
|
+
tone: SessionSlateMetricTone;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type SessionOutcomeSummary = {
|
|
62
|
+
label: string;
|
|
63
|
+
detail: string;
|
|
64
|
+
tone: SessionSlateMetricTone;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type FailureSummary = {
|
|
68
|
+
logNumber: number;
|
|
69
|
+
status: number | null;
|
|
70
|
+
error: string | null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function shouldShowRawExport(captureMode: CaptureMode): boolean {
|
|
74
|
+
return captureMode === "full";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getExportActionVisibility(
|
|
78
|
+
captureMode: CaptureMode,
|
|
79
|
+
logCount: number,
|
|
80
|
+
): ExportActionVisibility {
|
|
81
|
+
const hasLogs = logCount > 0;
|
|
82
|
+
return {
|
|
83
|
+
redacted: hasLogs,
|
|
84
|
+
raw: hasLogs && shouldShowRawExport(captureMode),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
|
|
89
|
+
const first = logs[0];
|
|
90
|
+
const last = logs[logs.length - 1];
|
|
91
|
+
if (first === undefined || last === undefined) return null;
|
|
92
|
+
return formatTimestampRange(first.timestamp, last.timestamp, timeDisplayFormat);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasLogFailure(log: CapturedLog): boolean {
|
|
96
|
+
if (log.error !== null && log.error !== undefined && log.error.length > 0) return true;
|
|
97
|
+
return log.responseStatus !== null && log.responseStatus >= 400;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getModelLabel(logs: readonly CapturedLog[]): string {
|
|
101
|
+
const models = new Set<string>();
|
|
102
|
+
for (const log of logs) {
|
|
103
|
+
if (log.model !== null && log.model.length > 0) {
|
|
104
|
+
models.add(log.model);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const sorted = [...models].sort();
|
|
108
|
+
if (sorted.length === 0) return "Model unknown";
|
|
109
|
+
if (sorted.length === 1) return sorted[0] ?? "Model unknown";
|
|
110
|
+
return `${sorted.length} models`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getProviderLabel(logs: readonly CapturedLog[]): string {
|
|
114
|
+
const providers = new Set<string>();
|
|
115
|
+
for (const log of logs) {
|
|
116
|
+
if (
|
|
117
|
+
log.providerName !== null &&
|
|
118
|
+
log.providerName !== undefined &&
|
|
119
|
+
log.providerName.length > 0
|
|
120
|
+
) {
|
|
121
|
+
providers.add(log.providerName);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const sorted = [...providers].sort();
|
|
125
|
+
if (sorted.length === 0) return "Provider unknown";
|
|
126
|
+
if (sorted.length === 1) return sorted[0] ?? "Provider unknown";
|
|
127
|
+
return `${sorted.length} providers`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatElapsedMs(ms: number): string {
|
|
131
|
+
if (ms < 1000) return `${ms}ms`;
|
|
132
|
+
if (ms < 60_000) {
|
|
133
|
+
const seconds = ms / 1000;
|
|
134
|
+
return `${seconds < 10 ? seconds.toFixed(1) : seconds.toFixed(0)}s`;
|
|
135
|
+
}
|
|
136
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
137
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
138
|
+
const seconds = totalSeconds % 60;
|
|
139
|
+
return `${minutes}m ${seconds}s`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatTokenPair(inputTokens: number, outputTokens: number): string {
|
|
143
|
+
return `${formatTokens(inputTokens)} in / ${formatTokens(outputTokens)} out`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function countSessionTurns(
|
|
147
|
+
logs: CapturedLog[],
|
|
148
|
+
conversationGroups: readonly ConversationLogs[],
|
|
149
|
+
): number {
|
|
150
|
+
if (logs.length === 0) return 0;
|
|
151
|
+
if (conversationGroups.length === 0) return buildTurnGroups(logs).length;
|
|
152
|
+
|
|
153
|
+
let turns = 0;
|
|
154
|
+
for (const group of conversationGroups) {
|
|
155
|
+
turns += buildTurnGroups(group.logs).length;
|
|
156
|
+
}
|
|
157
|
+
return turns;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function summarizeFinishReason({
|
|
161
|
+
stopReason,
|
|
162
|
+
pending,
|
|
163
|
+
lastFailureLogNumber,
|
|
164
|
+
}: {
|
|
165
|
+
stopReason: StopReason;
|
|
166
|
+
pending: number;
|
|
167
|
+
lastFailureLogNumber: number | null;
|
|
168
|
+
}): FinishSummary {
|
|
169
|
+
if (pending > 0) {
|
|
170
|
+
return {
|
|
171
|
+
label: "In flight",
|
|
172
|
+
detail: `${pending} request${pending === 1 ? "" : "s"} pending`,
|
|
173
|
+
tone: "cool",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (lastFailureLogNumber !== null && stopReason === null) {
|
|
178
|
+
return {
|
|
179
|
+
label: "Error",
|
|
180
|
+
detail: `Last failed request #${lastFailureLogNumber}`,
|
|
181
|
+
tone: "danger",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
switch (stopReason) {
|
|
186
|
+
case "end_turn":
|
|
187
|
+
return { label: "End turn", detail: "Assistant completed the turn", tone: "neutral" };
|
|
188
|
+
case "stop":
|
|
189
|
+
return { label: "Stop", detail: "Model reached a stop condition", tone: "neutral" };
|
|
190
|
+
case "length":
|
|
191
|
+
return { label: "Output limit", detail: "Turn ended at max output tokens", tone: "watch" };
|
|
192
|
+
case "tool_use":
|
|
193
|
+
return { label: "Tool call", detail: "Assistant requested a tool", tone: "cool" };
|
|
194
|
+
case null:
|
|
195
|
+
return { label: "Unknown", detail: "No finish reason captured", tone: "neutral" };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isClientAbortError(error: string | null): boolean {
|
|
200
|
+
if (error === null) return false;
|
|
201
|
+
const lower = error.toLowerCase();
|
|
202
|
+
return (
|
|
203
|
+
lower.includes("abort") ||
|
|
204
|
+
lower.includes("cancel") ||
|
|
205
|
+
lower.includes("client disconnected") ||
|
|
206
|
+
lower.includes("stream disconnected") ||
|
|
207
|
+
lower.includes("connection closed")
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function summarizeSessionOutcome({
|
|
212
|
+
pending,
|
|
213
|
+
failures,
|
|
214
|
+
lengthStops,
|
|
215
|
+
stopReason,
|
|
216
|
+
lastFailure,
|
|
217
|
+
}: {
|
|
218
|
+
pending: number;
|
|
219
|
+
failures: number;
|
|
220
|
+
lengthStops: number;
|
|
221
|
+
stopReason: StopReason;
|
|
222
|
+
lastFailure: FailureSummary | null;
|
|
223
|
+
}): SessionOutcomeSummary {
|
|
224
|
+
if (
|
|
225
|
+
lastFailure !== null &&
|
|
226
|
+
(lastFailure.status === 499 || isClientAbortError(lastFailure.error))
|
|
227
|
+
) {
|
|
228
|
+
return {
|
|
229
|
+
label: "Client aborted",
|
|
230
|
+
detail: `Request #${lastFailure.logNumber} ended before completion`,
|
|
231
|
+
tone: "watch",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (pending > 0) {
|
|
236
|
+
return {
|
|
237
|
+
label: "In flight",
|
|
238
|
+
detail: `${pending} request${pending === 1 ? "" : "s"} still running`,
|
|
239
|
+
tone: "cool",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (lastFailure !== null && lastFailure.status !== null && lastFailure.status >= 500) {
|
|
244
|
+
return {
|
|
245
|
+
label: "Provider error",
|
|
246
|
+
detail: `Request #${lastFailure.logNumber} returned HTTP ${lastFailure.status}`,
|
|
247
|
+
tone: "danger",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (failures > 0 && lastFailure !== null) {
|
|
252
|
+
return {
|
|
253
|
+
label: "Failed",
|
|
254
|
+
detail: `Request #${lastFailure.logNumber} needs attention`,
|
|
255
|
+
tone: "danger",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (lengthStops > 0 || stopReason === "length") {
|
|
260
|
+
return {
|
|
261
|
+
label: "Output limited",
|
|
262
|
+
detail: "Model stopped at the output token limit",
|
|
263
|
+
tone: "watch",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
switch (stopReason) {
|
|
268
|
+
case "end_turn":
|
|
269
|
+
case "stop":
|
|
270
|
+
return { label: "Completed", detail: "Assistant completed normally", tone: "neutral" };
|
|
271
|
+
case "tool_use":
|
|
272
|
+
return { label: "Tool requested", detail: "Assistant handed off to a tool", tone: "cool" };
|
|
273
|
+
case null:
|
|
274
|
+
return { label: "Unknown", detail: "No terminal signal captured", tone: "neutral" };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function buildSessionSlateStats({
|
|
279
|
+
logs,
|
|
280
|
+
conversationGroups,
|
|
281
|
+
isLoading,
|
|
282
|
+
timeDisplayFormat,
|
|
283
|
+
slowResponseThresholdSeconds,
|
|
284
|
+
}: {
|
|
285
|
+
logs: CapturedLog[];
|
|
286
|
+
conversationGroups: readonly ConversationLogs[];
|
|
287
|
+
isLoading: boolean;
|
|
288
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
289
|
+
slowResponseThresholdSeconds: number;
|
|
290
|
+
}): SessionSlateStats {
|
|
291
|
+
let failures = 0;
|
|
292
|
+
let pending = 0;
|
|
293
|
+
let slow = 0;
|
|
294
|
+
let lengthStops = 0;
|
|
295
|
+
let totalIn = 0;
|
|
296
|
+
let totalOut = 0;
|
|
297
|
+
let terminalStopReason: StopReason = null;
|
|
298
|
+
let lastFailureLogNumber: number | null = null;
|
|
299
|
+
let lastFailure: FailureSummary | null = null;
|
|
300
|
+
let slowestMs: number | null = null;
|
|
301
|
+
let slowestLogNumber: number | null = null;
|
|
302
|
+
let heaviestTokens = 0;
|
|
303
|
+
let heaviestInputTokens = 0;
|
|
304
|
+
let heaviestOutputTokens = 0;
|
|
305
|
+
let heaviestLogNumber: number | null = null;
|
|
306
|
+
const slowThresholdMs = slowResponseThresholdSeconds * 1000;
|
|
307
|
+
|
|
308
|
+
for (let index = 0; index < logs.length; index += 1) {
|
|
309
|
+
const log = logs[index];
|
|
310
|
+
if (log === undefined) continue;
|
|
311
|
+
const logNumber = index + 1;
|
|
312
|
+
const stopReason = extractStopReason(log);
|
|
313
|
+
if (stopReason !== null) terminalStopReason = stopReason;
|
|
314
|
+
if (stopReason === "length") lengthStops += 1;
|
|
315
|
+
|
|
316
|
+
if (hasLogFailure(log)) {
|
|
317
|
+
failures += 1;
|
|
318
|
+
lastFailureLogNumber = logNumber;
|
|
319
|
+
lastFailure = {
|
|
320
|
+
logNumber,
|
|
321
|
+
status: log.responseStatus,
|
|
322
|
+
error: log.error ?? null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (log.responseStatus === null) pending += 1;
|
|
326
|
+
if (
|
|
327
|
+
slowResponseThresholdSeconds > 0 &&
|
|
328
|
+
log.elapsedMs !== null &&
|
|
329
|
+
log.elapsedMs >= slowThresholdMs
|
|
330
|
+
) {
|
|
331
|
+
slow += 1;
|
|
332
|
+
}
|
|
333
|
+
if (log.inputTokens !== null) totalIn += log.inputTokens;
|
|
334
|
+
if (log.outputTokens !== null) totalOut += log.outputTokens;
|
|
335
|
+
if (log.elapsedMs !== null && (slowestMs === null || log.elapsedMs > slowestMs)) {
|
|
336
|
+
slowestMs = log.elapsedMs;
|
|
337
|
+
slowestLogNumber = logNumber;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const inputTokens = log.inputTokens ?? 0;
|
|
341
|
+
const outputTokens = log.outputTokens ?? 0;
|
|
342
|
+
const requestTokens = inputTokens + outputTokens;
|
|
343
|
+
if (requestTokens > heaviestTokens) {
|
|
344
|
+
heaviestTokens = requestTokens;
|
|
345
|
+
heaviestInputTokens = inputTokens;
|
|
346
|
+
heaviestOutputTokens = outputTokens;
|
|
347
|
+
heaviestLogNumber = logNumber;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const status: SessionSlateStatus =
|
|
352
|
+
isLoading || (pending > 0 && failures === 0)
|
|
353
|
+
? "rolling"
|
|
354
|
+
: failures > 0
|
|
355
|
+
? "failed"
|
|
356
|
+
: slow > 0 || lengthStops > 0
|
|
357
|
+
? "watch"
|
|
358
|
+
: "clear";
|
|
359
|
+
const statusLabel =
|
|
360
|
+
status === "rolling"
|
|
361
|
+
? "Rolling"
|
|
362
|
+
: status === "failed"
|
|
363
|
+
? "Failure"
|
|
364
|
+
: status === "watch"
|
|
365
|
+
? "Watch"
|
|
366
|
+
: "Clear";
|
|
367
|
+
const finish = summarizeFinishReason({
|
|
368
|
+
stopReason: terminalStopReason,
|
|
369
|
+
pending,
|
|
370
|
+
lastFailureLogNumber,
|
|
371
|
+
});
|
|
372
|
+
const outcome = summarizeSessionOutcome({
|
|
373
|
+
pending,
|
|
374
|
+
failures,
|
|
375
|
+
lengthStops,
|
|
376
|
+
stopReason: terminalStopReason,
|
|
377
|
+
lastFailure,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
status,
|
|
382
|
+
statusLabel,
|
|
383
|
+
modelLabel: getModelLabel(logs),
|
|
384
|
+
providerLabel: getProviderLabel(logs),
|
|
385
|
+
requests: logs.length,
|
|
386
|
+
turns: countSessionTurns(logs, conversationGroups),
|
|
387
|
+
failures,
|
|
388
|
+
pending,
|
|
389
|
+
slow,
|
|
390
|
+
lengthStops,
|
|
391
|
+
timeRange: formatTimeRange(logs, timeDisplayFormat),
|
|
392
|
+
tokenLabel: `${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`,
|
|
393
|
+
finishLabel: finish.label,
|
|
394
|
+
finishDetail: finish.detail,
|
|
395
|
+
finishTone: finish.tone,
|
|
396
|
+
outcomeLabel: outcome.label,
|
|
397
|
+
outcomeDetail: outcome.detail,
|
|
398
|
+
outcomeTone: outcome.tone,
|
|
399
|
+
slowestLabel: slowestMs === null ? "n/a" : formatElapsedMs(slowestMs),
|
|
400
|
+
slowestDetail: slowestLogNumber === null ? "Latency pending" : `Request #${slowestLogNumber}`,
|
|
401
|
+
heaviestDetail:
|
|
402
|
+
heaviestLogNumber === null
|
|
403
|
+
? "Tokens pending"
|
|
404
|
+
: `#${heaviestLogNumber} ${formatTokenPair(heaviestInputTokens, heaviestOutputTokens)}`,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function resolveSessionContextScope({
|
|
409
|
+
pinnedSessionId,
|
|
410
|
+
selectedSession,
|
|
411
|
+
sessions,
|
|
412
|
+
logs,
|
|
413
|
+
}: {
|
|
414
|
+
pinnedSessionId: string | undefined;
|
|
415
|
+
selectedSession: string;
|
|
416
|
+
sessions: readonly string[];
|
|
417
|
+
logs: readonly CapturedLog[];
|
|
418
|
+
}): SessionContextScope | null {
|
|
419
|
+
if (pinnedSessionId !== undefined) {
|
|
420
|
+
const scopedLogs = logs.filter((log) => log.sessionId === pinnedSessionId);
|
|
421
|
+
if (scopedLogs.length === 0) return null;
|
|
422
|
+
return { label: pinnedSessionId, logs: scopedLogs, showBackLink: true };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (selectedSession !== "__all__") {
|
|
426
|
+
const scopedLogs = logs.filter((log) => log.sessionId === selectedSession);
|
|
427
|
+
if (scopedLogs.length === 0) return null;
|
|
428
|
+
return { label: selectedSession, logs: scopedLogs, showBackLink: true };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const singleSessionId = sessions.length === 1 ? (sessions[0] ?? null) : null;
|
|
432
|
+
if (singleSessionId !== null) {
|
|
433
|
+
const allVisibleLogsBelongToSession = logs.every((log) => log.sessionId === singleSessionId);
|
|
434
|
+
if (allVisibleLogsBelongToSession) {
|
|
435
|
+
return { label: singleSessionId, logs: [...logs], showBackLink: false };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const allVisibleLogsAreSessionless =
|
|
440
|
+
sessions.length === 0 && logs.length > 0 && logs.every((log) => log.sessionId === null);
|
|
441
|
+
if (allVisibleLogsAreSessionless) {
|
|
442
|
+
return { label: "Visible requests", logs: [...logs], showBackLink: false };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function formatPaginationStatus({
|
|
449
|
+
logs,
|
|
450
|
+
pagination,
|
|
451
|
+
}: {
|
|
452
|
+
logs: readonly CapturedLog[];
|
|
453
|
+
pagination: PaginationStatusControls;
|
|
454
|
+
}): string {
|
|
455
|
+
if (pagination.isLoading && logs.length === 0) return "Loading logs...";
|
|
456
|
+
if (logs.length === 0) return "No logs on this page";
|
|
457
|
+
|
|
458
|
+
const first = logs[0];
|
|
459
|
+
const last = logs[logs.length - 1];
|
|
460
|
+
const range =
|
|
461
|
+
first === undefined || last === undefined ? "Current page" : `#${first.id}-#${last.id}`;
|
|
462
|
+
if (pagination.total === null) {
|
|
463
|
+
return `Showing ${logs.length} logs (${range})`;
|
|
464
|
+
}
|
|
465
|
+
if (pagination.total <= logs.length) {
|
|
466
|
+
return `Showing all ${pagination.total} logs`;
|
|
467
|
+
}
|
|
468
|
+
return `Showing ${logs.length} of ${pagination.total} logs (${range})`;
|
|
469
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type ThemeMode = "light" | "dark" | "eye-care";
|
|
2
|
+
|
|
3
|
+
export const THEME_MODE_STORAGE_KEY = "agent-inspector.theme";
|
|
4
|
+
|
|
5
|
+
export function normalizeThemeMode(value: string | null): ThemeMode {
|
|
6
|
+
switch (value) {
|
|
7
|
+
case "light":
|
|
8
|
+
return "light";
|
|
9
|
+
case "dark":
|
|
10
|
+
return "dark";
|
|
11
|
+
case "eye-care":
|
|
12
|
+
return "eye-care";
|
|
13
|
+
case null:
|
|
14
|
+
return "dark";
|
|
15
|
+
default:
|
|
16
|
+
return "dark";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function applyThemeMode(themeMode: ThemeMode): void {
|
|
21
|
+
if (typeof document === "undefined") return;
|
|
22
|
+
const root = document.documentElement;
|
|
23
|
+
root.classList.toggle("dark", themeMode === "dark");
|
|
24
|
+
root.classList.toggle("eye-care", themeMode === "eye-care");
|
|
25
|
+
root.dataset.theme = themeMode;
|
|
26
|
+
root.style.colorScheme = themeMode === "dark" ? "dark" : "light";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readStoredThemeMode(): ThemeMode {
|
|
30
|
+
if (typeof window === "undefined") return "dark";
|
|
31
|
+
try {
|
|
32
|
+
return normalizeThemeMode(window.localStorage.getItem(THEME_MODE_STORAGE_KEY));
|
|
33
|
+
} catch {
|
|
34
|
+
return "dark";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function persistThemeMode(themeMode: ThemeMode): void {
|
|
39
|
+
if (typeof window === "undefined") return;
|
|
40
|
+
try {
|
|
41
|
+
window.localStorage.setItem(THEME_MODE_STORAGE_KEY, themeMode);
|
|
42
|
+
} catch {
|
|
43
|
+
// Local storage can be unavailable in hardened browser contexts.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export type UiScalePreference = "auto" | "100" | "125" | "150";
|
|
2
|
+
|
|
3
|
+
export type UiScaleEnvironment = {
|
|
4
|
+
viewportWidth: number;
|
|
5
|
+
screenWidth: number;
|
|
6
|
+
devicePixelRatio: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const UI_SCALE_STORAGE_KEY = "agent-inspector.uiScale";
|
|
10
|
+
export const UI_SCALE_AUTO_MAX_DPR = 1.25;
|
|
11
|
+
export const UI_SCALE_AUTO_MEDIUM_WIDTH = 2200;
|
|
12
|
+
export const UI_SCALE_AUTO_LARGE_WIDTH = 3000;
|
|
13
|
+
|
|
14
|
+
function positiveFinite(value: number, fallback: number): number {
|
|
15
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeUiScalePreference(value: string | null): UiScalePreference {
|
|
19
|
+
switch (value) {
|
|
20
|
+
case "auto":
|
|
21
|
+
return "auto";
|
|
22
|
+
case "100":
|
|
23
|
+
return "100";
|
|
24
|
+
case "125":
|
|
25
|
+
return "125";
|
|
26
|
+
case "150":
|
|
27
|
+
return "150";
|
|
28
|
+
case null:
|
|
29
|
+
return "auto";
|
|
30
|
+
default:
|
|
31
|
+
return "auto";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveUiScale(
|
|
36
|
+
preference: UiScalePreference,
|
|
37
|
+
environment: UiScaleEnvironment,
|
|
38
|
+
): number {
|
|
39
|
+
switch (preference) {
|
|
40
|
+
case "100":
|
|
41
|
+
return 1;
|
|
42
|
+
case "125":
|
|
43
|
+
return 1.25;
|
|
44
|
+
case "150":
|
|
45
|
+
return 1.5;
|
|
46
|
+
case "auto": {
|
|
47
|
+
const viewportWidth = positiveFinite(environment.viewportWidth, 1280);
|
|
48
|
+
const screenWidth = positiveFinite(environment.screenWidth, viewportWidth);
|
|
49
|
+
const devicePixelRatio = positiveFinite(environment.devicePixelRatio, 1);
|
|
50
|
+
const availableWidth = Math.min(viewportWidth, screenWidth);
|
|
51
|
+
|
|
52
|
+
// High-DPI laptops already receive operating-system scaling. Auto only
|
|
53
|
+
// enlarges spacious low-DPI desktop layouts, where dense telemetry is
|
|
54
|
+
// otherwise physically small.
|
|
55
|
+
if (devicePixelRatio > UI_SCALE_AUTO_MAX_DPR) return 1;
|
|
56
|
+
if (availableWidth >= UI_SCALE_AUTO_LARGE_WIDTH) return 1.25;
|
|
57
|
+
if (availableWidth >= UI_SCALE_AUTO_MEDIUM_WIDTH) return 1.125;
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatUiScale(scale: number): string {
|
|
64
|
+
return `${Math.round(scale * 100)}%`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readUiScaleEnvironment(): UiScaleEnvironment {
|
|
68
|
+
if (typeof window === "undefined") {
|
|
69
|
+
return { viewportWidth: 1280, screenWidth: 1280, devicePixelRatio: 1 };
|
|
70
|
+
}
|
|
71
|
+
const screenWidth =
|
|
72
|
+
typeof window.screen === "undefined" ? window.innerWidth : window.screen.availWidth;
|
|
73
|
+
return {
|
|
74
|
+
viewportWidth: window.innerWidth,
|
|
75
|
+
screenWidth,
|
|
76
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function applyUiScalePreference(preference: UiScalePreference): number {
|
|
81
|
+
const scale = resolveUiScale(preference, readUiScaleEnvironment());
|
|
82
|
+
if (typeof document === "undefined") return scale;
|
|
83
|
+
|
|
84
|
+
const root = document.documentElement;
|
|
85
|
+
root.dataset.uiScale = preference;
|
|
86
|
+
root.dataset.uiScaleResolved = String(Math.round(scale * 100));
|
|
87
|
+
root.style.setProperty("--inspector-ui-scale", String(scale));
|
|
88
|
+
root.style.setProperty("--inspector-root-font-size", `${String(16 * scale)}px`);
|
|
89
|
+
root.style.setProperty("--inspector-font-9", `${String(9 * scale)}px`);
|
|
90
|
+
root.style.setProperty("--inspector-font-10", `${String(10 * scale)}px`);
|
|
91
|
+
root.style.setProperty("--inspector-font-11", `${String(11 * scale)}px`);
|
|
92
|
+
root.style.setProperty("--inspector-font-13", `${String(13 * scale)}px`);
|
|
93
|
+
return scale;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function readStoredUiScalePreference(): UiScalePreference {
|
|
97
|
+
if (typeof window === "undefined") return "auto";
|
|
98
|
+
try {
|
|
99
|
+
return normalizeUiScalePreference(window.localStorage.getItem(UI_SCALE_STORAGE_KEY));
|
|
100
|
+
} catch {
|
|
101
|
+
return "auto";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function persistUiScalePreference(preference: UiScalePreference): void {
|
|
106
|
+
if (typeof window === "undefined") return;
|
|
107
|
+
try {
|
|
108
|
+
window.localStorage.setItem(UI_SCALE_STORAGE_KEY, preference);
|
|
109
|
+
} catch {
|
|
110
|
+
// Local storage can be unavailable in hardened browser contexts.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
applyThemeMode,
|
|
4
|
+
normalizeThemeMode,
|
|
5
|
+
persistThemeMode,
|
|
6
|
+
readStoredThemeMode,
|
|
7
|
+
THEME_MODE_STORAGE_KEY,
|
|
8
|
+
type ThemeMode,
|
|
9
|
+
} from "./themeMode";
|
|
10
|
+
|
|
11
|
+
export function useThemeMode(): {
|
|
12
|
+
themeMode: ThemeMode;
|
|
13
|
+
setThemeMode: (themeMode: ThemeMode) => void;
|
|
14
|
+
} {
|
|
15
|
+
const [themeMode, setThemeModeState] = useState<ThemeMode>(readStoredThemeMode);
|
|
16
|
+
|
|
17
|
+
const setThemeMode = useCallback((nextThemeMode: ThemeMode) => {
|
|
18
|
+
setThemeModeState(nextThemeMode);
|
|
19
|
+
applyThemeMode(nextThemeMode);
|
|
20
|
+
persistThemeMode(nextThemeMode);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
applyThemeMode(themeMode);
|
|
25
|
+
}, [themeMode]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
function handleStorage(event: StorageEvent): void {
|
|
29
|
+
if (event.key !== THEME_MODE_STORAGE_KEY) return;
|
|
30
|
+
const nextThemeMode = normalizeThemeMode(event.newValue);
|
|
31
|
+
setThemeModeState(nextThemeMode);
|
|
32
|
+
applyThemeMode(nextThemeMode);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
window.addEventListener("storage", handleStorage);
|
|
36
|
+
return () => window.removeEventListener("storage", handleStorage);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
return { themeMode, setThemeMode };
|
|
40
|
+
}
|