@tonyclaw/agent-inspector 3.0.29 → 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 (152) 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-CctnITbt.js → ReplayDialog-DodiTL6E.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-BrN0RBbL.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-DKM91udC.js → _sessionId-Cj_HIFZx.js} +1 -1
  12. package/.output/public/assets/{index-Bq-nqolG.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-DPiESBo2.js → index-ZlhqCrSg.js} +6 -6
  17. package/.output/public/assets/{json-viewer-CfaDJwx8.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-CwGXPD4C.mjs → _sessionId-C5S3pGZd.mjs} +15 -15
  25. package/.output/server/_sessionId-g6D69lKq.mjs +81 -0
  26. package/.output/server/_ssr/{CompareDrawer-R_P1-VNr.mjs → CompareDrawer-CqUmGHal.mjs} +138 -103
  27. package/.output/server/_ssr/InspectorPet-DI0UJd01.mjs +925 -0
  28. package/.output/server/_ssr/{ProxyViewerContainer-DUgxRNbj.mjs → ProxyViewerContainer-CvsUmbJ3.mjs} +1157 -1613
  29. package/.output/server/_ssr/{ReplayDialog-De9wKKzS.mjs → ReplayDialog-DS48dpFG.mjs} +18 -18
  30. package/.output/server/_ssr/{RequestAnatomy-Db4MxUYo.mjs → RequestAnatomy-DxYVQU1C.mjs} +58 -58
  31. package/.output/server/_ssr/{ResponseView-8rDXOiNB.mjs → ResponseView-BLXW73eq.mjs} +60 -60
  32. package/.output/server/_ssr/{StreamingChunkSequence-N9teZHkA.mjs → StreamingChunkSequence-DD0iFuy0.mjs} +16 -16
  33. package/.output/server/_ssr/{index-D4DvByuW.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-BjoHRbjS.mjs → json-viewer-LDu2XBaK.mjs} +58 -58
  37. package/.output/server/_ssr/{router-BYlePDj8.mjs → router-GKvrwifJ.mjs} +9005 -6488
  38. package/.output/server/_tanstack-start-manifest_v-Df2n4mbT.mjs +4 -0
  39. package/.output/server/index.mjs +103 -75
  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/ecosystem/AgentLabDialog.tsx +166 -19
  50. package/src/components/errors/SafeErrorBoundary.tsx +201 -0
  51. package/src/components/pi-agent/PiAgentPanel.tsx +4 -1
  52. package/src/components/providers/ImportWizardDialog.tsx +18 -42
  53. package/src/components/providers/ProviderLogo.tsx +1 -1
  54. package/src/components/providers/ProvidersPanel.tsx +8 -5
  55. package/src/components/providers/SettingsDialog.tsx +34 -11
  56. package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
  57. package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
  58. package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
  59. package/src/components/proxy-viewer/LogEntry.tsx +283 -275
  60. package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
  61. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
  62. package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
  63. package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
  64. package/src/components/proxy-viewer/accessibility.ts +8 -0
  65. package/src/components/proxy-viewer/lazy.ts +4 -0
  66. package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
  67. package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
  68. package/src/components/proxy-viewer/viewerState.ts +2 -6
  69. package/src/components/ui/select.tsx +1 -1
  70. package/src/components/ui/tabs.tsx +3 -3
  71. package/src/components/ui/transient-toast.tsx +1 -1
  72. package/src/lib/apiClient.ts +17 -2
  73. package/src/lib/ecosystemContract.ts +34 -0
  74. package/src/lib/export-logs.ts +1 -1
  75. package/src/lib/providerContract.ts +70 -4
  76. package/src/lib/providerImportContract.ts +27 -0
  77. package/src/lib/providerModelMetadata.ts +16 -7
  78. package/src/lib/resourceLimits.ts +152 -0
  79. package/src/lib/safeDiagnostic.ts +38 -0
  80. package/src/lib/useProviders.ts +4 -4
  81. package/src/mcp/server.ts +39 -6
  82. package/src/mcp/toolHandlers.ts +131 -4
  83. package/src/proxy/chunkStorage.ts +20 -6
  84. package/src/proxy/config.ts +20 -6
  85. package/src/proxy/dataDir.ts +3 -0
  86. package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
  87. package/src/proxy/ecosystemExecutionRoute.ts +116 -0
  88. package/src/proxy/ecosystemTasks.ts +48 -0
  89. package/src/proxy/evidenceExporter.ts +23 -9
  90. package/src/proxy/groupEvidenceExporter.ts +26 -5
  91. package/src/proxy/groupStore.ts +13 -3
  92. package/src/proxy/handler.ts +103 -20
  93. package/src/proxy/identityProxy.ts +333 -14
  94. package/src/proxy/logFinalizer.ts +73 -6
  95. package/src/proxy/logImportUpload.ts +128 -0
  96. package/src/proxy/logImporter.ts +321 -70
  97. package/src/proxy/logIndex.ts +16 -6
  98. package/src/proxy/logger.ts +294 -41
  99. package/src/proxy/privateDataPath.ts +183 -0
  100. package/src/proxy/providerScanStore.ts +87 -0
  101. package/src/proxy/providerSecretStore.ts +58 -19
  102. package/src/proxy/providers.ts +175 -58
  103. package/src/proxy/rawStreamCapture.ts +66 -5
  104. package/src/proxy/runStore.ts +13 -3
  105. package/src/proxy/runtimeAdmission.ts +52 -0
  106. package/src/proxy/runtimeHealth.ts +206 -0
  107. package/src/proxy/runtimeShutdown.ts +75 -0
  108. package/src/proxy/sessionArchive.ts +15 -2
  109. package/src/proxy/sessionProcess.ts +19 -0
  110. package/src/proxy/sessionRuntime.ts +7 -0
  111. package/src/proxy/shutdownCoordinator.ts +90 -0
  112. package/src/proxy/sqliteLogIndex.ts +18 -2
  113. package/src/proxy/store.ts +18 -2
  114. package/src/routes/__root.tsx +18 -1
  115. package/src/routes/api/alerts.summary.ts +12 -8
  116. package/src/routes/api/alerts.ts +27 -9
  117. package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
  118. package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
  119. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
  120. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
  121. package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
  122. package/src/routes/api/health.ts +9 -2
  123. package/src/routes/api/knowledge.project-context.ts +11 -0
  124. package/src/routes/api/knowledge.search.ts +33 -2
  125. package/src/routes/api/logs.$id.body.ts +16 -2
  126. package/src/routes/api/logs.import.ts +7 -18
  127. package/src/routes/api/logs.stream.ts +147 -69
  128. package/src/routes/api/logs.ts +55 -14
  129. package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
  130. package/src/routes/api/providers.$providerId.ts +37 -6
  131. package/src/routes/api/providers.export.ts +34 -16
  132. package/src/routes/api/providers.import.ts +42 -8
  133. package/src/routes/api/providers.scan.ts +13 -8
  134. package/src/routes/api/providers.ts +36 -5
  135. package/src/routes/api/runs.ts +12 -12
  136. package/src/routes/api/sessions.ts +15 -8
  137. package/src/routes/index.tsx +6 -0
  138. package/src/routes/livez.ts +13 -0
  139. package/src/routes/readyz.ts +18 -0
  140. package/src/routes/session/$sessionId.tsx +6 -0
  141. package/styles/globals.css +19 -3
  142. package/.output/public/assets/CompareDrawer-Cb5ccPeX.js +0 -1
  143. package/.output/public/assets/ProxyViewerContainer-COBe5GTT.js +0 -4234
  144. package/.output/public/assets/ResponseView-Cqq87lX_.js +0 -3
  145. package/.output/public/assets/StreamingChunkSequence-CnTa9mCr.js +0 -1
  146. package/.output/public/assets/index-DTLNVcgc.js +0 -76
  147. package/.output/public/assets/index-DliqmwUw.css +0 -1
  148. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  149. package/.output/server/_tanstack-start-manifest_v-DMCA8KK3.mjs +0 -4
  150. package/src/assets/logos/mcp.png +0 -0
  151. package/src/assets/logos/qwen.png +0 -0
  152. package/src/components/ui/mcp-logo.tsx +0 -20
