@tonyclaw/agent-inspector 3.0.28 → 3.0.30

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 (156) hide show
  1. package/.output/cli.js +791 -191
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/CompareDrawer-B6cXJohL.js +1 -0
  4. package/.output/public/assets/InspectorPet-mAzeGmEE.js +4108 -0
  5. package/.output/public/assets/ProxyViewerContainer-CkFWv_Nm.js +126 -0
  6. package/.output/public/assets/{ReplayDialog-CZMApaF4.js → ReplayDialog-DodiTL6E.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-DNWccR7O.js → RequestAnatomy-B3IauToI.js} +1 -1
  8. package/.output/public/assets/ResponseView-Hn-5ordK.js +3 -0
  9. package/.output/public/assets/StreamingChunkSequence-vLDOVQM_.js +1 -0
  10. package/.output/public/assets/_sessionId-BFH4TOaI.js +1 -0
  11. package/.output/public/assets/{_sessionId-DRfvuE2k.js → _sessionId-Cj_HIFZx.js} +1 -1
  12. package/.output/public/assets/{index-D4HkS7Ct.js → index-Bmj2lcWE.js} +1 -1
  13. package/.output/public/assets/index-CgJj-HBC.css +1 -0
  14. package/.output/public/assets/index-Ch7uqnCT.js +1 -0
  15. package/.output/public/assets/index-DkDdKvLl.js +76 -0
  16. package/.output/public/assets/{index-B3aAuqWq.js → index-ZlhqCrSg.js} +6 -6
  17. package/.output/public/assets/{json-viewer-wuJSKaed.js → json-viewer-BdrnVQu-.js} +1 -1
  18. package/.output/public/assets/jszip.min-CS_nt_o1.js +2 -0
  19. package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
  20. package/.output/server/_libs/jszip.mjs +35 -9
  21. package/.output/server/_libs/lucide-react.mjs +86 -86
  22. package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
  23. package/.output/server/_libs/tanstack__react-router.mjs +1 -1
  24. package/.output/server/{_sessionId-BJTNiP_O.mjs → _sessionId-C5S3pGZd.mjs} +15 -15
  25. package/.output/server/_sessionId-g6D69lKq.mjs +81 -0
  26. package/.output/server/_ssr/{CompareDrawer-BMRH3N97.mjs → CompareDrawer-CqUmGHal.mjs} +138 -103
  27. package/.output/server/_ssr/InspectorPet-DI0UJd01.mjs +925 -0
  28. package/.output/server/_ssr/{ProxyViewerContainer-AFwcHAIR.mjs → ProxyViewerContainer-CvsUmbJ3.mjs} +1216 -1659
  29. package/.output/server/_ssr/{ReplayDialog-CbWvRHiN.mjs → ReplayDialog-DS48dpFG.mjs} +18 -18
  30. package/.output/server/_ssr/{RequestAnatomy-C4kcPyqN.mjs → RequestAnatomy-DxYVQU1C.mjs} +58 -58
  31. package/.output/server/_ssr/{ResponseView-D8VYt-gZ.mjs → ResponseView-BLXW73eq.mjs} +60 -60
  32. package/.output/server/_ssr/{StreamingChunkSequence-BUUo6vU0.mjs → StreamingChunkSequence-DD0iFuy0.mjs} +16 -16
  33. package/.output/server/_ssr/{index-DTwMEEXO.mjs → index-B10LIaK0.mjs} +15 -15
  34. package/.output/server/_ssr/index-RHVJnU2p.mjs +81 -0
  35. package/.output/server/_ssr/index.mjs +2 -2
  36. package/.output/server/_ssr/{json-viewer-BqF1pSQD.mjs → json-viewer-LDu2XBaK.mjs} +58 -58
  37. package/.output/server/_ssr/{router-C8BOns7y.mjs → router-GKvrwifJ.mjs} +9005 -6488
  38. package/.output/server/_tanstack-start-manifest_v-Df2n4mbT.mjs +4 -0
  39. package/.output/server/index.mjs +105 -77
  40. package/.output/workers/logFinalizer.worker.js +16908 -0
  41. package/.output/workers/sessionWorkerEntry.js +16904 -0
  42. package/README.md +21 -5
  43. package/package.json +6 -2
  44. package/src/assets/logos/qwen.webp +0 -0
  45. package/src/cli.ts +85 -10
  46. package/src/components/OnboardingBanner.tsx +41 -21
  47. package/src/components/ProxyViewer.tsx +198 -269
  48. package/src/components/ProxyViewerContainer.tsx +128 -17
  49. package/src/components/clients/ClientLogo.tsx +9 -6
  50. package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
  51. package/src/components/errors/SafeErrorBoundary.tsx +201 -0
  52. package/src/components/pi-agent/PiAgentPanel.tsx +4 -1
  53. package/src/components/providers/ImportWizardDialog.tsx +18 -42
  54. package/src/components/providers/ProviderLogo.tsx +1 -1
  55. package/src/components/providers/ProvidersPanel.tsx +8 -5
  56. package/src/components/providers/SettingsDialog.tsx +34 -11
  57. package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
  58. package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
  59. package/src/components/proxy-viewer/ConversationGroupList.tsx +49 -34
  60. package/src/components/proxy-viewer/ConversationHeader.tsx +22 -18
  61. package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
  62. package/src/components/proxy-viewer/LogEntry.tsx +283 -275
  63. package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
  64. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
  65. package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
  66. package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
  67. package/src/components/proxy-viewer/accessibility.ts +8 -0
  68. package/src/components/proxy-viewer/lazy.ts +4 -0
  69. package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
  70. package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
  71. package/src/components/proxy-viewer/viewerState.ts +2 -6
  72. package/src/components/ui/select.tsx +1 -1
  73. package/src/components/ui/tabs.tsx +3 -3
  74. package/src/components/ui/transient-toast.tsx +1 -1
  75. package/src/lib/apiClient.ts +17 -2
  76. package/src/lib/ecosystemContract.ts +34 -0
  77. package/src/lib/export-logs.ts +1 -1
  78. package/src/lib/providerContract.ts +70 -4
  79. package/src/lib/providerImportContract.ts +27 -0
  80. package/src/lib/providerModelMetadata.ts +16 -7
  81. package/src/lib/resourceLimits.ts +152 -0
  82. package/src/lib/safeDiagnostic.ts +38 -0
  83. package/src/lib/useProviders.ts +4 -4
  84. package/src/lib/utils.ts +5 -1
  85. package/src/mcp/server.ts +39 -6
  86. package/src/mcp/toolHandlers.ts +131 -4
  87. package/src/proxy/chunkStorage.ts +20 -6
  88. package/src/proxy/config.ts +20 -6
  89. package/src/proxy/dataDir.ts +3 -0
  90. package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
  91. package/src/proxy/ecosystemExecutionRoute.ts +116 -0
  92. package/src/proxy/ecosystemTasks.ts +48 -0
  93. package/src/proxy/evidenceExporter.ts +23 -9
  94. package/src/proxy/groupEvidenceExporter.ts +26 -5
  95. package/src/proxy/groupStore.ts +13 -3
  96. package/src/proxy/handler.ts +103 -20
  97. package/src/proxy/identityProxy.ts +333 -14
  98. package/src/proxy/logFinalizer.ts +73 -6
  99. package/src/proxy/logImportUpload.ts +128 -0
  100. package/src/proxy/logImporter.ts +321 -70
  101. package/src/proxy/logIndex.ts +16 -6
  102. package/src/proxy/logger.ts +294 -41
  103. package/src/proxy/privateDataPath.ts +183 -0
  104. package/src/proxy/providerScanStore.ts +87 -0
  105. package/src/proxy/providerSecretStore.ts +58 -19
  106. package/src/proxy/providers.ts +175 -58
  107. package/src/proxy/rawStreamCapture.ts +66 -5
  108. package/src/proxy/runStore.ts +13 -3
  109. package/src/proxy/runtimeAdmission.ts +52 -0
  110. package/src/proxy/runtimeHealth.ts +206 -0
  111. package/src/proxy/runtimeShutdown.ts +75 -0
  112. package/src/proxy/sessionArchive.ts +15 -2
  113. package/src/proxy/sessionProcess.ts +19 -0
  114. package/src/proxy/sessionRuntime.ts +7 -0
  115. package/src/proxy/shutdownCoordinator.ts +90 -0
  116. package/src/proxy/sqliteLogIndex.ts +18 -2
  117. package/src/proxy/store.ts +18 -2
  118. package/src/routes/__root.tsx +18 -1
  119. package/src/routes/api/alerts.summary.ts +12 -8
  120. package/src/routes/api/alerts.ts +27 -9
  121. package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
  122. package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
  123. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
  124. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
  125. package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
  126. package/src/routes/api/health.ts +9 -2
  127. package/src/routes/api/knowledge.project-context.ts +11 -0
  128. package/src/routes/api/knowledge.search.ts +33 -2
  129. package/src/routes/api/logs.$id.body.ts +16 -2
  130. package/src/routes/api/logs.import.ts +7 -18
  131. package/src/routes/api/logs.stream.ts +147 -69
  132. package/src/routes/api/logs.ts +55 -14
  133. package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
  134. package/src/routes/api/providers.$providerId.ts +37 -6
  135. package/src/routes/api/providers.export.ts +34 -16
  136. package/src/routes/api/providers.import.ts +42 -8
  137. package/src/routes/api/providers.scan.ts +13 -8
  138. package/src/routes/api/providers.ts +36 -5
  139. package/src/routes/api/runs.ts +12 -12
  140. package/src/routes/api/sessions.ts +15 -8
  141. package/src/routes/index.tsx +6 -0
  142. package/src/routes/livez.ts +13 -0
  143. package/src/routes/readyz.ts +18 -0
  144. package/src/routes/session/$sessionId.tsx +6 -0
  145. package/styles/globals.css +19 -3
  146. package/.output/public/assets/CompareDrawer-TghZr5t1.js +0 -1
  147. package/.output/public/assets/ProxyViewerContainer-fShBAZjj.js +0 -4234
  148. package/.output/public/assets/ResponseView-d25MdFhY.js +0 -3
  149. package/.output/public/assets/StreamingChunkSequence-DIFZEnWe.js +0 -1
  150. package/.output/public/assets/index-BsxAGqMA.css +0 -1
  151. package/.output/public/assets/index-DzARL2Fi.js +0 -76
  152. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  153. package/.output/server/_tanstack-start-manifest_v-BBf2nRhb.mjs +0 -4
  154. package/src/assets/logos/mcp.png +0 -0
  155. package/src/assets/logos/qwen.png +0 -0
  156. package/src/components/ui/mcp-logo.tsx +0 -20
