@tonyclaw/agent-inspector 3.0.23 → 3.0.25

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 (49) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-CFedvZHU.js → CompareDrawer-BnvOPwpe.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-DcqFqo1m.js +127 -0
  4. package/.output/public/assets/{ReplayDialog-X-fRSKAu.js → ReplayDialog-BJgoQWdW.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-S8qB5nVg.js → RequestAnatomy-CpBZEHnM.js} +1 -1
  6. package/.output/public/assets/{ResponseView-_rcOWSwJ.js → ResponseView-CWmyRb2M.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-Bc15AVpN.js → StreamingChunkSequence-BWhC8bwx.js} +1 -1
  8. package/.output/public/assets/{_sessionId-Cz-HW2Wf.js → _sessionId-Cq-2OqUU.js} +1 -1
  9. package/.output/public/assets/{index-CEgwWY3E.js → index-BKi9IRtV.js} +71 -7
  10. package/.output/public/assets/index-BMYuDL5X.css +1 -0
  11. package/.output/public/assets/{index-CXYBU0j8.js → index-Cf8O0Oos.js} +1 -1
  12. package/.output/public/assets/{index-ClKXooQd.js → index-DdgTJ4QJ.js} +1 -1
  13. package/.output/public/assets/{json-viewer-QfU4qGdd.js → json-viewer-Bqu_4hmm.js} +1 -1
  14. package/.output/server/_libs/lucide-react.mjs +306 -246
  15. package/.output/server/{_sessionId-9FAmYZVf.mjs → _sessionId-Ba7H3Gjw.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-BLKjOzwD.mjs → CompareDrawer-aH59Ab6x.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-Dcs2Mq8l.mjs → ProxyViewerContainer-By-I0tJv.mjs} +1791 -1259
  18. package/.output/server/_ssr/{ReplayDialog-oyiF4LIo.mjs → ReplayDialog-BO4Vy0et.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-DkyQXSuj.mjs → RequestAnatomy-FtOr7vSS.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-3NTDEiDh.mjs → ResponseView-cvw6ajue.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-DRq-5Bgn.mjs → StreamingChunkSequence-Oc8S2kD6.mjs} +3 -3
  22. package/.output/server/_ssr/{index-dtMiNUa1.mjs → index-BDRlQheR.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-BhUwicvp.mjs → json-viewer-Cd6trhQr.mjs} +3 -3
  25. package/.output/server/_ssr/{router-DO0OntUK.mjs → router-bofP-Ekl.mjs} +296 -91
  26. package/.output/server/{_tanstack-start-manifest_v-DO5v3BHY.mjs → _tanstack-start-manifest_v-BPuf86qB.mjs} +1 -1
  27. package/.output/server/index.mjs +76 -76
  28. package/package.json +3 -2
  29. package/src/components/ProxyViewer.tsx +138 -740
  30. package/src/components/ProxyViewerContainer.tsx +5 -47
  31. package/src/components/alerts/AlertsDialog.tsx +6 -2
  32. package/src/components/inspector-pet/InspectorPet.tsx +390 -0
  33. package/src/components/inspector-pet/inspectorPetLogic.ts +273 -0
  34. package/src/components/providers/ProviderCard.tsx +20 -579
  35. package/src/components/providers/ProviderTestResultsView.tsx +732 -0
  36. package/src/components/proxy-viewer/LogEntry.tsx +5 -0
  37. package/src/components/proxy-viewer/LogEntryHeader.tsx +7 -0
  38. package/src/components/proxy-viewer/ToolTraceEvents.tsx +33 -36
  39. package/src/components/proxy-viewer/TurnGroup.tsx +1 -5
  40. package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +46 -0
  41. package/src/components/proxy-viewer/proxyViewerLogic.ts +469 -0
  42. package/src/lib/themeMode.ts +45 -0
  43. package/src/lib/uiScale.ts +112 -0
  44. package/src/lib/useThemeMode.ts +40 -0
  45. package/src/lib/useUiScale.ts +52 -0
  46. package/src/routes/__root.tsx +78 -1
  47. package/styles/globals.css +403 -24
  48. package/.output/public/assets/ProxyViewerContainer-BlCrBvdm.js +0 -127
  49. package/.output/public/assets/index-B_SIQ47i.css +0 -1
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  type ChangeEvent,
3
- type CSSProperties,
4
3
  type JSX,
