@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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Suspense,
|
|
13
13
|
} from "react";
|
|
14
14
|
import {
|
|
15
|
+
ALargeSmall,
|
|
15
16
|
ArrowDownRight,
|
|
16
17
|
ArrowLeft,
|
|
17
18
|
ArrowUpRight,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
ChevronsRight,
|
|
26
27
|
Copy,
|
|
27
28
|
Download,
|
|
29
|
+
Eye,
|
|
28
30
|
FileJson,
|
|
29
31
|
Gauge,
|
|
30
32
|
Flag,
|
|
@@ -34,6 +36,8 @@ import {
|
|
|
34
36
|
RefreshCw,
|
|
35
37
|
Search,
|
|
36
38
|
Siren,
|
|
39
|
+
Moon,
|
|
40
|
+
Sun,
|
|
37
41
|
Timer,
|
|
38
42
|
Trash2,
|
|
39
43
|
Upload,
|
|
@@ -47,11 +51,23 @@ import { copyTextToClipboard } from "../lib/clipboard";
|
|
|
47
51
|
import type { InspectorGroupEvidence, InspectorGroupMember } from "../lib/groupContract";
|
|
48
52
|
import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
|
|
49
53
|
import { formatTimestampRange } from "../lib/timeDisplay";
|
|
50
|
-
import { extractStopReason, type StopReason } from "../lib/stopReason";
|
|
51
54
|
import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
|
|
55
|
+
import { useThemeMode } from "../lib/useThemeMode";
|
|
56
|
+
import { formatUiScale, normalizeUiScalePreference, type UiScalePreference } from "../lib/uiScale";
|
|
57
|
+
import { useUiScale } from "../lib/useUiScale";
|
|
52
58
|
import { useProviders } from "../lib/useProviders";
|
|
53
59
|
import packageJson from "../../package.json";
|
|
54
60
|
import { groupLogsByConversation, type ConversationGroupData } from "./proxy-viewer";
|
|
61
|
+
import {
|
|
62
|
+
buildSessionSlateStats,
|
|
63
|
+
formatPaginationStatus,
|
|
64
|
+
getExportActionVisibility,
|
|
65
|
+
resolveSessionContextScope,
|
|
66
|
+
type SessionContextScope,
|
|
67
|
+
type SessionSlateMetricTone,
|
|
68
|
+
type SessionSlateStats,
|
|
69
|
+
type SessionSlateStatus,
|
|
70
|
+
} from "./proxy-viewer/proxyViewerLogic";
|
|
55
71
|
|
|
56
72
|
import { CrabLogo } from "./ui/crab-logo";
|
|
57
73
|
import { crabVariants } from "./ui/crab-variants";
|
|
@@ -70,7 +86,7 @@ import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
|
70
86
|
import { LazyCompareDrawer } from "./proxy-viewer/lazy";
|
|
71
87
|
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
72
88
|
import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
|
|
73
|
-
import {
|
|
89
|
+
import { buildValidPredecessors } from "./proxy-viewer/viewerState";
|
|
74
90
|
import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
|
|
75
91
|
import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
|
|
76
92
|
import type {
|
|
@@ -107,26 +123,6 @@ function displayNumberForLog(
|
|
|
107
123
|
return displayNumberByLogId.get(log.id) ?? log.id;
|
|
108
124
|
}
|
|
109
125
|
|
|
110
|
-
export function shouldShowRawExport(captureMode: CaptureMode): boolean {
|
|
111
|
-
return captureMode === "full";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export type ExportActionVisibility = {
|
|
115
|
-
redacted: boolean;
|
|
116
|
-
raw: boolean;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
export function getExportActionVisibility(
|
|
120
|
-
captureMode: CaptureMode,
|
|
121
|
-
logCount: number,
|
|
122
|
-
): ExportActionVisibility {
|
|
123
|
-
const hasLogs = logCount > 0;
|
|
124
|
-
return {
|
|
125
|
-
redacted: hasLogs,
|
|
126
|
-
raw: hasLogs && shouldShowRawExport(captureMode),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
126
|
function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut: number } {
|
|
131
127
|
let totalIn = 0;
|
|
132
128
|
let totalOut = 0;
|
|
@@ -137,12 +133,6 @@ function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut:
|
|
|
137
133
|
return { totalIn, totalOut };
|
|
138
134
|
}
|
|
139
135
|
|
|
140
|
-
export type SessionContextScope = {
|
|
141
|
-
label: string;
|
|
142
|
-
logs: CapturedLog[];
|
|
143
|
-
showBackLink: boolean;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
136
|
export type SessionMembershipEvidence = {
|
|
147
137
|
groupId: string;
|
|
148
138
|
groupTitle: string;
|
|
@@ -177,13 +167,6 @@ export type SessionLoadProgress = {
|
|
|
177
167
|
loadedPages: number;
|
|
178
168
|
};
|
|
179
169
|
|
|
180
|
-
function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
|
|
181
|
-
const first = logs[0];
|
|
182
|
-
const last = logs[logs.length - 1];
|
|
183
|
-
if (first === undefined || last === undefined) return null;
|
|
184
|
-
return formatTimestampRange(first.timestamp, last.timestamp, timeDisplayFormat);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
170
|
function getFirstUserAgent(logs: CapturedLog[]): string | null {
|
|
188
171
|
for (const log of logs) {
|
|
189
172
|
if (log.userAgent !== null && log.userAgent !== undefined && log.userAgent !== "") {
|
|
@@ -193,366 +176,11 @@ function getFirstUserAgent(logs: CapturedLog[]): string | null {
|
|
|
193
176
|
return null;
|
|
194
177
|
}
|
|
195
178
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
type SessionSlateStats = {
|
|
204
|
-
status: SessionSlateStatus;
|
|
205
|
-
statusLabel: string;
|
|
206
|
-
modelLabel: string;
|
|
207
|
-
providerLabel: string;
|
|
208
|
-
requests: number;
|
|
209
|
-
turns: number;
|
|
210
|
-
failures: number;
|
|
211
|
-
pending: number;
|
|
212
|
-
slow: number;
|
|
213
|
-
lengthStops: number;
|
|
214
|
-
timeRange: string | null;
|
|
215
|
-
tokenLabel: string;
|
|
216
|
-
finishLabel: string;
|
|
217
|
-
finishDetail: string;
|
|
218
|
-
finishTone: SessionSlateMetricTone;
|
|
219
|
-
outcomeLabel: string;
|
|
220
|
-
outcomeDetail: string;
|
|
221
|
-
outcomeTone: SessionSlateMetricTone;
|
|
222
|
-
slowestLabel: string;
|
|
223
|
-
slowestDetail: string;
|
|
224
|
-
heaviestDetail: string;
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
function hasLogFailure(log: CapturedLog): boolean {
|
|
228
|
-
if (log.error !== null && log.error !== undefined && log.error.length > 0) return true;
|
|
229
|
-
return log.responseStatus !== null && log.responseStatus >= 400;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function getModelLabel(logs: readonly CapturedLog[]): string {
|
|
233
|
-
const models = new Set<string>();
|
|
234
|
-
for (const log of logs) {
|
|
235
|
-
if (log.model !== null && log.model.length > 0) {
|
|
236
|
-
models.add(log.model);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
const sorted = [...models].sort();
|
|
240
|
-
if (sorted.length === 0) return "Model unknown";
|
|
241
|
-
if (sorted.length === 1) return sorted[0] ?? "Model unknown";
|
|
242
|
-
return `${sorted.length} models`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function getProviderLabel(logs: readonly CapturedLog[]): string {
|
|
246
|
-
const providers = new Set<string>();
|
|
247
|
-
for (const log of logs) {
|
|
248
|
-
if (
|
|
249
|
-
log.providerName !== null &&
|
|
250
|
-
log.providerName !== undefined &&
|
|
251
|
-
log.providerName.length > 0
|
|
252
|
-
) {
|
|
253
|
-
providers.add(log.providerName);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const sorted = [...providers].sort();
|
|
257
|
-
if (sorted.length === 0) return "Provider unknown";
|
|
258
|
-
if (sorted.length === 1) return sorted[0] ?? "Provider unknown";
|
|
259
|
-
return `${sorted.length} providers`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function formatElapsedMs(ms: number): string {
|
|
263
|
-
if (ms < 1000) return `${ms}ms`;
|
|
264
|
-
if (ms < 60_000) {
|
|
265
|
-
const seconds = ms / 1000;
|
|
266
|
-
return `${seconds < 10 ? seconds.toFixed(1) : seconds.toFixed(0)}s`;
|
|
267
|
-
}
|
|
268
|
-
const totalSeconds = Math.round(ms / 1000);
|
|
269
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
270
|
-
const seconds = totalSeconds % 60;
|
|
271
|
-
return `${minutes}m ${seconds}s`;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function formatTokenPair(inputTokens: number, outputTokens: number): string {
|
|
275
|
-
return `${formatTokens(inputTokens)} in / ${formatTokens(outputTokens)} out`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function countSessionTurns(
|
|
279
|
-
logs: CapturedLog[],
|
|
280
|
-
conversationGroups: readonly ConversationLogs[],
|
|
281
|
-
): number {
|
|
282
|
-
if (logs.length === 0) return 0;
|
|
283
|
-
if (conversationGroups.length === 0) return buildTurnGroups(logs).length;
|
|
284
|
-
|
|
285
|
-
let turns = 0;
|
|
286
|
-
for (const group of conversationGroups) {
|
|
287
|
-
turns += buildTurnGroups(group.logs).length;
|
|
288
|
-
}
|
|
289
|
-
return turns;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
type FinishSummary = {
|
|
293
|
-
label: string;
|
|
294
|
-
detail: string;
|
|
295
|
-
tone: SessionSlateMetricTone;
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
type SessionOutcomeSummary = {
|
|
299
|
-
label: string;
|
|
300
|
-
detail: string;
|
|
301
|
-
tone: SessionSlateMetricTone;
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
type FailureSummary = {
|
|
305
|
-
logNumber: number;
|
|
306
|
-
status: number | null;
|
|
307
|
-
error: string | null;
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
function summarizeFinishReason({
|
|
311
|
-
stopReason,
|
|
312
|
-
pending,
|
|
313
|
-
lastFailureLogNumber,
|
|
314
|
-
}: {
|
|
315
|
-
stopReason: StopReason;
|
|
316
|
-
pending: number;
|
|
317
|
-
lastFailureLogNumber: number | null;
|
|
318
|
-
}): FinishSummary {
|
|
319
|
-
if (pending > 0) {
|
|
320
|
-
return {
|
|
321
|
-
label: "In flight",
|
|
322
|
-
detail: `${pending} request${pending === 1 ? "" : "s"} pending`,
|
|
323
|
-
tone: "cool",
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (lastFailureLogNumber !== null && stopReason === null) {
|
|
328
|
-
return {
|
|
329
|
-
label: "Error",
|
|
330
|
-
detail: `Last failed request #${lastFailureLogNumber}`,
|
|
331
|
-
tone: "danger",
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
switch (stopReason) {
|
|
336
|
-
case "end_turn":
|
|
337
|
-
return { label: "End turn", detail: "Assistant completed the turn", tone: "neutral" };
|
|
338
|
-
case "stop":
|
|
339
|
-
return { label: "Stop", detail: "Model reached a stop condition", tone: "neutral" };
|
|
340
|
-
case "length":
|
|
341
|
-
return { label: "Output limit", detail: "Turn ended at max output tokens", tone: "watch" };
|
|
342
|
-
case "tool_use":
|
|
343
|
-
return { label: "Tool call", detail: "Assistant requested a tool", tone: "cool" };
|
|
344
|
-
case null:
|
|
345
|
-
return { label: "Unknown", detail: "No finish reason captured", tone: "neutral" };
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function isClientAbortError(error: string | null): boolean {
|
|
350
|
-
if (error === null) return false;
|
|
351
|
-
const lower = error.toLowerCase();
|
|
352
|
-
return (
|
|
353
|
-
lower.includes("abort") ||
|
|
354
|
-
lower.includes("cancel") ||
|
|
355
|
-
lower.includes("client disconnected") ||
|
|
356
|
-
lower.includes("stream disconnected") ||
|
|
357
|
-
lower.includes("connection closed")
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function summarizeSessionOutcome({
|
|
362
|
-
pending,
|
|
363
|
-
failures,
|
|
364
|
-
lengthStops,
|
|
365
|
-
stopReason,
|
|
366
|
-
lastFailure,
|
|
367
|
-
}: {
|
|
368
|
-
pending: number;
|
|
369
|
-
failures: number;
|
|
370
|
-
lengthStops: number;
|
|
371
|
-
stopReason: StopReason;
|
|
372
|
-
lastFailure: FailureSummary | null;
|
|
373
|
-
}): SessionOutcomeSummary {
|
|
374
|
-
if (
|
|
375
|
-
lastFailure !== null &&
|
|
376
|
-
(lastFailure.status === 499 || isClientAbortError(lastFailure.error))
|
|
377
|
-
) {
|
|
378
|
-
return {
|
|
379
|
-
label: "Client aborted",
|
|
380
|
-
detail: `Request #${lastFailure.logNumber} ended before completion`,
|
|
381
|
-
tone: "watch",
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (pending > 0) {
|
|
386
|
-
return {
|
|
387
|
-
label: "In flight",
|
|
388
|
-
detail: `${pending} request${pending === 1 ? "" : "s"} still running`,
|
|
389
|
-
tone: "cool",
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (lastFailure !== null && lastFailure.status !== null && lastFailure.status >= 500) {
|
|
394
|
-
return {
|
|
395
|
-
label: "Provider error",
|
|
396
|
-
detail: `Request #${lastFailure.logNumber} returned HTTP ${lastFailure.status}`,
|
|
397
|
-
tone: "danger",
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (failures > 0 && lastFailure !== null) {
|
|
402
|
-
return {
|
|
403
|
-
label: "Failed",
|
|
404
|
-
detail: `Request #${lastFailure.logNumber} needs attention`,
|
|
405
|
-
tone: "danger",
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (lengthStops > 0 || stopReason === "length") {
|
|
410
|
-
return {
|
|
411
|
-
label: "Output limited",
|
|
412
|
-
detail: "Model stopped at the output token limit",
|
|
413
|
-
tone: "watch",
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
switch (stopReason) {
|
|
418
|
-
case "end_turn":
|
|
419
|
-
case "stop":
|
|
420
|
-
return { label: "Completed", detail: "Assistant completed normally", tone: "neutral" };
|
|
421
|
-
case "tool_use":
|
|
422
|
-
return { label: "Tool requested", detail: "Assistant handed off to a tool", tone: "cool" };
|
|
423
|
-
case null:
|
|
424
|
-
return { label: "Unknown", detail: "No terminal signal captured", tone: "neutral" };
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function buildSessionSlateStats({
|
|
429
|
-
logs,
|
|
430
|
-
conversationGroups,
|
|
431
|
-
isLoading,
|
|
432
|
-
timeDisplayFormat,
|
|
433
|
-
slowResponseThresholdSeconds,
|
|
434
|
-
}: {
|
|
435
|
-
logs: CapturedLog[];
|
|
436
|
-
conversationGroups: readonly ConversationLogs[];
|
|
437
|
-
isLoading: boolean;
|
|
438
|
-
timeDisplayFormat: TimeDisplayFormat;
|
|
439
|
-
slowResponseThresholdSeconds: number;
|
|
440
|
-
}): SessionSlateStats {
|
|
441
|
-
let failures = 0;
|
|
442
|
-
let pending = 0;
|
|
443
|
-
let slow = 0;
|
|
444
|
-
let lengthStops = 0;
|
|
445
|
-
let totalIn = 0;
|
|
446
|
-
let totalOut = 0;
|
|
447
|
-
let terminalStopReason: StopReason = null;
|
|
448
|
-
let lastFailureLogNumber: number | null = null;
|
|
449
|
-
let lastFailure: FailureSummary | null = null;
|
|
450
|
-
let slowestMs: number | null = null;
|
|
451
|
-
let slowestLogNumber: number | null = null;
|
|
452
|
-
let heaviestTokens = 0;
|
|
453
|
-
let heaviestInputTokens = 0;
|
|
454
|
-
let heaviestOutputTokens = 0;
|
|
455
|
-
let heaviestLogNumber: number | null = null;
|
|
456
|
-
const slowThresholdMs = slowResponseThresholdSeconds * 1000;
|
|
457
|
-
|
|
458
|
-
for (let index = 0; index < logs.length; index += 1) {
|
|
459
|
-
const log = logs[index];
|
|
460
|
-
if (log === undefined) continue;
|
|
461
|
-
const logNumber = index + 1;
|
|
462
|
-
const stopReason = extractStopReason(log);
|
|
463
|
-
if (stopReason !== null) terminalStopReason = stopReason;
|
|
464
|
-
if (stopReason === "length") lengthStops += 1;
|
|
465
|
-
|
|
466
|
-
if (hasLogFailure(log)) {
|
|
467
|
-
failures += 1;
|
|
468
|
-
lastFailureLogNumber = logNumber;
|
|
469
|
-
lastFailure = {
|
|
470
|
-
logNumber,
|
|
471
|
-
status: log.responseStatus,
|
|
472
|
-
error: log.error ?? null,
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
if (log.responseStatus === null) pending += 1;
|
|
476
|
-
if (
|
|
477
|
-
slowResponseThresholdSeconds > 0 &&
|
|
478
|
-
log.elapsedMs !== null &&
|
|
479
|
-
log.elapsedMs >= slowThresholdMs
|
|
480
|
-
) {
|
|
481
|
-
slow += 1;
|
|
482
|
-
}
|
|
483
|
-
if (log.inputTokens !== null) totalIn += log.inputTokens;
|
|
484
|
-
if (log.outputTokens !== null) totalOut += log.outputTokens;
|
|
485
|
-
if (log.elapsedMs !== null && (slowestMs === null || log.elapsedMs > slowestMs)) {
|
|
486
|
-
slowestMs = log.elapsedMs;
|
|
487
|
-
slowestLogNumber = logNumber;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const inputTokens = log.inputTokens ?? 0;
|
|
491
|
-
const outputTokens = log.outputTokens ?? 0;
|
|
492
|
-
const requestTokens = inputTokens + outputTokens;
|
|
493
|
-
if (requestTokens > heaviestTokens) {
|
|
494
|
-
heaviestTokens = requestTokens;
|
|
495
|
-
heaviestInputTokens = inputTokens;
|
|
496
|
-
heaviestOutputTokens = outputTokens;
|
|
497
|
-
heaviestLogNumber = logNumber;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const status: SessionSlateStatus =
|
|
502
|
-
isLoading || (pending > 0 && failures === 0)
|
|
503
|
-
? "rolling"
|
|
504
|
-
: failures > 0
|
|
505
|
-
? "failed"
|
|
506
|
-
: slow > 0 || lengthStops > 0
|
|
507
|
-
? "watch"
|
|
508
|
-
: "clear";
|
|
509
|
-
const statusLabel =
|
|
510
|
-
status === "rolling"
|
|
511
|
-
? "Rolling"
|
|
512
|
-
: status === "failed"
|
|
513
|
-
? "Failure"
|
|
514
|
-
: status === "watch"
|
|
515
|
-
? "Watch"
|
|
516
|
-
: "Clear";
|
|
517
|
-
const finish = summarizeFinishReason({
|
|
518
|
-
stopReason: terminalStopReason,
|
|
519
|
-
pending,
|
|
520
|
-
lastFailureLogNumber,
|
|
521
|
-
});
|
|
522
|
-
const outcome = summarizeSessionOutcome({
|
|
523
|
-
pending,
|
|
524
|
-
failures,
|
|
525
|
-
lengthStops,
|
|
526
|
-
stopReason: terminalStopReason,
|
|
527
|
-
lastFailure,
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
return {
|
|
531
|
-
status,
|
|
532
|
-
statusLabel,
|
|
533
|
-
modelLabel: getModelLabel(logs),
|
|
534
|
-
providerLabel: getProviderLabel(logs),
|
|
535
|
-
requests: logs.length,
|
|
536
|
-
turns: countSessionTurns(logs, conversationGroups),
|
|
537
|
-
failures,
|
|
538
|
-
pending,
|
|
539
|
-
slow,
|
|
540
|
-
lengthStops,
|
|
541
|
-
timeRange: formatTimeRange(logs, timeDisplayFormat),
|
|
542
|
-
tokenLabel: `${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`,
|
|
543
|
-
finishLabel: finish.label,
|
|
544
|
-
finishDetail: finish.detail,
|
|
545
|
-
finishTone: finish.tone,
|
|
546
|
-
outcomeLabel: outcome.label,
|
|
547
|
-
outcomeDetail: outcome.detail,
|
|
548
|
-
outcomeTone: outcome.tone,
|
|
549
|
-
slowestLabel: slowestMs === null ? "n/a" : formatElapsedMs(slowestMs),
|
|
550
|
-
slowestDetail: slowestLogNumber === null ? "Latency pending" : `Request #${slowestLogNumber}`,
|
|
551
|
-
heaviestDetail:
|
|
552
|
-
heaviestLogNumber === null
|
|
553
|
-
? "Tokens pending"
|
|
554
|
-
: `#${heaviestLogNumber} ${formatTokenPair(heaviestInputTokens, heaviestOutputTokens)}`,
|
|
555
|
-
};
|
|
179
|
+
function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
|
|
180
|
+
const first = logs[0];
|
|
181
|
+
const last = logs[logs.length - 1];
|
|
182
|
+
if (first === undefined || last === undefined) return null;
|
|
183
|
+
return formatTimestampRange(first.timestamp, last.timestamp, timeDisplayFormat);
|
|
556
184
|
}
|
|
557
185
|
|
|
558
186
|
function slateMetricToneClass(tone: SessionSlateMetricTone): string {
|
|
@@ -681,46 +309,6 @@ function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Eleme
|
|
|
681
309
|
);
|
|
682
310
|
}
|
|
683
311
|
|
|
684
|
-
export function resolveSessionContextScope({
|
|
685
|
-
pinnedSessionId,
|
|
686
|
-
selectedSession,
|
|
687
|
-
sessions,
|
|
688
|
-
logs,
|
|
689
|
-
}: {
|
|
690
|
-
pinnedSessionId: string | undefined;
|
|
691
|
-
selectedSession: string;
|
|
692
|
-
sessions: readonly string[];
|
|
693
|
-
logs: readonly CapturedLog[];
|
|
694
|
-
}): SessionContextScope | null {
|
|
695
|
-
if (pinnedSessionId !== undefined) {
|
|
696
|
-
const scopedLogs = logs.filter((log) => log.sessionId === pinnedSessionId);
|
|
697
|
-
if (scopedLogs.length === 0) return null;
|
|
698
|
-
return { label: pinnedSessionId, logs: scopedLogs, showBackLink: true };
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (selectedSession !== "__all__") {
|
|
702
|
-
const scopedLogs = logs.filter((log) => log.sessionId === selectedSession);
|
|
703
|
-
if (scopedLogs.length === 0) return null;
|
|
704
|
-
return { label: selectedSession, logs: scopedLogs, showBackLink: true };
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const singleSessionId = sessions.length === 1 ? (sessions[0] ?? null) : null;
|
|
708
|
-
if (singleSessionId !== null) {
|
|
709
|
-
const allVisibleLogsBelongToSession = logs.every((log) => log.sessionId === singleSessionId);
|
|
710
|
-
if (allVisibleLogsBelongToSession) {
|
|
711
|
-
return { label: singleSessionId, logs: [...logs], showBackLink: false };
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const allVisibleLogsAreSessionless =
|
|
716
|
-
sessions.length === 0 && logs.length > 0 && logs.every((log) => log.sessionId === null);
|
|
717
|
-
if (allVisibleLogsAreSessionless) {
|
|
718
|
-
return { label: "Visible requests", logs: [...logs], showBackLink: false };
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
312
|
function formatPercent(value: number | null): string {
|
|
725
313
|
if (value === null) return "Unknown";
|
|
726
314
|
const percent = value * 100;
|
|
@@ -969,29 +557,6 @@ function SessionMembershipPanel({
|
|
|
969
557
|
);
|
|
970
558
|
}
|
|
971
559
|
|
|
972
|
-
export function formatPaginationStatus({
|
|
973
|
-
logs,
|
|
974
|
-
pagination,
|
|
975
|
-
}: {
|
|
976
|
-
logs: readonly CapturedLog[];
|
|
977
|
-
pagination: LogPaginationControls;
|
|
978
|
-
}): string {
|
|
979
|
-
if (pagination.isLoading && logs.length === 0) return "Loading logs...";
|
|
980
|
-
if (logs.length === 0) return "No logs on this page";
|
|
981
|
-
|
|
982
|
-
const first = logs[0];
|
|
983
|
-
const last = logs[logs.length - 1];
|
|
984
|
-
const range =
|
|
985
|
-
first === undefined || last === undefined ? "Current page" : `#${first.id}-#${last.id}`;
|
|
986
|
-
if (pagination.total === null) {
|
|
987
|
-
return `Showing ${logs.length} logs (${range})`;
|
|
988
|
-
}
|
|
989
|
-
if (pagination.total <= logs.length) {
|
|
990
|
-
return `Showing all ${pagination.total} logs`;
|
|
991
|
-
}
|
|
992
|
-
return `Showing ${logs.length} of ${pagination.total} logs (${range})`;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
560
|
function LogPaginationButton({
|
|
996
561
|
label,
|
|
997
562
|
title,
|
|
@@ -1317,16 +882,109 @@ function CrabFamily({
|
|
|
1317
882
|
);
|
|
1318
883
|
}
|
|
1319
884
|
|
|
885
|
+
function ThemeModeToggle(): JSX.Element {
|
|
886
|
+
const { themeMode, setThemeMode } = useThemeMode();
|
|
887
|
+
|
|
888
|
+
return (
|
|
889
|
+
<div
|
|
890
|
+
className="border-border/70 bg-muted/70 inline-flex h-8 items-center rounded-md border p-0.5 shadow-sm"
|
|
891
|
+
aria-label="Theme mode"
|
|
892
|
+
role="group"
|
|
893
|
+
>
|
|
894
|
+
<button
|
|
895
|
+
type="button"
|
|
896
|
+
onClick={() => setThemeMode("light")}
|
|
897
|
+
className={cn(
|
|
898
|
+
"inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground",
|
|
899
|
+
themeMode === "light" && "bg-background text-foreground shadow-sm",
|
|
900
|
+
)}
|
|
901
|
+
aria-label="Use light mode"
|
|
902
|
+
aria-pressed={themeMode === "light"}
|
|
903
|
+
title="Light mode"
|
|
904
|
+
>
|
|
905
|
+
<Sun className="size-3.5" />
|
|
906
|
+
</button>
|
|
907
|
+
<button
|
|
908
|
+
type="button"
|
|
909
|
+
onClick={() => setThemeMode("eye-care")}
|
|
910
|
+
className={cn(
|
|
911
|
+
"inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground",
|
|
912
|
+
themeMode === "eye-care" && "bg-background text-foreground shadow-sm",
|
|
913
|
+
)}
|
|
914
|
+
aria-label="Use eye care mode"
|
|
915
|
+
aria-pressed={themeMode === "eye-care"}
|
|
916
|
+
title="Eye care mode"
|
|
917
|
+
>
|
|
918
|
+
<Eye className="size-3.5" />
|
|
919
|
+
</button>
|
|
920
|
+
<button
|
|
921
|
+
type="button"
|
|
922
|
+
onClick={() => setThemeMode("dark")}
|
|
923
|
+
className={cn(
|
|
924
|
+
"inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground",
|
|
925
|
+
themeMode === "dark" && "bg-background text-foreground shadow-sm",
|
|
926
|
+
)}
|
|
927
|
+
aria-label="Use dark mode"
|
|
928
|
+
aria-pressed={themeMode === "dark"}
|
|
929
|
+
title="Dark mode"
|
|
930
|
+
>
|
|
931
|
+
<Moon className="size-3.5" />
|
|
932
|
+
</button>
|
|
933
|
+
</div>
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function UiScaleControl({
|
|
938
|
+
preference,
|
|
939
|
+
resolvedScale,
|
|
940
|
+
onChange,
|
|
941
|
+
}: {
|
|
942
|
+
preference: UiScalePreference;
|
|
943
|
+
resolvedScale: number;
|
|
944
|
+
onChange: (preference: UiScalePreference) => void;
|
|
945
|
+
}): JSX.Element {
|
|
946
|
+
const resolvedLabel = formatUiScale(resolvedScale);
|
|
947
|
+
const currentLabel = preference === "auto" ? `Auto (${resolvedLabel})` : `${preference}%`;
|
|
948
|
+
|
|
949
|
+
return (
|
|
950
|
+
<Select
|
|
951
|
+
value={preference}
|
|
952
|
+
onValueChange={(value) => onChange(normalizeUiScalePreference(value))}
|
|
953
|
+
>
|
|
954
|
+
<SelectTrigger
|
|
955
|
+
size="sm"
|
|
956
|
+
className="bg-white/[0.035] hover:bg-white/[0.065] h-8 min-w-[4rem] justify-center gap-1 border-0 px-2 py-0 text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] [&>svg:last-child]:hidden"
|
|
957
|
+
aria-label={`Interface size: ${currentLabel}`}
|
|
958
|
+
>
|
|
959
|
+
<ALargeSmall className="size-3.5" />
|
|
960
|
+
<span className="font-mono text-[10px] font-semibold tabular-nums">{resolvedLabel}</span>
|
|
961
|
+
</SelectTrigger>
|
|
962
|
+
<SelectContent align="end">
|
|
963
|
+
<SelectItem value="auto">Auto</SelectItem>
|
|
964
|
+
<SelectItem value="100">100%</SelectItem>
|
|
965
|
+
<SelectItem value="125">125%</SelectItem>
|
|
966
|
+
<SelectItem value="150">150%</SelectItem>
|
|
967
|
+
</SelectContent>
|
|
968
|
+
</Select>
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
1320
972
|
function BrandHeader({
|
|
1321
973
|
compact,
|
|
1322
974
|
crabEntrancePhase,
|
|
1323
975
|
currentSessionId,
|
|
1324
976
|
logCount,
|
|
977
|
+
uiScalePreference,
|
|
978
|
+
resolvedUiScale,
|
|
979
|
+
onUiScaleChange,
|
|
1325
980
|
}: {
|
|
1326
981
|
compact: boolean;
|
|
1327
982
|
crabEntrancePhase: "hidden" | "playing" | "done";
|
|
1328
983
|
currentSessionId: string | null;
|
|
1329
984
|
logCount: number;
|
|
985
|
+
uiScalePreference: UiScalePreference;
|
|
986
|
+
resolvedUiScale: number;
|
|
987
|
+
onUiScaleChange: (preference: UiScalePreference) => void;
|
|
1330
988
|
}): JSX.Element {
|
|
1331
989
|
if (compact) {
|
|
1332
990
|
return (
|
|
@@ -1347,6 +1005,12 @@ function BrandHeader({
|
|
|
1347
1005
|
</div>
|
|
1348
1006
|
</div>
|
|
1349
1007
|
<div className="flex shrink-0 items-center gap-1">
|
|
1008
|
+
<ThemeModeToggle />
|
|
1009
|
+
<UiScaleControl
|
|
1010
|
+
preference={uiScalePreference}
|
|
1011
|
+
resolvedScale={resolvedUiScale}
|
|
1012
|
+
onChange={onUiScaleChange}
|
|
1013
|
+
/>
|
|
1350
1014
|
<AlertsDialog />
|
|
1351
1015
|
<GroupsDialog />
|
|
1352
1016
|
<AgentLabDialog currentSessionId={currentSessionId} logCount={logCount} />
|
|
@@ -1377,6 +1041,12 @@ function BrandHeader({
|
|
|
1377
1041
|
</span>
|
|
1378
1042
|
</h1>
|
|
1379
1043
|
<div className="flex flex-1 justify-end gap-1">
|
|
1044
|
+
<ThemeModeToggle />
|
|
1045
|
+
<UiScaleControl
|
|
1046
|
+
preference={uiScalePreference}
|
|
1047
|
+
resolvedScale={resolvedUiScale}
|
|
1048
|
+
onChange={onUiScaleChange}
|
|
1049
|
+
/>
|
|
1380
1050
|
<AlertsDialog />
|
|
1381
1051
|
<GroupsDialog />
|
|
1382
1052
|
<AgentLabDialog currentSessionId={currentSessionId} logCount={logCount} />
|
|
@@ -2049,6 +1719,7 @@ export function ProxyViewer({
|
|
|
2049
1719
|
}: ProxyViewerProps): JSX.Element {
|
|
2050
1720
|
const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
|
|
2051
1721
|
const { providers } = useProviders();
|
|
1722
|
+
const { uiScalePreference, resolvedUiScale, setUiScalePreference } = useUiScale();
|
|
2052
1723
|
const [exporting, setExporting] = useState<ExportMode | null>(null);
|
|
2053
1724
|
const [exportError, setExportError] = useState<string | null>(null);
|
|
2054
1725
|
const [importing, setImporting] = useState(false);
|
|
@@ -2334,7 +2005,9 @@ export function ProxyViewer({
|
|
|
2334
2005
|
onClick={toggleDetailsPanelCollapsed}
|
|
2335
2006
|
className={cn(
|
|
2336
2007
|
"group fixed top-1/2 right-3 z-40 inline-flex h-14 w-7 -translate-y-1/2 items-center justify-center rounded-l-md border border-white/[0.08] bg-background/90 text-muted-foreground shadow-[0_12px_34px_rgba(0,0,0,0.26),inset_0_1px_0_rgba(255,255,255,0.06)] backdrop-blur transition-[right,background-color,color,border-color] duration-200 hover:border-white/[0.16] hover:bg-white/[0.07] hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
|
|
2337
|
-
detailsPanelCollapsed
|
|
2008
|
+
detailsPanelCollapsed
|
|
2009
|
+
? "right-3"
|
|
2010
|
+
: "lg:right-[calc(21.25rem-0.875rem)] xl:right-[calc(22.5rem-0.875rem)]",
|
|
2338
2011
|
)}
|
|
2339
2012
|
aria-label={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
|
|
2340
2013
|
title={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
|
|
@@ -2352,7 +2025,7 @@ export function ProxyViewer({
|
|
|
2352
2025
|
"grid w-full min-w-0 grid-cols-1 lg:min-h-0 lg:flex-1 lg:overflow-hidden",
|
|
2353
2026
|
detailsPanelCollapsed
|
|
2354
2027
|
? "lg:grid-cols-[minmax(0,1fr)]"
|
|
2355
|
-
: "lg:grid-cols-[minmax(0,1fr)
|
|
2028
|
+
: "lg:grid-cols-[minmax(0,1fr)_21.25rem] xl:grid-cols-[minmax(0,1fr)_22.5rem]",
|
|
2356
2029
|
)}
|
|
2357
2030
|
>
|
|
2358
2031
|
{!detailsPanelCollapsed && (
|
|
@@ -2363,6 +2036,9 @@ export function ProxyViewer({
|
|
|
2363
2036
|
crabEntrancePhase={crabEntrancePhase}
|
|
2364
2037
|
currentSessionId={labSessionId}
|
|
2365
2038
|
logCount={logs.length}
|
|
2039
|
+
uiScalePreference={uiScalePreference}
|
|
2040
|
+
resolvedUiScale={resolvedUiScale}
|
|
2041
|
+
onUiScaleChange={setUiScalePreference}
|
|
2366
2042
|
/>
|
|
2367
2043
|
|
|
2368
2044
|
<SidebarPanel label="Session">
|