@@ -1,5 +1,5 @@
1
1
  import { Suspense, useEffect, useMemo, useRef, useState } from "react";
2
- import type { JSX } from "react";
2
+ import type { JSX, KeyboardEvent as ReactKeyboardEvent } from "react";
3
3
  import {
4
4
  Check,
5
5
  ChevronRight,
@@ -46,6 +46,11 @@ type ChangedOp = Extract<DiffOp, { kind: "changed" }>;
46
46
 
47
47
  type DiffMode = "unified" | "split";
48
48
 
49
+ export const COMPARE_RESPONSIVE_HEADER_CLASS_NAME =
50
+ "flex flex-col gap-3 border-b border-border px-3 py-3 sm:flex-row sm:items-start sm:gap-4 sm:px-4";
51
+ export const COMPARE_RESPONSIVE_SPLIT_CLASS_NAME =
52
+ "grid min-w-0 grid-cols-1 gap-x-2 gap-y-0.5 px-3 py-2 text-xs md:grid-cols-[200px_minmax(0,1fr)_minmax(0,1fr)]";
53
+
49
54
  /** Walk the JsonNode tree and pretty-print it back to a JSON string for the
50
55
  * expanded-equal-subtree view. The node is a plain object structure so
51
56
  * `JSON.stringify` produces correct output. */
@@ -162,23 +167,23 @@ const KIND_VISUAL: Record<
162
167
  > = {
163
168
  added: {
164
169
  icon: Plus,
165
- accent: "text-emerald-600 dark:text-emerald-400",
166
- bg: "bg-emerald-500/5 hover:bg-emerald-500/10",
167
- border: "border-l-emerald-500",
170
+ accent: "text-status-success",
171
+ bg: "bg-status-success/5 hover:bg-status-success/10",
172
+ border: "border-l-status-success",
168
173
  label: "ADDED",
169
174
  },
170
175
  removed: {
171
176
  icon: Minus,
172
- accent: "text-rose-600 dark:text-rose-400",
173
- bg: "bg-rose-500/5 hover:bg-rose-500/10",
174
- border: "border-l-rose-500",
177
+ accent: "text-status-danger",
178
+ bg: "bg-status-danger/5 hover:bg-status-danger/10",
179
+ border: "border-l-status-danger",
175
180
  label: "REMOVED",
176
181
  },
177
182
  changed: {
178
183
  icon: Pencil,
179
- accent: "text-amber-600 dark:text-amber-400",
180
- bg: "bg-amber-500/5 hover:bg-amber-500/10",
181
- border: "border-l-amber-500",
184
+ accent: "text-status-watch",
185
+ bg: "bg-status-watch/5 hover:bg-status-watch/10",
186
+ border: "border-l-status-watch",
182
187
  label: "CHANGED",
183
188
  },
184
189
  equal: {
@@ -294,19 +299,19 @@ function UnifiedOpRow({
294
299
  ? [
295
300
  {
296
301
  text: previewNode(op.left, 400),
297
- tone: "text-rose-700 dark:text-rose-300 line-through",
302
+ tone: "text-status-danger line-through",
298
303
  },
299
- { text: previewNode(op.right, 400), tone: "text-emerald-700 dark:text-emerald-300" },
304
+ { text: previewNode(op.right, 400), tone: "text-status-success" },
300
305
  ]
301
306
  : op.kind === "removed"
302
307
  ? [
303
308
  {
304
309
  text: previewNode(op.value, 400),
305
- tone: "text-rose-700 dark:text-rose-300 line-through",
310
+ tone: "text-status-danger line-through",
306
311
  },
307
312
  ]
308
313
  : op.kind === "added"
309
- ? [{ text: previewNode(op.value, 400), tone: "text-emerald-700 dark:text-emerald-300" }]
314
+ ? [{ text: previewNode(op.value, 400), tone: "text-status-success" }]
310
315
  : [{ text: previewNode(op.value, 400), tone: "text-muted-foreground" }];
311
316
 
312
317
  const justCopied = copiedPath === op.path && op.path !== "";
@@ -376,7 +381,7 @@ function UnifiedOpRow({
376
381
  className={cn(
377
382
  "shrink-0 p-1 rounded transition-colors cursor-pointer inline-flex items-center justify-center",
378
383
  justCopied
379
- ? "text-emerald-500"
384
+ ? "text-status-success"
380
385
  : "text-muted-foreground/50 hover:text-foreground hover:bg-muted",
381
386
  )}
382
387
  aria-label={justCopied ? "Copied" : "Copy"}
@@ -427,12 +432,12 @@ function ExpandedSubtree({ op }: { op: AddedOp | RemovedOp | ChangedOp }): JSX.E
427
432
  }
428
433
  return (
429
434
  <div className="pl-5 mt-2 grid grid-cols-1 md:grid-cols-2 gap-2">
430
- <div className="border border-rose-500/30 rounded p-2 bg-rose-500/5">
431
- <div className="text-[10px] uppercase tracking-wider text-rose-500 mb-1">Old</div>
435
+ <div className="rounded border border-status-danger/30 bg-status-danger/5 p-2">
436
+ <div className="mb-1 text-[10px] uppercase tracking-wider text-status-danger">Old</div>
432
437
  <LazyJsonStringView text={nodeToJsonString(op.left)} defaultExpandDepth={0} />
433
438
  </div>
434
- <div className="border border-emerald-500/30 rounded p-2 bg-emerald-500/5">
435
- <div className="text-[10px] uppercase tracking-wider text-emerald-500 mb-1">New</div>
439
+ <div className="rounded border border-status-success/30 bg-status-success/5 p-2">
440
+ <div className="mb-1 text-[10px] uppercase tracking-wider text-status-success">New</div>
436
441
  <LazyJsonStringView text={nodeToJsonString(op.right)} defaultExpandDepth={0} />
437
442
  </div>
438
443
  </div>
@@ -461,7 +466,7 @@ function SummaryChips({
461
466
  className={cn(
462
467
  "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
463
468
  counts.removed > 0
464
- ? "border-rose-500/40 text-rose-600 dark:text-rose-400 bg-rose-500/10 hover:bg-rose-500/20"
469
+ ? "border-status-danger/40 bg-status-danger/10 text-status-danger hover:bg-status-danger/20"
465
470
  : "border-border text-muted-foreground/40 cursor-not-allowed",
466
471
  )}
467
472
  title={counts.removed > 0 ? "Jump to first removed" : "No removals"}
@@ -476,7 +481,7 @@ function SummaryChips({
476
481
  className={cn(
477
482
  "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
478
483
  counts.added > 0
479
- ? "border-emerald-500/40 text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 hover:bg-emerald-500/20"
484
+ ? "border-status-success/40 bg-status-success/10 text-status-success hover:bg-status-success/20"
480
485
  : "border-border text-muted-foreground/40 cursor-not-allowed",
481
486
  )}
482
487
  title={counts.added > 0 ? "Jump to first added" : "No additions"}
@@ -491,7 +496,7 @@ function SummaryChips({
491
496
  className={cn(
492
497
  "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
493
498
  counts.changed > 0
494
- ? "border-amber-500/40 text-amber-600 dark:text-amber-400 bg-amber-500/10 hover:bg-amber-500/20"
499
+ ? "border-status-watch/40 bg-status-watch/10 text-status-watch hover:bg-status-watch/20"
495
500
  : "border-border text-muted-foreground/40 cursor-not-allowed",
496
501
  )}
497
502
  title={counts.changed > 0 ? "Jump to first changed" : "No changes"}
@@ -563,27 +568,29 @@ function SideSummary({
563
568
  className={cn(
564
569
  "text-[10px] px-1.5 py-0 h-5 font-mono shrink-0",
565
570
  side === "left"
566
- ? "border-rose-500/40 text-rose-400"
567
- : "border-emerald-500/40 text-emerald-400",
571
+ ? "border-status-danger/40 text-status-danger"
572
+ : "border-status-success/40 text-status-success",
568
573
  )}
569
574
  >
570
575
  {side === "left" ? "← Left" : "Right →"}
571
576
  </Badge>
572
- <span className="font-mono text-blue-400/80" title={`Log ID ${String(log.id)}`}>
577
+ <span className="font-mono text-status-info" title={`Log ID ${String(log.id)}`}>
573
578
  #{displayNumber}
574
579
  </span>
575
580
  {log.model !== null && (
576
- <span className="font-mono leading-snug text-muted-foreground">{log.model}</span>
581
+ <span className="min-w-0 truncate font-mono leading-snug text-muted-foreground">
582
+ {log.model}
583
+ </span>
577
584
  )}
578
585
  </div>
579
- <div className="flex items-center gap-3 text-muted-foreground font-mono">
586
+ <div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 font-mono text-muted-foreground">
580
587
  {log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
581
- <span className="text-emerald-400">
588
+ <span className="text-status-success">
582
589
  KV Cache +{formatTokens(log.cacheCreationInputTokens)}
583
590
  </span>
584
591
  )}
585
592
  {log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
586
- <span className="text-purple-400">
593
+ <span className="text-status-accent">
587
594
  KV Cache ~{formatTokens(log.cacheReadInputTokens)}
588
595
  </span>
589
596
  )}
@@ -605,6 +612,9 @@ export function CompareDrawer({
605
612
  rightDisplayNumber,
606
613
  onClose,
607
614
  }: CompareDrawerProps): JSX.Element {
615
+ const panelRef = useRef<HTMLDivElement>(null);
616
+ const closeButtonRef = useRef<HTMLButtonElement>(null);
617
+ const previousActiveElementRef = useRef<HTMLElement | null>(null);
608
618
  // Memoize the diff so re-renders (e.g. parent re-renders) don't recompute.
609
619
  const ops = useMemo<DiffOp[]>(() => {
610
620
  const leftRequest = getLogFormatAdapter(resolveLogFormat(left)).analyzeRequest(
@@ -716,8 +726,12 @@ export function CompareDrawer({
716
726
  }
717
727
  };
718
728
 
719
- // Esc keybinding + body scroll lock while the drawer is open.
729
+ // Esc keybinding, initial focus, focus restoration, and body scroll lock while open.
720
730
  useEffect(() => {
731
+ previousActiveElementRef.current =
732
+ document.activeElement instanceof HTMLElement ? document.activeElement : null;
733
+ closeButtonRef.current?.focus();
734
+
721
735
  const onKey = (e: KeyboardEvent) => {
722
736
  if (e.key === "Escape") onClose();
723
737
  };
@@ -727,9 +741,36 @@ export function CompareDrawer({
727
741
  return () => {
728
742
  document.removeEventListener("keydown", onKey);
729
743
  document.body.style.overflow = prevOverflow;
744
+ previousActiveElementRef.current?.focus();
730
745
  };
731
746
  }, [onClose]);
732
747
 
748
+ const handlePanelKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>): void => {
749
+ event.stopPropagation();
750
+ if (event.key !== "Tab") return;
751
+
752
+ const panel = panelRef.current;
753
+ if (panel === null) return;
754
+ const focusable = Array.from(
755
+ panel.querySelectorAll<HTMLElement>(
756
+ 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])',
757
+ ),
758
+ );
759
+ const first = focusable[0];
760
+ const last = focusable[focusable.length - 1];
761
+ if (first === undefined || last === undefined) return;
762
+
763
+ if (event.shiftKey && document.activeElement === first) {
764
+ event.preventDefault();
765
+ last.focus();
766
+ return;
767
+ }
768
+ if (!event.shiftKey && document.activeElement === last) {
769
+ event.preventDefault();
770
+ first.focus();
771
+ }
772
+ };
773
+
733
774
  const sameSession = getConversationId(left) === getConversationId(right);
734
775
  const allEqual = ops.length === 1 && ops[0]?.kind === "equal";
735
776
 
@@ -751,22 +792,24 @@ export function CompareDrawer({
751
792
 
752
793
  {/* Drawer panel */}
753
794
  <div
795
+ ref={panelRef}
754
796
  className={cn(
755
- "relative bg-background border-l border-border shadow-xl",
797
+ "relative overflow-x-hidden bg-background border-l border-border shadow-xl",
756
798
  "w-full md:w-[70vw] max-w-[1100px] flex flex-col h-full",
757
799
  )}
758
800
  onClick={(e) => e.stopPropagation()}
759
- onKeyDown={(e) => e.stopPropagation()}
801
+ onKeyDown={handlePanelKeyDown}
760
802
  >
761
803
  {/* Header */}
762
- <div className="flex items-start gap-4 px-4 py-3 border-b border-border">
763
- <div className="flex-1 flex gap-4 min-w-0">
804
+ <div className={COMPARE_RESPONSIVE_HEADER_CLASS_NAME}>
805
+ <div className="grid min-w-0 flex-1 grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
764
806
  <SideSummary log={left} side="left" displayNumber={leftDisplayNumber} />
765
807
  <SideSummary log={right} side="right" displayNumber={rightDisplayNumber} />
766
808
  </div>
767
- <div className="flex items-center gap-2 shrink-0">
809
+ <div className="flex w-full shrink-0 items-center justify-between gap-2 sm:w-auto sm:justify-start">
768
810
  <ModeToggle mode={mode} onChange={setMode} />
769
811
  <button
812
+ ref={closeButtonRef}
770
813
  type="button"
771
814
  onClick={onClose}
772
815
  aria-label="Close"
@@ -778,7 +821,7 @@ export function CompareDrawer({
778
821
  </div>
779
822
 
780
823
  {!sameSession && (
781
- <div className="px-4 py-1.5 text-xs text-amber-400 bg-amber-500/10 border-b border-border">
824
+ <div className="border-b border-border bg-warning/10 px-4 py-1.5 text-xs text-foreground">
782
825
  Heads up: the two selected logs are from different sessions.
783
826
  </div>
784
827
  )}
@@ -835,7 +878,7 @@ export function CompareDrawer({
835
878
  );
836
879
  }
837
880
 
838
- /** Legacy 3-column (path | left | right) view, retained as a toggle option. */
881
+ /** Three-column on wide screens and a bounded stacked comparison on narrow screens. */
839
882
  function SplitBody({
840
883
  grouped,
841
884
  left,
@@ -850,9 +893,9 @@ function SplitBody({
850
893
  rightDisplayNumber: number;
851
894
  }): JSX.Element {
852
895
  return (
853
- <div className="grid grid-cols-[200px_1fr_1fr] gap-x-2 gap-y-0.5 px-3 py-2 text-xs">
896
+ <div data-compare-layout="responsive-split" className={COMPARE_RESPONSIVE_SPLIT_CLASS_NAME}>
854
897
  {/* Column headers */}
855
- <div className="grid grid-cols-[200px_1fr_1fr] gap-x-2 col-span-3 pb-2 mb-2 border-b border-border text-[10px] uppercase tracking-wider text-muted-foreground">
898
+ <div className="col-span-1 mb-2 grid grid-cols-1 gap-x-2 gap-y-1 border-b border-border pb-2 text-[10px] uppercase tracking-wider text-muted-foreground md:col-span-3 md:grid-cols-[200px_minmax(0,1fr)_minmax(0,1fr)]">
856
899
  <span>Path</span>
857
900
  <span title={`Log ID ${String(left.id)}`}>Left (Log #{leftDisplayNumber})</span>
858
901
  <span title={`Log ID ${String(right.id)}`}>Right (Log #{rightDisplayNumber})</span>
@@ -864,7 +907,7 @@ function SplitBody({
864
907
  <div
865
908
  // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
866
909
  key={i}
867
- className="col-span-3 px-2 py-1 text-xs text-muted-foreground/60"
910
+ className="col-span-1 px-2 py-1 text-xs text-muted-foreground/60 md:col-span-3"
868
911
  >
869
912
  {g.ops.length} equal siblings collapsed — switch to Unified to expand
870
913
  </div>
@@ -876,7 +919,7 @@ function SplitBody({
876
919
  <div
877
920
  // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
878
921
  key={i}
879
- className="col-span-3 grid grid-cols-[200px_1fr_1fr] gap-x-2 px-2 py-0.5 text-muted-foreground"
922
+ className="col-span-1 grid min-w-0 grid-cols-1 gap-x-2 gap-y-1 px-2 py-0.5 text-muted-foreground md:col-span-3 md:grid-cols-[200px_minmax(0,1fr)_minmax(0,1fr)]"
880
923
  >
881
924
  <span className="font-mono text-xs truncate" title={op.path}>
882
925
  {op.path}
@@ -895,10 +938,10 @@ function SplitBody({
895
938
  <div
896
939
  // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
897
940
  key={i}
898
- className="col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-emerald-400/70 bg-emerald-500/5"
941
+ className="col-span-1 rounded border-l-2 border-l-status-success bg-status-success/5 px-2 py-1 text-xs md:col-span-3"
899
942
  >
900
943
  <div className="font-mono text-xs text-muted-foreground mb-0.5">{op.path}</div>
901
- <div className="font-mono break-all text-emerald-300/90">
944
+ <div className="font-mono break-all text-status-success">
902
945
  + {previewNode(op.value, 400)}
903
946
  </div>
904
947
  </div>
@@ -909,10 +952,10 @@ function SplitBody({
909
952
  <div
910
953
  // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
911
954
  key={i}
912
- className="col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-rose-400/70 bg-rose-500/5"
955
+ className="col-span-1 rounded border-l-2 border-l-status-danger bg-status-danger/5 px-2 py-1 text-xs md:col-span-3"
913
956
  >
914
957
  <div className="font-mono text-xs text-muted-foreground mb-0.5">{op.path}</div>
915
- <div className="font-mono break-all text-rose-300/90 line-through">
958
+ <div className="font-mono break-all text-status-danger line-through">
916
959
  − {previewNode(op.value, 400)}
917
960
  </div>
918
961
  </div>
@@ -922,14 +965,14 @@ function SplitBody({
922
965
  <div
923
966
  // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
924
967
  key={i}
925
- className="col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-amber-400/70 bg-amber-500/5"
968
+ className="col-span-1 rounded border-l-2 border-l-status-watch bg-status-watch/5 px-2 py-1 text-xs md:col-span-3"
926
969
  >
927
970
  <div className="font-mono text-xs text-muted-foreground mb-1">{op.path}</div>
928
- <div className="grid grid-cols-2 gap-2">
929
- <div className="font-mono text-rose-300/90 break-all line-through">
971
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
972
+ <div className="font-mono break-all text-status-danger line-through">
930
973
  {previewNode(op.left, 400)}
931
974
  </div>
932
- <div className="font-mono text-emerald-300/90 break-all">
975
+ <div className="font-mono break-all text-status-success">
933
976
  {previewNode(op.right, 400)}
934
977
  </div>
935
978
  </div>
@@ -284,14 +284,16 @@ function countSessions(processes: readonly ProcessGroupData[]): number {
284
284
  return sessions;
285
285
  }
286
286
 
287
- function readStoredIdSet(storageKey: string): ReadonlySet<string> {
288
- if (typeof window === "undefined") return new Set();
287
+ function readStoredIdSet(storageKey: string): ReadonlySet<string> | null {
288
+ if (typeof window === "undefined") return null;
289
289
  try {
290
- const parsed: unknown = JSON.parse(window.localStorage.getItem(storageKey) ?? "[]");
291
- if (!Array.isArray(parsed)) return new Set();
290
+ const storedValue = window.localStorage.getItem(storageKey);
291
+ if (storedValue === null) return null;
292
+ const parsed: unknown = JSON.parse(storedValue);
293
+ if (!Array.isArray(parsed)) return null;
292
294
  return new Set(parsed.filter((value): value is string => typeof value === "string"));
293
295
  } catch {
294
- return new Set();
296
+ return null;
295
297
  }
296
298
  }
297
299
 
@@ -312,6 +314,21 @@ function removeIds(values: ReadonlySet<string>, ids: ReadonlySet<string>): Reado
312
314
  return changed ? next : values;
313
315
  }
314
316
 
317
+ export function buildDefaultCollapsedProcessIds(
318
+ hierarchy: readonly ClientGroupData[],
319
+ ): ReadonlySet<string> {
320
+ const collapsedIds = new Set<string>();
321
+ for (const clientGroup of hierarchy) {
322
+ for (let index = 1; index < clientGroup.processes.length; index++) {
323
+ const processGroup = clientGroup.processes[index];
324
+ if (processGroup !== undefined && !hasPendingLogs(processGroup.logs)) {
325
+ collapsedIds.add(processGroup.id);
326
+ }
327
+ }
328
+ }
329
+ return collapsedIds;
330
+ }
331
+
315
332
  function shortProjectPath(projectFolder: string | null): string | null {
316
333
  if (projectFolder === null || projectFolder === "") return null;
317
334
  const parts = projectFolder
@@ -337,12 +354,12 @@ function SummaryChip({
337
354
  return (
338
355
  <span
339
356
  className={cn(
340
- "bg-white/[0.04] inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs text-muted-foreground",
357
+ "inline-flex h-7 items-center gap-1 rounded-md bg-[var(--inspector-surface-low)] px-2 text-xs text-muted-foreground",
341
358
  className,
342
359
  )}
343
360
  >
344
- <span className="font-mono text-[10px] uppercase text-white/35">{label}</span>
345
- <span className="font-mono tabular-nums text-slate-100">{value}</span>
361
+ <span className="font-mono text-[10px] uppercase text-muted-foreground/70">{label}</span>
362
+ <span className="font-mono tabular-nums text-foreground">{value}</span>
346
363
  </span>
347
364
  );
348
365
  }
@@ -351,13 +368,13 @@ function AncestorActivityIndicator({ count }: { count: number }): JSX.Element {
351
368
  const label = `${count} open turn${count === 1 ? "" : "s"}`;
352
369
  return (
353
370
  <span
354
- className="relative inline-flex size-6 shrink-0 items-center justify-center rounded-md bg-amber-300/[0.08] text-amber-100/90 ring-1 ring-amber-200/10"
371
+ className="relative inline-flex size-6 shrink-0 items-center justify-center rounded-md bg-[var(--inspector-surface-medium)] text-[var(--inspector-status-watch)] ring-1 ring-border/50"
355
372
  aria-label={label}
356
373
  title={label}
357
374
  >
358
375
  <Loader2 className="size-3 animate-spin" />
359
376
  {count > 1 && (
360
- <span className="absolute translate-x-2 -translate-y-2 rounded bg-background px-0.5 font-mono text-[8px] tabular-nums text-amber-100">
377
+ <span className="absolute translate-x-2 -translate-y-2 rounded bg-card px-0.5 font-mono text-[8px] tabular-nums text-[var(--inspector-status-watch)]">
361
378
  {count}
362
379
  </span>
363
380
  )}
@@ -373,14 +390,16 @@ export function ConversationGroupList({
373
390
  const [collapsedClientIds, setCollapsedClientIds] = useState<ReadonlySet<string>>(
374
391
  () => new Set(),
375
392
  );
376
- const [collapsedProcessIds, setCollapsedProcessIds] = useState<ReadonlySet<string>>(
377
- () => new Set(),
393
+ const [collapsedProcessIds, setCollapsedProcessIds] = useState<ReadonlySet<string>>(() =>
394
+ buildDefaultCollapsedProcessIds(hierarchy),
378
395
  );
379
396
  const [storedCollapseStateLoaded, setStoredCollapseStateLoaded] = useState(false);
380
397
 
381
398
  useEffect(() => {
382
- setCollapsedClientIds(readStoredIdSet(COLLAPSED_CLIENTS_STORAGE_KEY));
383
- setCollapsedProcessIds(readStoredIdSet(COLLAPSED_PROCESSES_STORAGE_KEY));
399
+ const storedClientIds = readStoredIdSet(COLLAPSED_CLIENTS_STORAGE_KEY);
400
+ const storedProcessIds = readStoredIdSet(COLLAPSED_PROCESSES_STORAGE_KEY);
401
+ if (storedClientIds !== null) setCollapsedClientIds(storedClientIds);
402
+ if (storedProcessIds !== null) setCollapsedProcessIds(storedProcessIds);
384
403
  setStoredCollapseStateLoaded(true);
385
404
  }, []);
386
405
 
@@ -470,38 +489,34 @@ function ClientGroupSection({
470
489
  return (
471
490
  <section
472
491
  className={cn(
473
- "bg-card/60 relative isolate overflow-hidden rounded-[8px] shadow-sm",
474
- isWorking && "bg-card/70",
492
+ "relative isolate overflow-hidden rounded-[8px] bg-card/65",
493
+ isWorking && "bg-card/80",
475
494
  )}
476
495
  data-ide-group={group.id}
477
496
  >
478
497
  <button
479
498
  type="button"
480
- className="grid w-full min-w-0 grid-cols-[2.25rem_minmax(0,1fr)_auto] items-center gap-2.5 px-3 py-2.5 text-left transition-colors hover:bg-white/[0.025] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-300/40"
499
+ className="grid w-full min-w-0 grid-cols-[2.25rem_minmax(0,1fr)_auto] items-center gap-2.5 px-3 py-2.5 text-left transition-colors hover:bg-[var(--inspector-surface-low)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
481
500
  onClick={onToggleClient}
482
501
  aria-expanded={!collapsed}
483
502
  >
484
503
  {group.client === "unknown" ? (
485
- <span className="bg-white/[0.04] inline-flex size-9 shrink-0 items-center justify-center rounded-md text-slate-300">
504
+ <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-md bg-[var(--inspector-surface-low)] text-muted-foreground">
486
505
  <Monitor className="size-4" />
487
506
  </span>
488
507
  ) : (
489
- <ClientLogo
490
- client={group.client}
491
- className="size-9 rounded-md text-cyan-100"
492
- decorative={true}
493
- />
508
+ <ClientLogo client={group.client} className="size-9 rounded-md" decorative={true} />
494
509
  )}
495
510
 
496
511
  <span className="min-w-0">
497
512
  <span className="flex min-w-0 items-center gap-2">
498
513
  <span
499
- className="truncate text-sm font-semibold text-slate-50"
514
+ className="truncate text-sm font-semibold text-foreground"
500
515
  title={group.userAgent ?? group.label}
501
516
  >
502
517
  {group.label}
503
518
  </span>
504
- <span className="rounded bg-cyan-300/[0.05] px-1.5 py-0.5 font-mono text-[9px] uppercase text-cyan-100/70">
519
+ <span className="rounded bg-[var(--inspector-surface-medium)] px-1.5 py-0.5 font-mono text-[9px] uppercase text-muted-foreground">
505
520
  {clientAppTypeLabel(group.client)}
506
521
  </span>
507
522
  {isWorking && <AncestorActivityIndicator count={pendingTurnCount} />}
@@ -524,14 +539,14 @@ function ClientGroupSection({
524
539
  <SummaryChip label="OUT" value={formatTokenCount(totals.outputTokens)} />
525
540
  </span>
526
541
  )}
527
- <span className="bg-white/[0.04] hover:bg-white/[0.07] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-slate-300">
542
+ <span className="inline-flex size-8 shrink-0 items-center justify-center rounded-md bg-[var(--inspector-surface-low)] text-muted-foreground transition-colors group-hover:bg-[var(--inspector-surface-medium)]">
528
543
  {collapsed ? <ChevronRight className="size-4" /> : <ChevronDown className="size-4" />}
529
544
  </span>
530
545
  </span>
531
546
  </button>
532
547
 
533
548
  {!collapsed && (
534
- <div className="space-y-1 px-2 pb-2 pt-1">
549
+ <div className="space-y-0.5 px-2 pb-2 pt-0.5">
535
550
  {group.processes.map((processGroup) => (
536
551
  <ProcessGroupSection
537
552
  key={processGroup.id}
@@ -567,23 +582,23 @@ function ProcessGroupSection({
567
582
  const processLabel =
568
583
  group.clientPid !== null && group.clientPid !== undefined
569
584
  ? `PID ${group.clientPid}`
570
- : "PID unavailable";
585
+ : "Process unavailable";
571
586
 
572
587
  return (
573
- <section className="relative pl-4">
588
+ <section className="relative pl-3">
574
589
  <button
575
590
  type="button"
576
- className="bg-black/[0.07] grid w-full min-w-0 grid-cols-[2rem_minmax(0,1fr)_auto] items-center gap-2.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-white/[0.03] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-300/35"
591
+ className="grid w-full min-w-0 grid-cols-[2rem_minmax(0,1fr)_auto] items-center gap-2.5 rounded-md bg-[var(--inspector-surface-low)] px-2.5 py-2 text-left transition-colors hover:bg-[var(--inspector-surface-medium)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
577
592
  onClick={onToggle}
578
593
  aria-expanded={!collapsed}
579
594
  >
580
- <span className="bg-white/[0.04] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-amber-100/85">
595
+ <span className="inline-flex size-8 shrink-0 items-center justify-center text-[var(--inspector-status-watch)]">
581
596
  <Cpu className="size-4" />
582
597
  </span>
583
598
 
584
599
  <span className="min-w-0">
585
600
  <span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
586
- <span className="font-mono text-xs font-semibold tabular-nums text-slate-100">
601
+ <span className="font-mono text-xs font-semibold tabular-nums text-foreground">
587
602
  {processLabel}
588
603
  </span>
589
604
  {isWorking && <AncestorActivityIndicator count={pendingTurnCount} />}
@@ -612,14 +627,14 @@ function ProcessGroupSection({
612
627
  <SummaryChip label="OUT" value={formatTokenCount(totals.outputTokens)} />
613
628
  </span>
614
629
  )}
615
- <span className="bg-white/[0.04] hover:bg-white/[0.07] inline-flex size-7 shrink-0 items-center justify-center rounded-md text-slate-300">
630
+ <span className="inline-flex size-7 shrink-0 items-center justify-center rounded-md bg-[var(--inspector-surface-low)] text-muted-foreground transition-colors hover:bg-[var(--inspector-surface-medium)]">
616
631
  {collapsed ? <ChevronRight className="size-4" /> : <ChevronDown className="size-4" />}
617
632
  </span>
618
633
  </span>
619
634
  </button>
620
635
 
621
636
  {!collapsed && (
622
- <div className="space-y-1.5 pb-1 pl-4 pr-1 pt-1.5">
637
+ <div className="space-y-1 pb-1 pl-3 pr-1 pt-1">
623
638
  {group.sessions.map((session) => (
624
639
  <ConversationGroup
625
640
  key={session.id}