@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
@@ -30,7 +30,6 @@ import {
30
30
  Flag,
31
31
  PanelRightClose,
32
32
  PanelRightOpen,
33
- Plus,
34
33
  RefreshCw,
35
34
  Search,
36
35
  Siren,
@@ -54,10 +53,12 @@ import { useThemeMode } from "../lib/useThemeMode";
54
53
  import { formatUiScale, normalizeUiScalePreference, type UiScalePreference } from "../lib/uiScale";
55
54
  import { useUiScale } from "../lib/useUiScale";
56
55
  import { useProviders } from "../lib/useProviders";
57
- import packageJson from "../../package.json";
58
56
  import { groupLogsByConversation, type ConversationGroupData } from "./proxy-viewer";
59
57
  import {
60
58
  buildSessionSlateStats,
59
+ COPYABLE_COMMAND_CONTAINER_CLASS_NAME,
60
+ COPYABLE_COMMAND_TEXT_CLASS_NAME,
61
+ exportRequiresConfirmation,
61
62
  formatPaginationStatus,
62
63
  getExportActionVisibility,
63
64
  resolveSessionContextScope,
@@ -68,8 +69,6 @@ import {
68
69
  } from "./proxy-viewer/proxyViewerLogic";
69
70
 
70
71
  import { CrabLogo } from "./ui/crab-logo";
71
- import { crabVariants } from "./ui/crab-variants";
72
- import { McpLogo } from "./ui/mcp-logo";
73
72
  import { Button } from "./ui/button";
74
73
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
75
74
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
@@ -79,12 +78,25 @@ import { AlertsDialog } from "./alerts/AlertsDialog";
79
78
  import { GroupsDialog } from "./groups/GroupsDialog";
80
79
  import { SettingsDialog } from "./providers/SettingsDialog";
81
80
  import { AgentLabDialog } from "./ecosystem/AgentLabDialog";
82
- import { InspectorPet } from "./inspector-pet/InspectorPet";
83
81
  import { PiAgentPanel } from "./pi-agent/PiAgentPanel";
82
+ import { LazyFeatureBoundary } from "./errors/SafeErrorBoundary";
84
83
  import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
85
- import { LazyCompareDrawer } from "./proxy-viewer/lazy";
84
+ import { LazyCompareDrawer, LazyInspectorPet } from "./proxy-viewer/lazy";
86
85
  import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
86
+ import { ApplicationBar } from "./proxy-viewer/ApplicationBar";
87
+ import {
88
+ SessionLoadProgressBar,
89
+ type SessionLoadProgress,
90
+ } from "./proxy-viewer/SessionLoadProgressBar";
91
+ import { browserPrefersReducedMotion, preferredScrollBehavior } from "./proxy-viewer/accessibility";
92
+ import { LiveConnectionStatus } from "./proxy-viewer/LiveConnectionStatus";
93
+ import {
94
+ getLiveEmptyStateCopy,
95
+ resolveLiveEmptyState,
96
+ type LiveConnectionState,
97
+ } from "./proxy-viewer/liveConnectionState";
87
98
  import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
99
+ import { RAW_EXPORT_DIALOG_CLASS_NAME, RawExportWarning } from "./proxy-viewer/RawExportWarning";
88
100
  import { buildValidPredecessors } from "./proxy-viewer/viewerState";
89
101
  import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
90
102
  import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
@@ -158,14 +170,6 @@ export type LogPaginationControls = {
158
170
  onNewest: () => void;
159
171
  };
160
172
 
161
- export type SessionLoadProgress = {
162
- label: string;
163
- elapsedMs: number;
164
- timeoutMs: number;
165
- loadedLogs: number;
166
- loadedPages: number;
167
- };
168
-
169
173
  function getFirstUserAgent(logs: CapturedLog[]): string | null {
170
174
  for (const log of logs) {
171
175
  if (log.userAgent !== null && log.userAgent !== undefined && log.userAgent !== "") {
@@ -574,7 +578,7 @@ function LogPaginationButton({
574
578
  type="button"
575
579
  onClick={onClick}
576
580
  disabled={disabled}
577
- className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-45"
581
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-45"
578
582
  title={title}
579
583
  >
580
584
  {icon}
@@ -583,44 +587,6 @@ function LogPaginationButton({
583
587
  );
584
588
  }
585
589
 
586
- function formatDurationSeconds(ms: number): string {
587
- return `${Math.max(0, Math.ceil(ms / 1000)).toString()}s`;
588
- }
589
-
590
- function SessionLoadProgressBar({ progress }: { progress: SessionLoadProgress }): JSX.Element {
591
- const elapsedRatio = Math.min(1, progress.elapsedMs / progress.timeoutMs);
592
- const percent = Math.max(4, Math.round(elapsedRatio * 100));
593
- const remainingMs = Math.max(0, progress.timeoutMs - progress.elapsedMs);
594
- const nearTimeout = elapsedRatio >= 0.75;
595
- const details = [
596
- progress.loadedLogs > 0 ? `${progress.loadedLogs.toString()} logs` : null,
597
- progress.loadedPages > 0
598
- ? `${progress.loadedPages.toString()} page${progress.loadedPages === 1 ? "" : "s"}`
599
- : null,
600
- `${formatDurationSeconds(remainingMs)} before timeout`,
601
- ].filter((item): item is string => item !== null);
602
-
603
- return (
604
- <div className="min-w-0 space-y-1 px-1 pb-1">
605
- <div className="flex min-w-0 items-center justify-between gap-3 font-mono text-[10px] text-muted-foreground">
606
- <span className="min-w-0 truncate">{progress.label}</span>
607
- <span className={cn("shrink-0 tabular-nums", nearTimeout && "text-amber-300")}>
608
- {details.join(" / ")}
609
- </span>
610
- </div>
611
- <div className="h-1.5 overflow-hidden rounded-full bg-white/[0.06]">
612
- <div
613
- className={cn(
614
- "h-full rounded-full transition-all duration-300",
615
- nearTimeout ? "bg-amber-300/80" : "bg-cyan-300/70",
616
- )}
617
- style={{ width: `${percent.toString()}%` }}
618
- />
619
- </div>
620
- </div>
621
- );
622
- }
623
-
624
590
  function LogPaginationBar({
625
591
  logs,
626
592
  pagination,
@@ -634,7 +600,8 @@ function LogPaginationBar({
634
600
  const disabled = pagination.isLoading;
635
601
 
636
602
  return (
637
- <div
603
+ <nav
604
+ aria-label="Session log pagination"
638
605
  className={cn(
639
606
  "flex min-h-9 flex-col gap-2 px-1 py-1 sm:flex-row sm:items-center",
640
607
  embedded
@@ -643,8 +610,10 @@ function LogPaginationBar({
643
610
  )}
644
611
  >
645
612
  <div className="w-full min-w-0 px-1 font-mono text-[11px] text-muted-foreground sm:flex-1">
646
- {formatPaginationStatus({ logs, pagination })}
647
- {pagination.isLoading && logs.length > 0 && <span className="ml-2">Loading...</span>}
613
+ <div role="status" aria-live="polite" aria-atomic="true">
614
+ {formatPaginationStatus({ logs, pagination })}
615
+ {pagination.isLoading && logs.length > 0 && <span className="ml-2">Loading...</span>}
616
+ </div>
648
617
  {pagination.loadProgress !== undefined && (
649
618
  <SessionLoadProgressBar progress={pagination.loadProgress} />
650
619
  )}
@@ -679,7 +648,7 @@ function LogPaginationBar({
679
648
  onClick={pagination.onNewest}
680
649
  />
681
650
  </div>
682
- </div>
651
+ </nav>
683
652
  );
684
653
  }
685
654
 
@@ -739,7 +708,10 @@ function highlightLogTarget(target: HTMLElement): void {
739
708
  function focusLogTarget(logId: number): boolean {
740
709
  const target = document.getElementById(`log-${String(logId)}`);
741
710
  if (!(target instanceof HTMLElement)) return false;
742
- target.scrollIntoView({ block: "center", behavior: "smooth" });
711
+ target.scrollIntoView({
712
+ block: "center",
713
+ behavior: preferredScrollBehavior(browserPrefersReducedMotion()),
714
+ });
743
715
  target.focus({ preventScroll: true });
744
716
  highlightLogTarget(target);
745
717
  return true;
@@ -774,12 +746,12 @@ function CopyableCommand({ command }: { command: string }): JSX.Element {
774
746
  }, [command]);
775
747
 
776
748
  return (
777
- <div className="bg-black/20 inline-flex max-w-full items-center gap-2 rounded-md px-3 py-2 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
778
- <pre className="m-0 overflow-x-auto font-mono text-sm text-cyan-200">{command}</pre>
749
+ <div data-command-example="true" className={COPYABLE_COMMAND_CONTAINER_CLASS_NAME}>
750
+ <pre className={COPYABLE_COMMAND_TEXT_CLASS_NAME}>{command}</pre>
779
751
  <button
780
752
  type="button"
781
753
  onClick={handleCopy}
782
- className="shrink-0 cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
754
+ className="shrink-0 cursor-pointer rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
783
755
  aria-label="Copy command"
784
756
  >
785
757
  {copied ? <Check className="size-4" /> : <Copy className="size-4" />}
@@ -827,60 +799,6 @@ function SidebarMetric({ label, value }: { label: string; value: string | number
827
799
  );
828
800
  }
829
801
 
830
- const CRAB_VARIANT_COLORS = [
831
- "text-amber-500",
832
- "text-rose-500",
833
- "text-sky-500",
834
- "text-emerald-500",
835
- "text-violet-500",
836
- "text-orange-500",
837
- "text-cyan-500",
838
- "text-pink-500",
839
- "text-lime-500",
840
- "text-blue-500",
841
- "text-yellow-500",
842
- "text-fuchsia-500",
843
- ] as const;
844
-
845
- function CrabFamily({
846
- compact,
847
- entrancePhase,
848
- }: {
849
- compact: boolean;
850
- entrancePhase: "hidden" | "playing" | "done";
851
- }): JSX.Element {
852
- if (compact) {
853
- return <CrabLogo className="size-14 text-amber-500" />;
854
- }
855
-
856
- return (
857
- <span className="flex shrink-0 items-end gap-1 group cursor-default" aria-hidden="true">
858
- <CrabLogo className="size-10 text-amber-500 transition-all duration-300 group-hover:scale-125 group-hover:-translate-y-1.5" />
859
- <span className="hidden items-end gap-0.5 sm:flex">
860
- {crabVariants.map((Crab, i) => {
861
- const color = CRAB_VARIANT_COLORS[i] ?? "text-amber-500";
862
- const entranceClass =
863
- entrancePhase === "hidden"
864
- ? "opacity-0 scale-0"
865
- : entrancePhase === "playing"
866
- ? "animate-crab-piano-pop"
867
- : "";
868
- return (
869
- <Crab
870
- key={i}
871
- className={`size-5 ${color} transition-all duration-300 ease-out group-hover:scale-125 group-hover:-translate-y-1 ${entranceClass}`}
872
- style={{
873
- transitionDelay: `${i * 50}ms`,
874
- ...(entrancePhase === "playing" ? { animationDelay: `${i * 400}ms` } : {}),
875
- }}
876
- />
877
- );
878
- })}
879
- </span>
880
- </span>
881
- );
882
- }
883
-
884
802
  function ThemeModeToggle(): JSX.Element {
885
803
  const { themeMode, setThemeMode } = useThemeMode();
886
804
 
@@ -894,7 +812,7 @@ function ThemeModeToggle(): JSX.Element {
894
812
  type="button"
895
813
  onClick={() => setThemeMode("light")}
896
814
  className={cn(
897
- "inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground",
815
+ "inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
898
816
  themeMode === "light" && "bg-background text-foreground shadow-sm",
899
817
  )}
900
818
  aria-label="Use light mode"
@@ -907,7 +825,7 @@ function ThemeModeToggle(): JSX.Element {
907
825
  type="button"
908
826
  onClick={() => setThemeMode("eye-care")}
909
827
  className={cn(
910
- "inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground",
828
+ "inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
911
829
  themeMode === "eye-care" && "bg-background text-foreground shadow-sm",
912
830
  )}
913
831
  aria-label="Use eye care mode"
@@ -920,7 +838,7 @@ function ThemeModeToggle(): JSX.Element {
920
838
  type="button"
921
839
  onClick={() => setThemeMode("dark")}
922
840
  className={cn(
923
- "inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground",
841
+ "inline-flex size-7 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
924
842
  themeMode === "dark" && "bg-background text-foreground shadow-sm",
925
843
  )}
926
844
  aria-label="Use dark mode"
@@ -994,93 +912,6 @@ function UiScaleControl({
994
912
  );
995
913
  }
996
914
 
997
- function BrandHeader({
998
- compact,
999
- crabEntrancePhase,
1000
- currentSessionId,
1001
- logCount,
1002
- uiScalePreference,
1003
- resolvedUiScale,
1004
- onUiScaleChange,
1005
- }: {
1006
- compact: boolean;
1007
- crabEntrancePhase: "hidden" | "playing" | "done";
1008
- currentSessionId: string | null;
1009
- logCount: number;
1010
- uiScalePreference: UiScalePreference;
1011
- resolvedUiScale: number;
1012
- onUiScaleChange: (preference: UiScalePreference) => void;
1013
- }): JSX.Element {
1014
- if (compact) {
1015
- return (
1016
- <div className="bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_16px_44px_rgba(0,0,0,0.18)] rounded-[8px] p-3">
1017
- <div className="flex flex-wrap items-start justify-between gap-3">
1018
- <div className="flex min-w-fit items-start gap-3">
1019
- <CrabFamily compact={true} entrancePhase={crabEntrancePhase} />
1020
- <div className="min-w-fit">
1021
- <div className="flex min-w-fit flex-wrap items-baseline gap-x-2 gap-y-0.5">
1022
- <span className="whitespace-nowrap text-base font-bold">Agent Inspector</span>
1023
- <span className="hidden shrink-0 font-mono text-[11px] font-semibold text-muted-foreground sm:inline">
1024
- v{packageJson.version}
1025
- </span>
1026
- </div>
1027
- <div className="hidden text-[11px] font-semibold text-muted-foreground sm:block">
1028
- Local First. Evidence First.
1029
- </div>
1030
- </div>
1031
- </div>
1032
- <div className="flex shrink-0 items-center gap-1">
1033
- <ThemeModeToggle />
1034
- <UiScaleControl
1035
- preference={uiScalePreference}
1036
- resolvedScale={resolvedUiScale}
1037
- onChange={onUiScaleChange}
1038
- />
1039
- <AlertsDialog />
1040
- <GroupsDialog />
1041
- <AgentLabDialog currentSessionId={currentSessionId} logCount={logCount} />
1042
- <SettingsDialog />
1043
- </div>
1044
- </div>
1045
- </div>
1046
- );
1047
- }
1048
-
1049
- return (
1050
- <div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-3 pb-6">
1051
- <div className="min-w-10 flex-1" aria-hidden="true" />
1052
- <h1 className="flex min-w-fit flex-col items-center gap-2 text-center">
1053
- <span className="flex max-w-full flex-wrap items-end justify-center gap-x-2 gap-y-1">
1054
- <CrabFamily compact={false} entrancePhase={crabEntrancePhase} />
1055
- <span className="flex min-w-fit flex-wrap items-baseline justify-center gap-x-2 gap-y-0.5 pl-1">
1056
- <span className="whitespace-nowrap text-lg font-bold">Agent Inspector</span>
1057
- <span className="shrink-0 font-mono text-xs font-semibold text-muted-foreground">
1058
- v{packageJson.version}
1059
- </span>
1060
- </span>
1061
- <Plus className="size-4 shrink-0 text-muted-foreground/70" aria-hidden="true" />
1062
- <McpLogo className="size-10 shrink-0" />
1063
- </span>
1064
- <span className="max-w-full text-sm font-semibold text-muted-foreground">
1065
- Local First. Evidence First.
1066
- </span>
1067
- </h1>
1068
- <div className="flex flex-1 justify-end gap-1">
1069
- <ThemeModeToggle />
1070
- <UiScaleControl
1071
- preference={uiScalePreference}
1072
- resolvedScale={resolvedUiScale}
1073
- onChange={onUiScaleChange}
1074
- />
1075
- <AlertsDialog />
1076
- <GroupsDialog />
1077
- <AgentLabDialog currentSessionId={currentSessionId} logCount={logCount} />
1078
- <SettingsDialog />
1079
- </div>
1080
- </div>
1081
- );
1082
- }
1083
-
1084
915
  const DETAILS_PANEL_COLLAPSED_STORAGE_KEY = "agent-inspector.detailsPanelCollapsed";
1085
916
  const INSPECTOR_PET_VISIBLE_STORAGE_KEY = "agent-inspector.pet.visible";
1086
917
 
@@ -1435,10 +1266,18 @@ export type ProxyViewerProps = {
1435
1266
  timeDisplayFormat: TimeDisplayFormat;
1436
1267
  /** True while a pinned session page is waiting for its initial log scan. */
1437
1268
  isLoading?: boolean;
1269
+ /** Current truth for the all-sessions live stream. Omitted on stored session pages. */
1270
+ liveConnection?: LiveConnectionState;
1271
+ /** Retry the live stream immediately without discarding displayed logs. */
1272
+ onRetryLiveConnection?: () => void;
1273
+ /** Stop automatic live-stream retries while keeping displayed logs available. */
1274
+ onStopLiveConnection?: () => void;
1438
1275
  /** Cursor controls for history pages. Undefined keeps the live stream UI compact. */
1439
1276
  pagination?: LogPaginationControls;
1440
1277
  /** Active session log loading progress, shown near load/reload controls. */
1441
1278
  sessionLoadProgress?: SessionLoadProgress;
1279
+ /** Recoverable session history load failure; existing logs remain visible. */
1280
+ sessionLoadError?: string;
1442
1281
  /** Group/run member metadata attached to the pinned session id. */
1443
1282
  sessionMemberships?: SessionMembershipEvidence[];
1444
1283
  /** Hide the session filter dropdown. Used on `/session/$id` routes where
@@ -1469,8 +1308,12 @@ export function ProxyViewer({
1469
1308
  slowResponseThresholdSeconds,
1470
1309
  timeDisplayFormat,
1471
1310
  isLoading = false,
1311
+ liveConnection,
1312
+ onRetryLiveConnection,
1313
+ onStopLiveConnection,
1472
1314
  pagination,
1473
1315
  sessionLoadProgress,
1316
+ sessionLoadError,
1474
1317
  sessionMemberships = [],
1475
1318
  hideSessionFilter = false,
1476
1319
  pinnedSessionId,
@@ -1480,6 +1323,7 @@ export function ProxyViewer({
1480
1323
  const { uiScalePreference, resolvedUiScale, setUiScalePreference } = useUiScale();
1481
1324
  const [exporting, setExporting] = useState<ExportMode | null>(null);
1482
1325
  const [exportError, setExportError] = useState<string | null>(null);
1326
+ const [rawExportDialogOpen, setRawExportDialogOpen] = useState(false);
1483
1327
  const [importing, setImporting] = useState(false);
1484
1328
  const [importStatus, setImportStatus] = useState<string | null>(null);
1485
1329
  const [importError, setImportError] = useState<string | null>(null);
@@ -1490,9 +1334,6 @@ export function ProxyViewer({
1490
1334
  const [petVisible, setPetVisible] = useState(true);
1491
1335
  const [petVisibilityStorageLoaded, setPetVisibilityStorageLoaded] = useState(false);
1492
1336
  const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
1493
- const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
1494
- "hidden",
1495
- );
1496
1337
  const logListRef = useRef<HTMLDivElement>(null);
1497
1338
  const logListWrapperRef = useRef<HTMLDivElement>(null);
1498
1339
  const importInputRef = useRef<HTMLInputElement>(null);
@@ -1517,18 +1358,6 @@ export function ProxyViewer({
1517
1358
  [],
1518
1359
  );
1519
1360
 
1520
- useEffect(() => {
1521
- const perCrabDuration = 400;
1522
- const startDelay = 50;
1523
- const playingDone = startDelay + crabVariants.length * perCrabDuration;
1524
- const t1 = setTimeout(() => setCrabEntrancePhase("playing"), startDelay);
1525
- const t2 = setTimeout(() => setCrabEntrancePhase("done"), playingDone);
1526
- return () => {
1527
- clearTimeout(t1);
1528
- clearTimeout(t2);
1529
- };
1530
- }, []);
1531
-
1532
1361
  useEffect(() => {
1533
1362
  setDetailsPanelCollapsed(readStoredDetailsPanelCollapsed());
1534
1363
  setDetailsPanelStorageLoaded(true);
@@ -1594,6 +1423,24 @@ export function ProxyViewer({
1594
1423
  },
1595
1424
  [logs, onNotify],
1596
1425
  );
1426
+ const handleExportRequest = useCallback(
1427
+ (mode: ExportMode) => {
1428
+ if (exportRequiresConfirmation(mode)) {
1429
+ setRawExportDialogOpen(true);
1430
+ return;
1431
+ }
1432
+ void handleExport(mode);
1433
+ },
1434
+ [handleExport],
1435
+ );
1436
+ const handleExportRedactedInstead = useCallback(() => {
1437
+ setRawExportDialogOpen(false);
1438
+ void handleExport("redacted");
1439
+ }, [handleExport]);
1440
+ const handleConfirmRawExport = useCallback(() => {
1441
+ setRawExportDialogOpen(false);
1442
+ void handleExport("raw");
1443
+ }, [handleExport]);
1597
1444
  const handleImportClick = useCallback(() => {
1598
1445
  importInputRef.current?.click();
1599
1446
  }, []);
@@ -1736,6 +1583,8 @@ export function ProxyViewer({
1736
1583
  : selectedSession === "__all__"
1737
1584
  ? "All sessions"
1738
1585
  : truncateSessionId(selectedSession);
1586
+ const liveEmptyState = resolveLiveEmptyState(liveConnection);
1587
+ const liveEmptyStateCopy = getLiveEmptyStateCopy(liveEmptyState);
1739
1588
  const slateStats = useMemo(
1740
1589
  () =>
1741
1590
  buildSessionSlateStats({
@@ -1776,12 +1625,17 @@ export function ProxyViewer({
1776
1625
  : selectedSession;
1777
1626
 
1778
1627
  return (
1779
- <div className="bg-background min-h-screen w-full text-foreground lg:h-screen lg:flex lg:flex-col lg:overflow-hidden">
1628
+ <div className="bg-background min-h-screen w-full overflow-x-clip text-foreground lg:h-screen lg:flex lg:flex-col lg:overflow-hidden">
1629
+ <ApplicationBar
1630
+ liveConnection={liveConnection}
1631
+ alertsAction={<AlertsDialog />}
1632
+ settingsAction={<SettingsDialog />}
1633
+ />
1780
1634
  <button
1781
1635
  type="button"
1782
1636
  onClick={toggleDetailsPanelCollapsed}
1783
1637
  className={cn(
1784
- "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",
1638
+ "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 motion-reduce:transition-none 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",
1785
1639
  detailsPanelCollapsed
1786
1640
  ? "right-3"
1787
1641
  : "lg:right-[calc(21.25rem-0.875rem)] xl:right-[calc(22.5rem-0.875rem)]",
@@ -1796,7 +1650,13 @@ export function ProxyViewer({
1796
1650
  <PanelRightClose className="size-3.5" />
1797
1651
  )}
1798
1652
  </button>
1799
- {petVisibilityStorageLoaded && petVisible && <InspectorPet logs={logs} />}
1653
+ {petVisibilityStorageLoaded && petVisible && (
1654
+ <LazyFeatureBoundary feature="Inspector Pet">
1655
+ <Suspense fallback={null}>
1656
+ <LazyInspectorPet logs={logs} />
1657
+ </Suspense>
1658
+ </LazyFeatureBoundary>
1659
+ )}
1800
1660
  <div
1801
1661
  className={cn(
1802
1662
  "grid w-full min-w-0 grid-cols-1 lg:min-h-0 lg:flex-1 lg:overflow-hidden",
@@ -1808,15 +1668,18 @@ export function ProxyViewer({
1808
1668
  {!detailsPanelCollapsed && (
1809
1669
  <aside className="bg-background/95 order-1 border-b border-white/[0.045] lg:order-2 lg:h-full lg:overflow-y-auto lg:border-b-0 lg:border-l lg:border-white/[0.055] inspector-scrollbar">
1810
1670
  <div className="space-y-4 p-4">
1811
- <BrandHeader
1812
- compact={true}
1813
- crabEntrancePhase={crabEntrancePhase}
1814
- currentSessionId={labSessionId}
1815
- logCount={logs.length}
1816
- uiScalePreference={uiScalePreference}
1817
- resolvedUiScale={resolvedUiScale}
1818
- onUiScaleChange={setUiScalePreference}
1819
- />
1671
+ <SidebarPanel label="Tools">
1672
+ <div className="flex flex-wrap items-center gap-1.5">
1673
+ <ThemeModeToggle />
1674
+ <UiScaleControl
1675
+ preference={uiScalePreference}
1676
+ resolvedScale={resolvedUiScale}
1677
+ onChange={setUiScalePreference}
1678
+ />
1679
+ <GroupsDialog />
1680
+ <AgentLabDialog currentSessionId={labSessionId} logCount={logs.length} />
1681
+ </div>
1682
+ </SidebarPanel>
1820
1683
 
1821
1684
  <SidebarPanel label="Session">
1822
1685
  <div className="grid grid-cols-2 gap-2">
@@ -1891,7 +1754,7 @@ export function ProxyViewer({
1891
1754
  <button
1892
1755
  type="button"
1893
1756
  onClick={() => onSessionChange("__all__")}
1894
- className="absolute right-1.5 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground"
1757
+ className="absolute right-1.5 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
1895
1758
  aria-label="Clear session filter"
1896
1759
  title="Clear session filter"
1897
1760
  >
@@ -1900,7 +1763,10 @@ export function ProxyViewer({
1900
1763
  )}
1901
1764
  </div>
1902
1765
  <Select value={sessionSelectValue} onValueChange={handleSessionSelectChange}>
1903
- <SelectTrigger className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]">
1766
+ <SelectTrigger
1767
+ aria-label="Select a session"
1768
+ className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]"
1769
+ >
1904
1770
  <SelectValue placeholder="All sessions" />
1905
1771
  </SelectTrigger>
1906
1772
  <SelectContent>
@@ -1920,7 +1786,10 @@ export function ProxyViewer({
1920
1786
  </>
1921
1787
  )}
1922
1788
  <Select value={selectedModel} onValueChange={onModelChange}>
1923
- <SelectTrigger className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]">
1789
+ <SelectTrigger
1790
+ aria-label="Select a model"
1791
+ className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]"
1792
+ >
1924
1793
  <SelectValue placeholder="All models" />
1925
1794
  </SelectTrigger>
1926
1795
  <SelectContent>
@@ -1961,7 +1830,7 @@ export function ProxyViewer({
1961
1830
  type="button"
1962
1831
  onClick={handleImportClick}
1963
1832
  disabled={importing}
1964
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
1833
+ className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1965
1834
  title="Import Agent Inspector export"
1966
1835
  >
1967
1836
  {importing ? (
@@ -1979,10 +1848,10 @@ export function ProxyViewer({
1979
1848
  <button
1980
1849
  type="button"
1981
1850
  onClick={() => {
1982
- void handleExport("redacted");
1851
+ handleExportRequest("redacted");
1983
1852
  }}
1984
1853
  disabled={exporting !== null}
1985
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
1854
+ className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1986
1855
  title="Export redacted logs as JSON ZIP"
1987
1856
  >
1988
1857
  {exporting === "redacted" ? (
@@ -1999,10 +1868,10 @@ export function ProxyViewer({
1999
1868
  <button
2000
1869
  type="button"
2001
1870
  onClick={() => {
2002
- void handleExport("raw");
1871
+ handleExportRequest("raw");
2003
1872
  }}
2004
1873
  disabled={exporting !== null}
2005
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
1874
+ className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
2006
1875
  title="Export raw logs without redaction"
2007
1876
  >
2008
1877
  {exporting === "raw" ? (
@@ -2020,10 +1889,15 @@ export function ProxyViewer({
2020
1889
  type="button"
2021
1890
  onClick={handleReloadCurrentSession}
2022
1891
  disabled={isLoading}
2023
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
1892
+ className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
2024
1893
  title="Reload this session from stored logs"
2025
1894
  >
2026
- <RefreshCw className={cn("size-3.5", isLoading && "animate-spin")} />
1895
+ <RefreshCw
1896
+ className={cn(
1897
+ "size-3.5",
1898
+ isLoading && "animate-spin motion-reduce:animate-none",
1899
+ )}
1900
+ />
2027
1901
  <span>{reloadSessionLabel}</span>
2028
1902
  </button>
2029
1903
  )}
@@ -2031,7 +1905,7 @@ export function ProxyViewer({
2031
1905
  type="button"
2032
1906
  onClick={handleClearCurrentView}
2033
1907
  disabled={!canClearCurrentView}
2034
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
1908
+ className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
2035
1909
  title="Clear the current view. Stored logs remain reloadable."
2036
1910
  >
2037
1911
  <Trash2 className="size-3.5" />
@@ -2042,7 +1916,7 @@ export function ProxyViewer({
2042
1916
  type="button"
2043
1917
  onClick={handleRequestDeleteCurrentView}
2044
1918
  disabled={!canDeleteCurrentView || deletingCurrentView}
2045
- className="bg-red-500/[0.07] hover:bg-red-500/[0.12] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-red-200 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.14)] transition-colors hover:text-red-100 disabled:cursor-not-allowed disabled:opacity-50"
1919
+ className="bg-red-500/[0.07] hover:bg-red-500/[0.12] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-red-200 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.14)] transition-colors hover:text-red-100 focus-visible:ring-1 focus-visible:ring-red-300 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
2046
1920
  title="Delete persisted history for the selected session."
2047
1921
  >
2048
1922
  <Trash2 className="size-3.5" />
@@ -2055,6 +1929,26 @@ export function ProxyViewer({
2055
1929
  <SessionLoadProgressBar progress={sessionLoadProgress} />
2056
1930
  </div>
2057
1931
  )}
1932
+ {sessionLoadError !== undefined && (
1933
+ <div
1934
+ role="alert"
1935
+ className="mt-2 flex flex-wrap items-center gap-2 rounded-md border border-red-400/25 bg-red-500/10 px-3 py-2 text-xs text-red-100"
1936
+ >
1937
+ <span className="min-w-0 flex-1">
1938
+ Session logs could not be loaded. {sessionLoadError} Existing logs remain visible.
1939
+ </span>
1940
+ {onReloadSession !== undefined && (
1941
+ <button
1942
+ type="button"
1943
+ onClick={handleReloadCurrentSession}
1944
+ className="inline-flex h-7 items-center gap-1 rounded-md bg-black/15 px-2 font-medium transition-colors hover:bg-black/25 focus-visible:ring-1 focus-visible:ring-current focus-visible:outline-none"
1945
+ >
1946
+ <RefreshCw className="size-3" />
1947
+ Retry
1948
+ </button>
1949
+ )}
1950
+ </div>
1951
+ )}
2058
1952
  <LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
2059
1953
  {onNotify === undefined && exportError !== null && (
2060
1954
  <div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
@@ -2071,6 +1965,17 @@ export function ProxyViewer({
2071
1965
  {importStatus}
2072
1966
  </div>
2073
1967
  )}
1968
+ {liveConnection !== undefined &&
1969
+ liveConnection.status !== "live" &&
1970
+ onRetryLiveConnection !== undefined &&
1971
+ onStopLiveConnection !== undefined && (
1972
+ <LiveConnectionStatus
1973
+ state={liveConnection}
1974
+ hasLogs={logs.length > 0}
1975
+ onRetry={onRetryLiveConnection}
1976
+ onStop={onStopLiveConnection}
1977
+ />
1978
+ )}
2074
1979
  </div>
2075
1980
 
2076
1981
  <div className="px-4 py-4 lg:px-5">
@@ -2080,7 +1985,11 @@ export function ProxyViewer({
2080
1985
  <div className="py-16 text-center text-muted-foreground">
2081
1986
  <div className="bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)] mx-auto max-w-xl space-y-4 rounded-[8px] px-4 py-8">
2082
1987
  <p className="text-sm font-medium">
2083
- {isLoading ? "Loading session data..." : "No logs loaded for this session"}
1988
+ {isLoading
1989
+ ? "Loading session data..."
1990
+ : sessionLoadError === undefined
1991
+ ? "No logs loaded for this session"
1992
+ : "Session logs could not be loaded"}
2084
1993
  </p>
2085
1994
  <p className="inline-block max-w-full rounded bg-muted px-3 py-1 font-mono text-xs break-all">
2086
1995
  {truncateSessionId(selectedSession)}
@@ -2103,7 +2012,7 @@ export function ProxyViewer({
2103
2012
  <button
2104
2013
  type="button"
2105
2014
  onClick={handleReloadCurrentSession}
2106
- className="bg-white/[0.045] hover:bg-white/[0.075] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground"
2015
+ className="bg-white/[0.045] hover:bg-white/[0.075] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
2107
2016
  >
2108
2017
  <RefreshCw className="size-3.5" />
2109
2018
  <span>Load session</span>
@@ -2112,7 +2021,7 @@ export function ProxyViewer({
2112
2021
  {hideSessionFilter ? (
2113
2022
  <a
2114
2023
  href="/"
2115
- className="inline-flex h-8 items-center rounded-md px-3 text-xs text-muted-foreground underline transition-colors hover:text-foreground"
2024
+ className="inline-flex h-8 items-center rounded-md px-3 text-xs text-muted-foreground underline transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
2116
2025
  >
2117
2026
  Back to all sessions
2118
2027
  </a>
@@ -2120,7 +2029,7 @@ export function ProxyViewer({
2120
2029
  <button
2121
2030
  type="button"
2122
2031
  onClick={() => onSessionChange("__all__")}
2123
- className="inline-flex h-8 items-center rounded-md px-3 text-xs text-muted-foreground underline transition-colors hover:text-foreground"
2032
+ className="inline-flex h-8 items-center rounded-md px-3 text-xs text-muted-foreground underline transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
2124
2033
  >
2125
2034
  Show all sessions
2126
2035
  </button>
@@ -2133,21 +2042,28 @@ export function ProxyViewer({
2133
2042
  ) : (
2134
2043
  <div className="py-16 text-center text-muted-foreground">
2135
2044
  <div className="bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)] mx-auto max-w-2xl space-y-4 rounded-[8px] px-4 py-8">
2136
- <p className="text-sm">No logs loaded in the current view.</p>
2137
- {sessions.length > 0 && (
2045
+ <p className="text-sm font-medium">{liveEmptyStateCopy.title}</p>
2046
+ <p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
2047
+ {liveEmptyStateCopy.description}
2048
+ </p>
2049
+ {liveEmptyState === "empty" && sessions.length > 0 && (
2138
2050
  <p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
2139
2051
  Pick a session from the filter panel to load stored logs on demand.
2140
2052
  </p>
2141
2053
  )}
2142
- <div className="flex flex-col items-center gap-2">
2143
- <CopyableCommand command="ANTHROPIC_BASE_URL=http://localhost:9527/proxy <your-tool>" />
2144
- <CopyableCommand command="OPENAI_BASE_URL=http://localhost:9527/proxy <your-tool>" />
2145
- </div>
2146
- <p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
2147
- Container note: if your AI tool runs in a different container or host than
2148
- Agent Inspector, replace localhost with an address reachable from that tool
2149
- and verify the network/firewall path.
2150
- </p>
2054
+ {liveEmptyStateCopy.showConnectionCommands && (
2055
+ <>
2056
+ <div className="flex flex-col items-center gap-2">
2057
+ <CopyableCommand command="ANTHROPIC_BASE_URL=http://localhost:9527/proxy <your-tool>" />
2058
+ <CopyableCommand command="OPENAI_BASE_URL=http://localhost:9527/proxy <your-tool>" />
2059
+ </div>
2060
+ <p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
2061
+ Container note: if your AI tool runs in a different container or host than
2062
+ Agent Inspector, replace localhost with an address reachable from that
2063
+ tool and verify the network/firewall path.
2064
+ </p>
2065
+ </>
2066
+ )}
2151
2067
  </div>
2152
2068
  </div>
2153
2069
  )
@@ -2155,7 +2071,8 @@ export function ProxyViewer({
2155
2071
  <div
2156
2072
  ref={logListWrapperRef}
2157
2073
  tabIndex={0}
2158
- className="flex flex-col gap-2 focus:outline-none"
2074
+ aria-label="Captured logs"
2075
+ className="flex flex-col gap-2 rounded-md focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none"
2159
2076
  >
2160
2077
  <div ref={logListRef}>
2161
2078
  <ConversationGroupList
@@ -2180,6 +2097,16 @@ export function ProxyViewer({
2180
2097
  </main>
2181
2098
  </div>
2182
2099
 
2100
+ <Dialog open={rawExportDialogOpen} onOpenChange={setRawExportDialogOpen}>
2101
+ <DialogContent className={RAW_EXPORT_DIALOG_CLASS_NAME}>
2102
+ <RawExportWarning
2103
+ onCancel={() => setRawExportDialogOpen(false)}
2104
+ onExportRedacted={handleExportRedactedInstead}
2105
+ onConfirmRaw={handleConfirmRawExport}
2106
+ />
2107
+ </DialogContent>
2108
+ </Dialog>
2109
+
2183
2110
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
2184
2111
  <DialogContent className="border-red-500/20 bg-zinc-950 text-zinc-100 sm:max-w-md">
2185
2112
  <DialogHeader>
@@ -2217,15 +2144,17 @@ export function ProxyViewer({
2217
2144
 
2218
2145
  {/* Compare drawer: sibling of the log list, not a route change. */}
2219
2146
  {comparePair !== null && (
2220
- <Suspense fallback={null}>
2221
- <LazyCompareDrawer
2222
- left={comparePair[0]}
2223
- right={comparePair[1]}
2224
- leftDisplayNumber={displayNumberForLog(comparePair[0], displayNumberByLogId)}
2225
- rightDisplayNumber={displayNumberForLog(comparePair[1], displayNumberByLogId)}
2226
- onClose={closeCompare}
2227
- />
2228
- </Suspense>
2147
+ <LazyFeatureBoundary feature="Compare">
2148
+ <Suspense fallback={null}>
2149
+ <LazyCompareDrawer
2150
+ left={comparePair[0]}
2151
+ right={comparePair[1]}
2152
+ leftDisplayNumber={displayNumberForLog(comparePair[0], displayNumberByLogId)}
2153
+ rightDisplayNumber={displayNumberForLog(comparePair[1], displayNumberByLogId)}
2154
+ onClose={closeCompare}
2155
+ />
2156
+ </Suspense>
2157
+ </LazyFeatureBoundary>
2229
2158
  )}
2230
2159
  </div>
2231
2160
  );