5
- type PointerEvent as ReactPointerEvent,
6
4
  type ReactNode,
7
5
  useCallback,
8
6
  useEffect,
@@ -12,6 +10,7 @@ import {
12
10
  Suspense,
13
11
  } from "react";
14
12
  import {
13
+ ALargeSmall,
15
14
  ArrowDownRight,
16
15
  ArrowLeft,
17
16
  ArrowUpRight,
@@ -25,6 +24,7 @@ import {
25
24
  ChevronsRight,
26
25
  Copy,
27
26
  Download,
27
+ Eye,
28
28
  FileJson,
29
29
  Gauge,
30
30
  Flag,
@@ -34,6 +34,8 @@ import {
34
34
  RefreshCw,
35
35
  Search,
36
36
  Siren,
37
+ Moon,
38
+ Sun,
37
39
  Timer,
38
40
  Trash2,
39
41
  Upload,
@@ -47,11 +49,23 @@ import { copyTextToClipboard } from "../lib/clipboard";
47
49
  import type { InspectorGroupEvidence, InspectorGroupMember } from "../lib/groupContract";
48
50
  import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
49
51
  import { formatTimestampRange } from "../lib/timeDisplay";
50
- import { extractStopReason, type StopReason } from "../lib/stopReason";
51
52
  import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
53
+ import { useThemeMode } from "../lib/useThemeMode";
54
+ import { formatUiScale, normalizeUiScalePreference, type UiScalePreference } from "../lib/uiScale";
55
+ import { useUiScale } from "../lib/useUiScale";
52
56
  import { useProviders } from "../lib/useProviders";
53
57
  import packageJson from "../../package.json";
54
58
  import { groupLogsByConversation, type ConversationGroupData } from "./proxy-viewer";
59
+ import {
60
+ buildSessionSlateStats,
61
+ formatPaginationStatus,
62
+ getExportActionVisibility,
63
+ resolveSessionContextScope,
64
+ type SessionContextScope,
65
+ type SessionSlateMetricTone,
66
+ type SessionSlateStats,
67
+ type SessionSlateStatus,
68
+ } from "./proxy-viewer/proxyViewerLogic";
55
69
 
56
70
  import { CrabLogo } from "./ui/crab-logo";
57
71
  import { crabVariants } from "./ui/crab-variants";
@@ -65,12 +79,13 @@ import { AlertsDialog } from "./alerts/AlertsDialog";
65
79
  import { GroupsDialog } from "./groups/GroupsDialog";
66
80
  import { SettingsDialog } from "./providers/SettingsDialog";
67
81
  import { AgentLabDialog } from "./ecosystem/AgentLabDialog";
82
+ import { InspectorPet } from "./inspector-pet/InspectorPet";
68
83
  import { PiAgentPanel } from "./pi-agent/PiAgentPanel";
69
84
  import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
70
85
  import { LazyCompareDrawer } from "./proxy-viewer/lazy";
71
86
  import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
72
87
  import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
73
- import { buildTurnGroups, buildValidPredecessors } from "./proxy-viewer/viewerState";
88
+ import { buildValidPredecessors } from "./proxy-viewer/viewerState";
74
89
  import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
75
90
  import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
76
91
  import type {
@@ -107,26 +122,6 @@ function displayNumberForLog(
107
122
  return displayNumberByLogId.get(log.id) ?? log.id;
108
123
  }
109
124
 
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
125
  function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut: number } {
131
126
  let totalIn = 0;
132
127
  let totalOut = 0;
@@ -137,12 +132,6 @@ function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut:
137
132
  return { totalIn, totalOut };
138
133
  }
139
134
 
140
- export type SessionContextScope = {
141
- label: string;
142
- logs: CapturedLog[];
143
- showBackLink: boolean;
144
- };
145
-
146
135
  export type SessionMembershipEvidence = {
147
136
  groupId: string;
148
137
  groupTitle: string;
@@ -177,13 +166,6 @@ export type SessionLoadProgress = {
177
166
  loadedPages: number;
178
167
  };
179
168
 
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
169
  function getFirstUserAgent(logs: CapturedLog[]): string | null {
188
170
  for (const log of logs) {
189
171
  if (log.userAgent !== null && log.userAgent !== undefined && log.userAgent !== "") {
@@ -193,366 +175,11 @@ function getFirstUserAgent(logs: CapturedLog[]): string | null {
193
175
  return null;
194
176
  }
195
177
 
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
- };
178
+ function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
179
+ const first = logs[0];
180
+ const last = logs[logs.length - 1];
181
+ if (first === undefined || last === undefined) return null;
182
+ return formatTimestampRange(first.timestamp, last.timestamp, timeDisplayFormat);
556
183
  }
557
184
 
558
185
  function slateMetricToneClass(tone: SessionSlateMetricTone): string {
@@ -681,46 +308,6 @@ function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Eleme
681
308
  );
682
309
  }
683
310
 
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
311
  function formatPercent(value: number | null): string {
725
312
  if (value === null) return "Unknown";
726
313
  const percent = value * 100;
@@ -969,29 +556,6 @@ function SessionMembershipPanel({
969
556
  );
970
557
  }
971
558
 
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
559
  function LogPaginationButton({
996
560
  label,
997
561
  title,
@@ -1317,16 +881,109 @@ function CrabFamily({
1317
881
  );
1318
882
  }
1319
883
 
884
+ function ThemeModeToggle(): JSX.Element {
885
+ const { themeMode, setThemeMode } = useThemeMode();
886
+
887
+ return (
888
+ <div
889
+ className="border-border/70 bg-muted/70 inline-flex h-8 items-center rounded-md border p-0.5 shadow-sm"
890
+ aria-label="Theme mode"
891
+ role="group"
892
+ >
893
+ <button
894
+ type="button"
895
+ onClick={() => setThemeMode("light")}
896
+ className={cn(
897
+ "inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground",
898
+ themeMode === "light" && "bg-background text-foreground shadow-sm",
899
+ )}
900
+ aria-label="Use light mode"
901
+ aria-pressed={themeMode === "light"}
902
+ title="Light mode"
903
+ >
904
+ <Sun className="size-3.5" />
905
+ </button>
906
+ <button
907
+ type="button"
908
+ onClick={() => setThemeMode("eye-care")}
909
+ className={cn(
910
+ "inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground",
911
+ themeMode === "eye-care" && "bg-background text-foreground shadow-sm",
912
+ )}
913
+ aria-label="Use eye care mode"
914
+ aria-pressed={themeMode === "eye-care"}
915
+ title="Eye care mode"
916
+ >
917
+ <Eye className="size-3.5" />
918
+ </button>
919
+ <button
920
+ type="button"
921
+ onClick={() => setThemeMode("dark")}
922
+ className={cn(
923
+ "inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground",
924
+ themeMode === "dark" && "bg-background text-foreground shadow-sm",
925
+ )}
926
+ aria-label="Use dark mode"
927
+ aria-pressed={themeMode === "dark"}
928
+ title="Dark mode"
929
+ >
930
+ <Moon className="size-3.5" />
931
+ </button>
932
+ </div>
933
+ );
934
+ }
935
+
936
+ function UiScaleControl({
937
+ preference,
938
+ resolvedScale,
939
+ onChange,
940
+ }: {
941
+ preference: UiScalePreference;
942
+ resolvedScale: number;
943
+ onChange: (preference: UiScalePreference) => void;
944
+ }): JSX.Element {
945
+ const resolvedLabel = formatUiScale(resolvedScale);
946
+ const currentLabel = preference === "auto" ? `Auto (${resolvedLabel})` : `${preference}%`;
947
+
948
+ return (
949
+ <Select
950
+ value={preference}
951
+ onValueChange={(value) => onChange(normalizeUiScalePreference(value))}
952
+ >
953
+ <SelectTrigger
954
+ size="sm"
955
+ 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"
956
+ aria-label={`Interface size: ${currentLabel}`}
957
+ >
958
+ <ALargeSmall className="size-3.5" />
959
+ <span className="font-mono text-[10px] font-semibold tabular-nums">{resolvedLabel}</span>
960
+ </SelectTrigger>
961
+ <SelectContent align="end">
962
+ <SelectItem value="auto">Auto</SelectItem>
963
+ <SelectItem value="100">100%</SelectItem>
964
+ <SelectItem value="125">125%</SelectItem>
965
+ <SelectItem value="150">150%</SelectItem>
966
+ </SelectContent>
967
+ </Select>
968
+ );
969
+ }
970
+
1320
971
  function BrandHeader({
1321
972
  compact,
1322
973
  crabEntrancePhase,
1323
974
  currentSessionId,
1324
975
  logCount,
976
+ uiScalePreference,
977
+ resolvedUiScale,
978
+ onUiScaleChange,
1325
979
  }: {
1326
980
  compact: boolean;
1327
981
  crabEntrancePhase: "hidden" | "playing" | "done";
1328
982
  currentSessionId: string | null;
1329
983
  logCount: number;
984
+ uiScalePreference: UiScalePreference;
985
+ resolvedUiScale: number;
986
+ onUiScaleChange: (preference: UiScalePreference) => void;
1330
987
  }): JSX.Element {
1331
988
  if (compact) {
1332
989
  return (
@@ -1347,6 +1004,12 @@ function BrandHeader({
1347
1004
  </div>
1348
1005
  </div>
1349
1006
  <div className="flex shrink-0 items-center gap-1">
1007
+ <ThemeModeToggle />
1008
+ <UiScaleControl
1009
+ preference={uiScalePreference}
1010
+ resolvedScale={resolvedUiScale}
1011
+ onChange={onUiScaleChange}
1012
+ />
1350
1013
  <AlertsDialog />
1351
1014
  <GroupsDialog />
1352
1015
  <AgentLabDialog currentSessionId={currentSessionId} logCount={logCount} />
@@ -1377,6 +1040,12 @@ function BrandHeader({
1377
1040
  </span>
1378
1041
  </h1>
1379
1042
  <div className="flex flex-1 justify-end gap-1">
1043
+ <ThemeModeToggle />
1044
+ <UiScaleControl
1045
+ preference={uiScalePreference}
1046
+ resolvedScale={resolvedUiScale}
1047
+ onChange={onUiScaleChange}
1048
+ />
1380
1049
  <AlertsDialog />
1381
1050
  <GroupsDialog />
1382
1051
  <AgentLabDialog currentSessionId={currentSessionId} logCount={logCount} />
@@ -1386,25 +1055,6 @@ function BrandHeader({
1386
1055
  );
1387
1056
  }
1388
1057
 
1389
- type FloatingLogoPosition = {
1390
- x: number;
1391
- y: number;
1392
- };
1393
-
1394
- type FloatingLogoDragState = {
1395
- pointerId: number;
1396
- offsetX: number;
1397
- offsetY: number;
1398
- };
1399
-
1400
- type FloatingLogoEdge = "left" | "right" | "top" | "bottom";
1401
-
1402
- const FLOATING_LOGO_SIZE = 56;
1403
- const FLOATING_LOGO_MARGIN = 14;
1404
- const FLOATING_LOGO_MIN_DRIFT_MS = 5200;
1405
- const FLOATING_LOGO_DRIFT_WINDOW_MS = 2400;
1406
- const FLOATING_LOGO_MIN_STEP = 72;
1407
- const FLOATING_LOGO_MAX_STEP = 156;
1408
1058
  const DETAILS_PANEL_COLLAPSED_STORAGE_KEY = "agent-inspector.detailsPanelCollapsed";
1409
1059
 
1410
1060
  function readStoredDetailsPanelCollapsed(): boolean {
@@ -1413,264 +1063,6 @@ function readStoredDetailsPanelCollapsed(): boolean {
1413
1063
  return stored === null || stored === "1";
1414
1064
  }
1415
1065
 
1416
- function clampFloatingLogoPosition(position: FloatingLogoPosition): FloatingLogoPosition {
1417
- const maxX = Math.max(
1418
- FLOATING_LOGO_MARGIN,
1419
- window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1420
- );
1421
- const maxY = Math.max(
1422
- FLOATING_LOGO_MARGIN,
1423
- window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1424
- );
1425
- return {
1426
- x: Math.min(Math.max(position.x, FLOATING_LOGO_MARGIN), maxX),
1427
- y: Math.min(Math.max(position.y, FLOATING_LOGO_MARGIN), maxY),
1428
- };
1429
- }
1430
-
1431
- function snapFloatingLogoToEdge(position: FloatingLogoPosition): FloatingLogoPosition {
1432
- const clamped = clampFloatingLogoPosition(position);
1433
- const maxX = Math.max(
1434
- FLOATING_LOGO_MARGIN,
1435
- window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1436
- );
1437
- const maxY = Math.max(
1438
- FLOATING_LOGO_MARGIN,
1439
- window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1440
- );
1441
- const leftDistance = clamped.x - FLOATING_LOGO_MARGIN;
1442
- const rightDistance = maxX - clamped.x;
1443
- const topDistance = clamped.y - FLOATING_LOGO_MARGIN;
1444
- const bottomDistance = maxY - clamped.y;
1445
- const nearestDistance = Math.min(leftDistance, rightDistance, topDistance, bottomDistance);
1446
-
1447
- if (nearestDistance === leftDistance) return { x: FLOATING_LOGO_MARGIN, y: clamped.y };
1448
- if (nearestDistance === rightDistance) return { x: maxX, y: clamped.y };
1449
- if (nearestDistance === topDistance) return { x: clamped.x, y: FLOATING_LOGO_MARGIN };
1450
- return { x: clamped.x, y: maxY };
1451
- }
1452
-
1453
- function randomFloatingLogoEdgePosition(): FloatingLogoPosition {
1454
- const maxX = Math.max(
1455
- FLOATING_LOGO_MARGIN,
1456
- window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1457
- );
1458
- const maxY = Math.max(
1459
- FLOATING_LOGO_MARGIN,
1460
- window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1461
- );
1462
- const randomX = FLOATING_LOGO_MARGIN + Math.random() * (maxX - FLOATING_LOGO_MARGIN);
1463
- const randomY = FLOATING_LOGO_MARGIN + Math.random() * (maxY - FLOATING_LOGO_MARGIN);
1464
- const edgeIndex = Math.floor(Math.random() * 4);
1465
-
1466
- switch (edgeIndex) {
1467
- case 0:
1468
- return { x: FLOATING_LOGO_MARGIN, y: randomY };
1469
- case 1:
1470
- return { x: maxX, y: randomY };
1471
- case 2:
1472
- return { x: randomX, y: FLOATING_LOGO_MARGIN };
1473
- case 3:
1474
- return { x: randomX, y: maxY };
1475
- default:
1476
- return { x: maxX, y: FLOATING_LOGO_MARGIN };
1477
- }
1478
- }
1479
-
1480
- function nearestFloatingLogoEdge(position: FloatingLogoPosition): FloatingLogoEdge {
1481
- const clamped = clampFloatingLogoPosition(position);
1482
- const maxX = Math.max(
1483
- FLOATING_LOGO_MARGIN,
1484
- window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1485
- );
1486
- const maxY = Math.max(
1487
- FLOATING_LOGO_MARGIN,
1488
- window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1489
- );
1490
- const distances: { edge: FloatingLogoEdge; distance: number }[] = [
1491
- { edge: "left", distance: clamped.x - FLOATING_LOGO_MARGIN },
1492
- { edge: "right", distance: maxX - clamped.x },
1493
- { edge: "top", distance: clamped.y - FLOATING_LOGO_MARGIN },
1494
- { edge: "bottom", distance: maxY - clamped.y },
1495
- ];
1496
- return distances.reduce((best, current) => (current.distance < best.distance ? current : best))
1497
- .edge;
1498
- }
1499
-
1500
- function nextFloatingLogoWalkingPosition(position: FloatingLogoPosition): FloatingLogoPosition {
1501
- const clamped = snapFloatingLogoToEdge(position);
1502
- const maxX = Math.max(
1503
- FLOATING_LOGO_MARGIN,
1504
- window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1505
- );
1506
- const maxY = Math.max(
1507
- FLOATING_LOGO_MARGIN,
1508
- window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1509
- );
1510
- const step =
1511
- FLOATING_LOGO_MIN_STEP + Math.random() * (FLOATING_LOGO_MAX_STEP - FLOATING_LOGO_MIN_STEP);
1512
- const direction = Math.random() > 0.5 ? 1 : -1;
1513
- const edge = nearestFloatingLogoEdge(clamped);
1514
-
1515
- switch (edge) {
1516
- case "left": {
1517
- const nextY = clamped.y + step * direction;
1518
- if (nextY <= FLOATING_LOGO_MARGIN)
1519
- return { x: FLOATING_LOGO_MARGIN, y: FLOATING_LOGO_MARGIN };
1520
- if (nextY >= maxY) return { x: FLOATING_LOGO_MARGIN, y: maxY };
1521
- return { x: FLOATING_LOGO_MARGIN, y: nextY };
1522
- }
1523
- case "right": {
1524
- const nextY = clamped.y + step * direction;
1525
- if (nextY <= FLOATING_LOGO_MARGIN) return { x: maxX, y: FLOATING_LOGO_MARGIN };
1526
- if (nextY >= maxY) return { x: maxX, y: maxY };
1527
- return { x: maxX, y: nextY };
1528
- }
1529
- case "top": {
1530
- const nextX = clamped.x + step * direction;
1531
- if (nextX <= FLOATING_LOGO_MARGIN)
1532
- return { x: FLOATING_LOGO_MARGIN, y: FLOATING_LOGO_MARGIN };
1533
- if (nextX >= maxX) return { x: maxX, y: FLOATING_LOGO_MARGIN };
1534
- return { x: nextX, y: FLOATING_LOGO_MARGIN };
1535
- }
1536
- case "bottom": {
1537
- const nextX = clamped.x + step * direction;
1538
- if (nextX <= FLOATING_LOGO_MARGIN) return { x: FLOATING_LOGO_MARGIN, y: maxY };
1539
- if (nextX >= maxX) return { x: maxX, y: maxY };
1540
- return { x: nextX, y: maxY };
1541
- }
1542
- }
1543
- }
1544
-
1545
- function FloatingInspectorLogo(): JSX.Element {
1546
- const [position, setPosition] = useState<FloatingLogoPosition>({
1547
- x: FLOATING_LOGO_MARGIN,
1548
- y: FLOATING_LOGO_MARGIN,
1549
- });
1550
- const [motionTick, setMotionTick] = useState(0);
1551
- const [dragging, setDragging] = useState(false);
1552
- const dragStateRef = useRef<FloatingLogoDragState | null>(null);
1553
-
1554
- useEffect(() => {
1555
- setPosition(randomFloatingLogoEdgePosition());
1556
- }, []);
1557
-
1558
- useEffect(() => {
1559
- if (dragging) return undefined;
1560
- let timeoutId: number | null = null;
1561
-
1562
- const scheduleDrift = () => {
1563
- const delay = FLOATING_LOGO_MIN_DRIFT_MS + Math.random() * FLOATING_LOGO_DRIFT_WINDOW_MS;
1564
- timeoutId = window.setTimeout(() => {
1565
- setMotionTick((current) => current + 1);
1566
- setPosition((current) => nextFloatingLogoWalkingPosition(current));
1567
- scheduleDrift();
1568
- }, delay);
1569
- };
1570
-
1571
- scheduleDrift();
1572
- return () => {
1573
- if (timeoutId !== null) {
1574
- window.clearTimeout(timeoutId);
1575
- }
1576
- };
1577
- }, [dragging]);
1578
-
1579
- useEffect(() => {
1580
- const handleResize = () => {
1581
- setPosition((current) => snapFloatingLogoToEdge(current));
1582
- };
1583
- window.addEventListener("resize", handleResize);
1584
- return () => window.removeEventListener("resize", handleResize);
1585
- }, []);
1586
-
1587
- const handlePointerDown = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
1588
- const rect = event.currentTarget.getBoundingClientRect();
1589
- dragStateRef.current = {
1590
- pointerId: event.pointerId,
1591
- offsetX: event.clientX - rect.left,
1592
- offsetY: event.clientY - rect.top,
1593
- };
1594
- event.currentTarget.setPointerCapture(event.pointerId);
1595
- setDragging(true);
1596
- }, []);
1597
-
1598
- const handlePointerMove = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
1599
- const dragState = dragStateRef.current;
1600
- if (dragState === null || dragState.pointerId !== event.pointerId) return;
1601
- setPosition(
1602
- clampFloatingLogoPosition({
1603
- x: event.clientX - dragState.offsetX,
1604
- y: event.clientY - dragState.offsetY,
1605
- }),
1606
- );
1607
- }, []);
1608
-
1609
- const handlePointerUp = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
1610
- const dragState = dragStateRef.current;
1611
- if (dragState === null || dragState.pointerId !== event.pointerId) return;
1612
- if (event.currentTarget.hasPointerCapture(event.pointerId)) {
1613
- event.currentTarget.releasePointerCapture(event.pointerId);
1614
- }
1615
- dragStateRef.current = null;
1616
- setDragging(false);
1617
- setPosition((current) => snapFloatingLogoToEdge(current));
1618
- }, []);
1619
-
1620
- const style = useMemo<CSSProperties>(
1621
- () => ({
1622
- transform: `translate3d(${position.x}px, ${position.y}px, 0) rotate(${
1623
- motionTick % 2 === 0 ? "-4deg" : "5deg"
1624
- }) scale(${dragging ? "1.08" : "1"})`,
1625
- transition: dragging ? "none" : "transform 4800ms cubic-bezier(0.33, 1, 0.68, 1)",
1626
- touchAction: "none",
1627
- }),
1628
- [dragging, motionTick, position.x, position.y],
1629
- );
1630
-
1631
- return (
1632
- <button
1633
- type="button"
1634
- className={cn(
1635
- "fixed left-0 top-0 z-40 flex size-14 items-center justify-center rounded-full bg-transparent text-foreground outline-none transition-[filter,opacity] duration-200 hover:opacity-95 hover:drop-shadow-[0_0_24px_rgba(245,158,11,0.36)] focus-visible:ring-1 focus-visible:ring-amber-300/55",
1636
- dragging ? "cursor-grabbing" : "cursor-grab",
1637
- )}
1638
- style={style}
1639
- onPointerDown={handlePointerDown}
1640
- onPointerMove={handlePointerMove}
1641
- onPointerUp={handlePointerUp}
1642
- onPointerCancel={handlePointerUp}
1643
- aria-label="Floating Inspector logo"
1644
- title="Drag Inspector logo"
1645
- >
1646
- <span
1647
- className={cn(
1648
- "relative flex size-12 items-center justify-center",
1649
- !dragging && "floating-crab-body",
1650
- )}
1651
- aria-hidden="true"
1652
- >
1653
- <CrabLogo className="relative z-10 size-11 text-amber-500 drop-shadow-[0_0_15px_rgba(245,158,11,0.30)]" />
1654
- <span className="pointer-events-none absolute inset-x-0 bottom-0 z-0 flex items-end justify-center gap-1 text-amber-400/80">
1655
- {[0, 1, 2, 3, 4, 5].map((leg) => (
1656
- <span
1657
- key={leg}
1658
- className={cn(
1659
- "floating-crab-leg h-2 w-0.5 rounded-full bg-current shadow-[0_0_7px_rgba(245,158,11,0.24)]",
1660
- leg < 3 ? "floating-crab-leg-left" : "floating-crab-leg-right",
1661
- dragging && "animate-none",
1662
- )}
1663
- style={{
1664
- animationDelay: `${leg % 2 === 0 ? 0 : 180}ms`,
1665
- }}
1666
- />
1667
- ))}
1668
- </span>
1669
- </span>
1670
- </button>
1671
- );
1672
- }
1673
-
1674
1066
  function SessionContextLogButton({ label, logId }: { label: string; logId: number }): JSX.Element {
1675
1067
  return (
1676
1068
  <button
@@ -2049,6 +1441,7 @@ export function ProxyViewer({
2049
1441
  }: ProxyViewerProps): JSX.Element {
2050
1442
  const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
2051
1443
  const { providers } = useProviders();
1444
+ const { uiScalePreference, resolvedUiScale, setUiScalePreference } = useUiScale();
2052
1445
  const [exporting, setExporting] = useState<ExportMode | null>(null);
2053
1446
  const [exportError, setExportError] = useState<string | null>(null);
2054
1447
  const [importing, setImporting] = useState(false);
@@ -2334,7 +1727,9 @@ export function ProxyViewer({
2334
1727
  onClick={toggleDetailsPanelCollapsed}
2335
1728
  className={cn(
2336
1729
  "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]",
1730
+ detailsPanelCollapsed
1731
+ ? "right-3"
1732
+ : "lg:right-[calc(21.25rem-0.875rem)] xl:right-[calc(22.5rem-0.875rem)]",
2338
1733
  )}
2339
1734
  aria-label={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
2340
1735
  title={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
@@ -2346,13 +1741,13 @@ export function ProxyViewer({
2346
1741
  <PanelRightClose className="size-3.5" />
2347
1742
  )}
2348
1743
  </button>
2349
- {detailsPanelCollapsed && <FloatingInspectorLogo />}
1744
+ {detailsPanelCollapsed && <InspectorPet logs={logs} />}
2350
1745
  <div
2351
1746
  className={cn(
2352
1747
  "grid w-full min-w-0 grid-cols-1 lg:min-h-0 lg:flex-1 lg:overflow-hidden",
2353
1748
  detailsPanelCollapsed
2354
1749
  ? "lg:grid-cols-[minmax(0,1fr)]"
2355
- : "lg:grid-cols-[minmax(0,1fr)_340px] xl:grid-cols-[minmax(0,1fr)_360px]",
1750
+ : "lg:grid-cols-[minmax(0,1fr)_21.25rem] xl:grid-cols-[minmax(0,1fr)_22.5rem]",
2356
1751
  )}
2357
1752
  >
2358
1753
  {!detailsPanelCollapsed && (
@@ -2363,6 +1758,9 @@ export function ProxyViewer({
2363
1758
  crabEntrancePhase={crabEntrancePhase}
2364
1759
  currentSessionId={labSessionId}
2365
1760
  logCount={logs.length}
1761
+ uiScalePreference={uiScalePreference}
1762
+ resolvedUiScale={resolvedUiScale}
1763
+ onUiScaleChange={setUiScalePreference}
2366
1764
  />
2367
1765
 
2368
1766
  <SidebarPanel label="Session">