@tonyclaw/agent-inspector 3.0.23 → 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.
Files changed (47) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-CFedvZHU.js → CompareDrawer-BC52FYwn.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-WuChOrSS.js +127 -0
  4. package/.output/public/assets/{ReplayDialog-X-fRSKAu.js → ReplayDialog-BcKsarKt.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-S8qB5nVg.js → RequestAnatomy-DR0uGpCj.js} +1 -1
  6. package/.output/public/assets/{ResponseView-_rcOWSwJ.js → ResponseView-kxAAPfmn.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-Bc15AVpN.js → StreamingChunkSequence-YQR0PjQj.js} +1 -1
  8. package/.output/public/assets/{_sessionId-Cz-HW2Wf.js → _sessionId-DRl-Y9om.js} +1 -1
  9. package/.output/public/assets/{index-CEgwWY3E.js → index-CQbrc-hi.js} +71 -7
  10. package/.output/public/assets/{index-CXYBU0j8.js → index-CcSIBI4L.js} +1 -1
  11. package/.output/public/assets/index-DQeXi4Rv.css +1 -0
  12. package/.output/public/assets/{index-ClKXooQd.js → index-ZrMgyEZV.js} +1 -1
  13. package/.output/public/assets/{json-viewer-QfU4qGdd.js → json-viewer-CS5pwnEA.js} +1 -1
  14. package/.output/server/_libs/lucide-react.mjs +266 -234
  15. package/.output/server/{_sessionId-9FAmYZVf.mjs → _sessionId-BLk2xPkR.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-BLKjOzwD.mjs → CompareDrawer-Cw7Dseo3.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-Dcs2Mq8l.mjs → ProxyViewerContainer-D8SB0zyQ.mjs} +1124 -877
  18. package/.output/server/_ssr/{ReplayDialog-oyiF4LIo.mjs → ReplayDialog-B-YMhZkE.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-DkyQXSuj.mjs → RequestAnatomy-a9d8UTYZ.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-3NTDEiDh.mjs → ResponseView-PzaZS4CN.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-DRq-5Bgn.mjs → StreamingChunkSequence-C21_TCXk.mjs} +2 -2
  22. package/.output/server/_ssr/{index-dtMiNUa1.mjs → index-CrCeNP7H.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-BhUwicvp.mjs → json-viewer-Di2gxs9H.mjs} +3 -3
  25. package/.output/server/_ssr/{router-DO0OntUK.mjs → router-BJzdUbcH.mjs} +296 -91
  26. package/.output/server/{_tanstack-start-manifest_v-DO5v3BHY.mjs → _tanstack-start-manifest_v-CGBTdAYC.mjs} +1 -1
  27. package/.output/server/index.mjs +76 -76
  28. package/package.json +3 -2
  29. package/src/components/ProxyViewer.tsx +136 -460
  30. package/src/components/ProxyViewerContainer.tsx +5 -47
  31. package/src/components/alerts/AlertsDialog.tsx +6 -2
  32. package/src/components/providers/ProviderCard.tsx +20 -579
  33. package/src/components/providers/ProviderTestResultsView.tsx +732 -0
  34. package/src/components/proxy-viewer/LogEntry.tsx +5 -0
  35. package/src/components/proxy-viewer/LogEntryHeader.tsx +7 -0
  36. package/src/components/proxy-viewer/ToolTraceEvents.tsx +33 -36
  37. package/src/components/proxy-viewer/TurnGroup.tsx +1 -5
  38. package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +46 -0
  39. package/src/components/proxy-viewer/proxyViewerLogic.ts +469 -0
  40. package/src/lib/themeMode.ts +45 -0
  41. package/src/lib/uiScale.ts +112 -0
  42. package/src/lib/useThemeMode.ts +40 -0
  43. package/src/lib/useUiScale.ts +52 -0
  44. package/src/routes/__root.tsx +78 -1
  45. package/styles/globals.css +305 -23
  46. package/.output/public/assets/ProxyViewerContainer-BlCrBvdm.js +0 -127
  47. package/.output/public/assets/index-B_SIQ47i.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 { buildTurnGroups, buildValidPredecessors } from "./proxy-viewer/viewerState";
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
- type SessionSlateStatus = "rolling" | "clear" | "watch" | "failed";
197
- type SessionSlateMetricTone = "neutral" | "cool" | "watch" | "danger";
198
-
199
- type ConversationLogs = {
200
- logs: CapturedLog[];
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 ? "right-3" : "lg:right-[326px] xl:right-[346px]",
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)_340px] xl:grid-cols-[minmax(0,1fr)_360px]",
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">