@@ -13,15 +13,20 @@ import { OnboardingBanner } from "./OnboardingBanner";
13
13
  import {
14
14
  ProxyViewer,
15
15
  type LogPaginationControls,
16
- type SessionLoadProgress,
17
16
  type SessionMembershipEvidence,
18
17
  } from "./ProxyViewer";
18
+ import type { SessionLoadProgress } from "./proxy-viewer/SessionLoadProgressBar";
19
+ import { browserPrefersReducedMotion, preferredScrollBehavior } from "./proxy-viewer/accessibility";
19
20
  import {
20
21
  filterLogs,
21
22
  mergeLogsById,
22
23
  mergeSessionIds,
23
24
  } from "./proxy-viewer/proxyViewerContainerLogic";
24
25
  import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
26
+ import {
27
+ createInitialLiveConnectionState,
28
+ reduceLiveConnectionState,
29
+ } from "./proxy-viewer/liveConnectionState";
25
30
  import { TransientToast, useTransientToast } from "./ui/transient-toast";
26
31
 
27
32
  type SSEUpdate =
@@ -125,12 +130,13 @@ const SESSION_RELOAD_PAGE_LIMIT = 500;
125
130
  const MAX_SESSION_RELOAD_PAGES = 200;
126
131
  const SESSION_LOGS_FETCH_TIMEOUT_MS = 20_000;
127
132
  const SESSION_MEMBERSHIP_FETCH_TIMEOUT_MS = 10_000;
133
+ const SSE_RECONNECT_DELAY_MS = 3_000;
134
+ const SSE_STALE_AFTER_MS = 10_000;
128
135
 
129
136
  function buildLogsStreamUrl(sessionId: string | undefined): string {
130
- const params = new URLSearchParams();
137
+ const params = new URLSearchParams({ compact: "1" });
131
138
  if (sessionId !== undefined) {
132
139
  params.set("sessionId", sessionId);
133
- params.set("compact", "1");
134
140
  }
135
141
  const query = params.toString();
136
142
  return query.length > 0 ? `/api/logs/stream?${query}` : "/api/logs/stream";
@@ -253,9 +259,10 @@ export function ProxyViewerContainer({
253
259
  const [selectedModel, setSelectedModel] = useState("__all__");
254
260
  const [knownSessions, setKnownSessions] = useState<string[]>([]);
255
261
  const { notice, showToast, dismissToast } = useTransientToast();
256
- const [streamInitialized, setStreamInitialized] = useState(initialSessionId === undefined);
262
+ const [liveConnection, setLiveConnection] = useState(createInitialLiveConnectionState);
257
263
  const [logPage, setLogPage] = useState<LogCursorPage | null>(null);
258
264
  const [sessionPageLoading, setSessionPageLoading] = useState(initialSessionId !== undefined);
265
+ const [sessionLoadError, setSessionLoadError] = useState<string | null>(null);
259
266
  const [sessionLoadProgress, setSessionLoadProgress] = useState<SessionLoadProgress | undefined>(
260
267
  undefined,
261
268
  );
@@ -263,6 +270,8 @@ export function ProxyViewerContainer({
263
270
  const [, startLogTransition] = useTransition();
264
271
  const eventSourceRef = useRef<EventSource | null>(null);
265
272
  const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
273
+ const staleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
274
+ const automaticRetryEnabledRef = useRef(true);
266
275
  const handledHashRef = useRef<string | null>(null);
267
276
  const sessionPageRequestIdRef = useRef(0);
268
277
  const sessionLoadStartedAtRef = useRef<number | null>(null);
@@ -381,7 +390,11 @@ export function ProxyViewerContainer({
381
390
  eventSourceRef.current.close();
382
391
  }
383
392
 
384
- setStreamInitialized(false);
393
+ if (reconnectTimeoutRef.current !== null) {
394
+ clearTimeout(reconnectTimeoutRef.current);
395
+ reconnectTimeoutRef.current = null;
396
+ }
397
+ setLiveConnection((state) => reduceLiveConnectionState(state, { type: "connect-requested" }));
385
398
  const es = new EventSource(buildLogsStreamUrl(initialSessionId));
386
399
  eventSourceRef.current = es;
387
400
 
@@ -391,6 +404,12 @@ export function ProxyViewerContainer({
391
404
  const parsed: unknown = JSON.parse(rawData);
392
405
  const updateResult = SSEUpdateSchema.safeParse(parsed);
393
406
  if (!updateResult.success) {
407
+ setLiveConnection((state) =>
408
+ reduceLiveConnectionState(state, {
409
+ type: "recoverable-error",
410
+ message: "The live stream returned invalid data.",
411
+ }),
412
+ );
394
413
  showErrorToast("Failed to parse SSE data");
395
414
  return;
396
415
  }
@@ -409,7 +428,13 @@ export function ProxyViewerContainer({
409
428
  return nextLogs;
410
429
  });
411
430
  setKnownSessions((prev) => mergeSessionIds(prev, extractSessions(update.logs)));
412
- setStreamInitialized(true);
431
+ if (staleTimeoutRef.current !== null) {
432
+ clearTimeout(staleTimeoutRef.current);
433
+ staleTimeoutRef.current = null;
434
+ }
435
+ setLiveConnection((state) =>
436
+ reduceLiveConnectionState(state, { type: "message-received", at: Date.now() }),
437
+ );
413
438
  dismissToast();
414
439
  } else if (update.type === "update") {
415
440
  setKnownSessions((prev) => {
@@ -417,23 +442,70 @@ export function ProxyViewerContainer({
417
442
  return mergeSessionIds(prev, [update.log.sessionId]);
418
443
  });
419
444
  scheduleUpdate(update.log);
445
+ if (staleTimeoutRef.current !== null) {
446
+ clearTimeout(staleTimeoutRef.current);
447
+ staleTimeoutRef.current = null;
448
+ }
449
+ setLiveConnection((state) =>
450
+ reduceLiveConnectionState(state, { type: "message-received", at: Date.now() }),
451
+ );
420
452
  }
421
453
  } catch {
454
+ setLiveConnection((state) =>
455
+ reduceLiveConnectionState(state, {
456
+ type: "recoverable-error",
457
+ message: "The live stream returned malformed JSON.",
458
+ }),
459
+ );
422
460
  showErrorToast("Failed to parse SSE data");
423
461
  }
424
462
  };
425
463
 
426
464
  es.onerror = () => {
427
- showErrorToast("SSE connection lost, reconnecting...");
428
- setStreamInitialized(true);
465
+ const online = navigator.onLine;
466
+ setLiveConnection((state) =>
467
+ reduceLiveConnectionState(state, {
468
+ type: "connection-lost",
469
+ online,
470
+ message: "The live stream disconnected.",
471
+ }),
472
+ );
429
473
  es.close();
430
- if (reconnectTimeoutRef.current !== null) {
431
- clearTimeout(reconnectTimeoutRef.current);
474
+ if (!online || !automaticRetryEnabledRef.current) return;
475
+ if (staleTimeoutRef.current === null) {
476
+ staleTimeoutRef.current = setTimeout(() => {
477
+ staleTimeoutRef.current = null;
478
+ setLiveConnection((state) => reduceLiveConnectionState(state, { type: "stale-timeout" }));
479
+ }, SSE_STALE_AFTER_MS);
432
480
  }
433
- reconnectTimeoutRef.current = setTimeout(connectSSE, 3000);
481
+ reconnectTimeoutRef.current = setTimeout(connectSSE, SSE_RECONNECT_DELAY_MS);
434
482
  };
435
483
  }, [dismissToast, initialSessionId, scheduleUpdate, showErrorToast]);
436
484
 
485
+ const retryLiveConnection = useCallback(() => {
486
+ automaticRetryEnabledRef.current = true;
487
+ if (reconnectTimeoutRef.current !== null) {
488
+ clearTimeout(reconnectTimeoutRef.current);
489
+ reconnectTimeoutRef.current = null;
490
+ }
491
+ connectSSE();
492
+ }, [connectSSE]);
493
+
494
+ const stopLiveConnection = useCallback(() => {
495
+ automaticRetryEnabledRef.current = false;
496
+ eventSourceRef.current?.close();
497
+ eventSourceRef.current = null;
498
+ if (reconnectTimeoutRef.current !== null) {
499
+ clearTimeout(reconnectTimeoutRef.current);
500
+ reconnectTimeoutRef.current = null;
501
+ }
502
+ if (staleTimeoutRef.current !== null) {
503
+ clearTimeout(staleTimeoutRef.current);
504
+ staleTimeoutRef.current = null;
505
+ }
506
+ setLiveConnection((state) => reduceLiveConnectionState(state, { type: "stop-retrying" }));
507
+ }, []);
508
+
437
509
  const loadSessionPage = useCallback(
438
510
  (request: SessionPageRequest) => {
439
511
  if (initialSessionId === undefined) return;
@@ -441,6 +513,7 @@ export function ProxyViewerContainer({
441
513
  const requestId = sessionPageRequestIdRef.current + 1;
442
514
  sessionPageRequestIdRef.current = requestId;
443
515
  setSessionPageLoading(true);
516
+ setSessionLoadError(null);
444
517
  startSessionLoadProgress("Loading session logs", requestId);
445
518
  dismissToast();
446
519
 
@@ -453,16 +526,17 @@ export function ProxyViewerContainer({
453
526
  setLogPage(page);
454
527
  setSelectedSession(initialSessionId);
455
528
  setSessionPageLoading(false);
529
+ setSessionLoadError(null);
456
530
  stopSessionLoadProgress();
457
531
  dismissToast();
458
532
  })
459
533
  .catch((err: unknown) => {
460
534
  if (sessionPageRequestIdRef.current !== requestId) return;
461
- setAllLogs([]);
462
- setLogPage(null);
463
535
  setSessionPageLoading(false);
464
536
  stopSessionLoadProgress();
465
- showErrorToast(err instanceof Error ? err.message : "Failed to load session logs");
537
+ const message = err instanceof Error ? err.message : "Failed to load session logs";
538
+ setSessionLoadError(message);
539
+ showErrorToast(message);
466
540
  });
467
541
  },
468
542
  [
@@ -479,6 +553,7 @@ export function ProxyViewerContainer({
479
553
  const requestId = sessionPageRequestIdRef.current + 1;
480
554
  sessionPageRequestIdRef.current = requestId;
481
555
  setSessionPageLoading(true);
556
+ setSessionLoadError(null);
482
557
  startSessionLoadProgress("Reloading full session", requestId);
483
558
  dismissToast();
484
559
 
@@ -521,6 +596,7 @@ export function ProxyViewerContainer({
521
596
  });
522
597
  setKnownSessions((prev) => mergeSessionIds(prev, [sessionId]));
523
598
  setSessionPageLoading(false);
599
+ setSessionLoadError(null);
524
600
  stopSessionLoadProgress();
525
601
  if (page.hasNewer) {
526
602
  showToast({
@@ -534,7 +610,9 @@ export function ProxyViewerContainer({
534
610
  if (sessionPageRequestIdRef.current !== requestId) return;
535
611
  setSessionPageLoading(false);
536
612
  stopSessionLoadProgress();
537
- showErrorToast(err instanceof Error ? err.message : "Failed to reload session logs");
613
+ const message = err instanceof Error ? err.message : "Failed to reload session logs";
614
+ setSessionLoadError(message);
615
+ showErrorToast(message);
538
616
  });
539
617
  },
540
618
  [
@@ -597,7 +675,28 @@ export function ProxyViewerContainer({
597
675
  useEffect(() => {
598
676
  if (initialSessionId !== undefined) return undefined;
599
677
  connectSSE();
678
+ const handleOffline = (): void => {
679
+ eventSourceRef.current?.close();
680
+ eventSourceRef.current = null;
681
+ if (reconnectTimeoutRef.current !== null) {
682
+ clearTimeout(reconnectTimeoutRef.current);
683
+ reconnectTimeoutRef.current = null;
684
+ }
685
+ if (staleTimeoutRef.current !== null) {
686
+ clearTimeout(staleTimeoutRef.current);
687
+ staleTimeoutRef.current = null;
688
+ }
689
+ setLiveConnection((state) => reduceLiveConnectionState(state, { type: "browser-offline" }));
690
+ };
691
+ const handleOnline = (): void => {
692
+ if (!automaticRetryEnabledRef.current) return;
693
+ connectSSE();
694
+ };
695
+ window.addEventListener("offline", handleOffline);
696
+ window.addEventListener("online", handleOnline);
600
697
  return () => {
698
+ window.removeEventListener("offline", handleOffline);
699
+ window.removeEventListener("online", handleOnline);
601
700
  if (eventSourceRef.current) {
602
701
  eventSourceRef.current.close();
603
702
  eventSourceRef.current = null;
@@ -606,6 +705,10 @@ export function ProxyViewerContainer({
606
705
  clearTimeout(reconnectTimeoutRef.current);
607
706
  reconnectTimeoutRef.current = null;
608
707
  }
708
+ if (staleTimeoutRef.current !== null) {
709
+ clearTimeout(staleTimeoutRef.current);
710
+ staleTimeoutRef.current = null;
711
+ }
609
712
  if (flushTimerRef.current !== null) {
610
713
  clearTimeout(flushTimerRef.current);
611
714
  flushTimerRef.current = null;
@@ -632,7 +735,10 @@ export function ProxyViewerContainer({
632
735
  const target = document.getElementById(targetId);
633
736
  if (target !== null) {
634
737
  handledHashRef.current = hash;
635
- target.scrollIntoView({ block: "center", behavior: "smooth" });
738
+ target.scrollIntoView({
739
+ block: "center",
740
+ behavior: preferredScrollBehavior(browserPrefersReducedMotion()),
741
+ });
636
742
  if (target instanceof HTMLElement) {
637
743
  highlightedTarget = target;
638
744
  target.setAttribute("data-deep-link-highlight", "true");
@@ -926,11 +1032,16 @@ export function ProxyViewerContainer({
926
1032
  onImportLogs={initialSessionId === undefined ? handleImportLogs : undefined}
927
1033
  isLoading={
928
1034
  initialSessionId === undefined
929
- ? !streamInitialized || sessionPageLoading
1035
+ ? (liveConnection.status === "connecting" && liveConnection.lastSuccessAt === null) ||
1036
+ sessionPageLoading
930
1037
  : sessionPageLoading
931
1038
  }
1039
+ liveConnection={initialSessionId === undefined ? liveConnection : undefined}
1040
+ onRetryLiveConnection={retryLiveConnection}
1041
+ onStopLiveConnection={stopLiveConnection}
932
1042
  pagination={pagination}
933
1043
  sessionLoadProgress={sessionLoadProgress}
1044
+ sessionLoadError={sessionLoadError ?? undefined}
934
1045
  sessionMemberships={sessionMemberships}
935
1046
  viewMode={viewMode}
936
1047
  captureMode={captureMode}
@@ -17,12 +17,14 @@ import {
17
17
  } from "lucide-react";
18
18
  import useSWR from "swr";
19
19
  import {
20
+ EcosystemExecutionConfirmationResponseSchema,
20
21
  EcosystemPackagesResponseSchema,
21
22
  EcosystemTaskResponseSchema,
22
23
  EcosystemTasksResponseSchema,
23
24
  type EcosystemPackage,
24
25
  type EcosystemPackageState,
25
26
  type EcosystemRecipe,
27
+ type EcosystemExecutionConfirmation,
26
28
  type EcosystemRunnerPreset,
27
29
  type EcosystemTask,
28
30
  type EcosystemTaskAction,
@@ -32,7 +34,14 @@ import { copyTextToClipboard } from "../../lib/clipboard";
32
34
  import { cn } from "../../lib/utils";
33
35
  import { Badge } from "../ui/badge";
34
36
  import { Button } from "../ui/button";
35
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
37
+ import {
38
+ Dialog,
39
+ DialogContent,
40
+ DialogDescription,
41
+ DialogHeader,
42
+ DialogTitle,
43
+ DialogTrigger,
44
+ } from "../ui/dialog";
36
45
  import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
37
46
 
38
47
  type AgentLabDialogProps = {
@@ -49,6 +58,11 @@ type WorkflowCard = {
49
58
  icon: typeof Activity;
50
59
  };
51
60
 
61
+ type PendingEcosystemExecution = {
62
+ endpoint: string;
63
+ confirmation: EcosystemExecutionConfirmation;
64
+ };
65
+
52
66
  const WORKFLOWS: WorkflowCard[] = [
53
67
  {
54
68
  id: "observe",
@@ -113,25 +127,55 @@ async function fetchEcosystemTasks(url: string) {
113
127
  );
114
128
  }
115
129
 
116
- async function startEcosystemTask(
130
+ async function prepareEcosystemTask(
117
131
  packageId: string,
118
132
  action: EcosystemTaskAction,
119
- ): Promise<EcosystemTask> {
133
+ ): Promise<PendingEcosystemExecution> {
134
+ const endpoint = `/api/ecosystem/packages/${encodeURIComponent(packageId)}/${action}`;
120
135
  const response = await fetchJson(
121
- `/api/ecosystem/packages/${encodeURIComponent(packageId)}/${action}`,
122
- EcosystemTaskResponseSchema,
123
- { method: "POST" },
124
- (result) => `Failed to start ecosystem task: ${String(result.status)}`,
136
+ endpoint,
137
+ EcosystemExecutionConfirmationResponseSchema,
138
+ {
139
+ method: "POST",
140
+ headers: { "content-type": "application/json" },
141
+ body: JSON.stringify({ intent: "prepare" }),
142
+ },
143
+ (result) => `Failed to prepare ecosystem task: ${String(result.status)}`,
125
144
  );
126
- return response.task;
145
+ return { endpoint, confirmation: response.confirmation };
146
+ }
147
+
148
+ async function prepareEcosystemRecipe(recipeId: string): Promise<PendingEcosystemExecution> {
149
+ const endpoint = `/api/ecosystem/recipes/${encodeURIComponent(recipeId)}/run`;
150
+ const response = await fetchJson(
151
+ endpoint,
152
+ EcosystemExecutionConfirmationResponseSchema,
153
+ {
154
+ method: "POST",
155
+ headers: { "content-type": "application/json" },
156
+ body: JSON.stringify({ intent: "prepare" }),
157
+ },
158
+ (result) => `Failed to prepare ecosystem recipe: ${String(result.status)}`,
159
+ );
160
+ return { endpoint, confirmation: response.confirmation };
127
161
  }
128
162
 
129
- async function startEcosystemRecipe(recipeId: string): Promise<EcosystemTask> {
163
+ async function confirmEcosystemExecution(
164
+ pending: PendingEcosystemExecution,
165
+ ): Promise<EcosystemTask> {
130
166
  const response = await fetchJson(
131
- `/api/ecosystem/recipes/${encodeURIComponent(recipeId)}/run`,
167
+ pending.endpoint,
132
168
  EcosystemTaskResponseSchema,
133
- { method: "POST" },
134
- (result) => `Failed to start ecosystem recipe: ${String(result.status)}`,
169
+ {
170
+ method: "POST",
171
+ headers: { "content-type": "application/json" },
172
+ body: JSON.stringify({
173
+ intent: "confirm",
174
+ nonce: pending.confirmation.nonce,
175
+ confirmation: pending.confirmation.phrase,
176
+ }),
177
+ },
178
+ (result) => `Failed to start ecosystem task: ${String(result.status)}`,
135
179
  );
136
180
  return response.task;
137
181
  }
@@ -499,6 +543,84 @@ function RecentTaskRow({
499
543
  );
500
544
  }
501
545
 
546
+ function ExecutionConfirmationDialog({
547
+ pending,
548
+ busy,
549
+ onOpenChange,
550
+ onConfirm,
551
+ }: {
552
+ pending: PendingEcosystemExecution | null;
553
+ busy: boolean;
554
+ onOpenChange: (open: boolean) => void;
555
+ onConfirm: () => void;
556
+ }): JSX.Element {
557
+ const plan = pending?.confirmation.plan ?? null;
558
+ return (
559
+ <Dialog open={pending !== null} onOpenChange={onOpenChange}>
560
+ <DialogContent className="max-w-xl">
561
+ <DialogHeader>
562
+ <DialogTitle>Confirm local command</DialogTitle>
563
+ <DialogDescription>
564
+ Review the exact allowlisted action Agent Inspector will run on this machine.
565
+ </DialogDescription>
566
+ </DialogHeader>
567
+ {plan !== null && (
568
+ <div className="space-y-3">
569
+ <div className="grid gap-2 text-xs sm:grid-cols-2">
570
+ <div className="rounded-md border bg-muted/30 p-3">
571
+ <div className="text-muted-foreground">Source</div>
572
+ <div className="mt-1 break-words font-medium">{plan.source}</div>
573
+ </div>
574
+ <div className="rounded-md border bg-muted/30 p-3">
575
+ <div className="text-muted-foreground">Timeout</div>
576
+ <div className="mt-1 font-medium">{String(plan.timeoutMs / 1000)} seconds</div>
577
+ </div>
578
+ <div className="rounded-md border bg-muted/30 p-3 sm:col-span-2">
579
+ <div className="text-muted-foreground">Affected scope</div>
580
+ <div className="mt-1 font-medium">{plan.scope}</div>
581
+ </div>
582
+ </div>
583
+ <div>
584
+ <div className="mb-1 text-xs font-medium">Resolved command</div>
585
+ <pre className="max-h-32 overflow-auto whitespace-pre-wrap break-all rounded-md bg-black/80 p-3 font-mono text-xs leading-5 text-white/80">
586
+ {plan.command}
587
+ </pre>
588
+ </div>
589
+ <p className="text-xs text-muted-foreground">
590
+ Confirmation phrase: <code>{pending?.confirmation.phrase}</code>. The one-time
591
+ confirmation expires at{" "}
592
+ {new Date(pending?.confirmation.expiresAt ?? "").toLocaleTimeString()}.
593
+ </p>
594
+ </div>
595
+ )}
596
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
597
+ <Button
598
+ type="button"
599
+ variant="outline"
600
+ disabled={busy}
601
+ onClick={() => onOpenChange(false)}
602
+ >
603
+ Cancel
604
+ </Button>
605
+ <Button
606
+ type="button"
607
+ variant="secondary"
608
+ disabled={busy || plan === null}
609
+ onClick={onConfirm}
610
+ >
611
+ {busy ? (
612
+ <Loader2 className="size-3.5 animate-spin" />
613
+ ) : (
614
+ <Terminal className="size-3.5" />
615
+ )}
616
+ {busy ? "Starting" : "Confirm & Run"}
617
+ </Button>
618
+ </div>
619
+ </DialogContent>
620
+ </Dialog>
621
+ );
622
+ }
623
+
502
624
  export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogProps): JSX.Element {
503
625
  const [open, setOpen] = useState(false);
504
626
  const [copied, setCopied] = useState(false);
@@ -506,6 +628,8 @@ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogPro
506
628
  const [copiedRecipeId, setCopiedRecipeId] = useState<string | null>(null);
507
629
  const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
508
630
  const [taskError, setTaskError] = useState<string | null>(null);
631
+ const [pendingExecution, setPendingExecution] = useState<PendingEcosystemExecution | null>(null);
632
+ const [confirmationBusy, setConfirmationBusy] = useState(false);
509
633
  const response = useSWR("/api/ecosystem/packages", fetchEcosystemPackages, {
510
634
  revalidateOnFocus: false,
511
635
  revalidateIfStale: false,
@@ -560,14 +684,12 @@ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogPro
560
684
  };
561
685
  const runRecipe = (recipe: EcosystemRecipe): void => {
562
686
  setTaskError(null);
563
- void startEcosystemRecipe(recipe.id)
564
- .then((task) => {
565
- setActiveTaskId(task.id);
566
- void activeTaskResponse.mutate({ task }, { revalidate: true });
567
- void tasksResponse.mutate();
687
+ void prepareEcosystemRecipe(recipe.id)
688
+ .then((pending) => {
689
+ setPendingExecution(pending);
568
690
  })
569
691
  .catch((error: unknown) => {
570
- setTaskError(error instanceof Error ? error.message : "Failed to start ecosystem recipe");
692
+ setTaskError(error instanceof Error ? error.message : "Failed to prepare ecosystem recipe");
571
693
  });
572
694
  };
573
695
 
@@ -582,14 +704,31 @@ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogPro
582
704
 
583
705
  const handleStartTask = (packageId: string, action: EcosystemTaskAction): void => {
584
706
  setTaskError(null);
585
- void startEcosystemTask(packageId, action)
707
+ void prepareEcosystemTask(packageId, action)
708
+ .then((pending) => {
709
+ setPendingExecution(pending);
710
+ })
711
+ .catch((error: unknown) => {
712
+ setTaskError(error instanceof Error ? error.message : "Failed to prepare ecosystem task");
713
+ });
714
+ };
715
+
716
+ const confirmPendingExecution = (): void => {
717
+ if (pendingExecution === null) return;
718
+ setTaskError(null);
719
+ setConfirmationBusy(true);
720
+ void confirmEcosystemExecution(pendingExecution)
586
721
  .then((task) => {
587
722
  setActiveTaskId(task.id);
723
+ setPendingExecution(null);
588
724
  void activeTaskResponse.mutate({ task }, { revalidate: true });
589
725
  void tasksResponse.mutate();
590
726
  })
591
727
  .catch((error: unknown) => {
592
728
  setTaskError(error instanceof Error ? error.message : "Failed to start ecosystem task");
729
+ })
730
+ .finally(() => {
731
+ setConfirmationBusy(false);
593
732
  });
594
733
  };
595
734
 
@@ -788,6 +927,14 @@ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogPro
788
927
  <TaskConsole task={activeTask} />
789
928
  </div>
790
929
  </DialogContent>
930
+ <ExecutionConfirmationDialog
931
+ pending={pendingExecution}
932
+ busy={confirmationBusy}
933
+ onOpenChange={(confirmationOpen) => {
934
+ if (!confirmationOpen && !confirmationBusy) setPendingExecution(null);
935
+ }}
936
+ onConfirm={confirmPendingExecution}
937
+ />
791
938
  </Dialog>
792
939
  );
793
940
  }