@tonyclaw/agent-inspector 3.0.29 → 3.0.31

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-DB2KggLE.js +1 -0
  4. package/.output/public/assets/InspectorPet-CfwDw6kD.js +4108 -0
  5. package/.output/public/assets/ProxyViewerContainer-PcrO96_F.js +126 -0
  6. package/.output/public/assets/{ReplayDialog-CctnITbt.js → ReplayDialog-3Pl7EKjp.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-BrN0RBbL.js → RequestAnatomy-BcPjcHLX.js} +1 -1
  8. package/.output/public/assets/ResponseView-DOx51wV3.js +2 -0
  9. package/.output/public/assets/StreamingChunkSequence-LkYJWM1A.js +1 -0
  10. package/.output/public/assets/_sessionId-BSg32vC6.js +1 -0
  11. package/.output/public/assets/{_sessionId-DKM91udC.js → _sessionId-C0CA68QV.js} +1 -1
  12. package/.output/public/assets/{index-Bq-nqolG.js → index-BpBUqdAB.js} +1 -1
  13. package/.output/public/assets/index-D93dCgAQ.js +1 -0
  14. package/.output/public/assets/{index-DPiESBo2.js → index-FV5sxqx3.js} +6 -6
  15. package/.output/public/assets/index-VUsF4nhf.js +76 -0
  16. package/.output/public/assets/index-hyQHZeQI.css +1 -0
  17. package/.output/public/assets/{json-viewer-CfaDJwx8.js → json-viewer-WctIbAh_.js} +1 -1
  18. package/.output/public/assets/jszip.min-DnSIoTix.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 +80 -80
  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-BHBK3VbF.mjs} +15 -15
  25. package/.output/server/_sessionId-CyuJ6Kfl.mjs +81 -0
  26. package/.output/server/_ssr/{CompareDrawer-R_P1-VNr.mjs → CompareDrawer-27amlv_a.mjs} +138 -103
  27. package/.output/server/_ssr/InspectorPet-CQnGXfLs.mjs +925 -0
  28. package/.output/server/_ssr/{ProxyViewerContainer-DUgxRNbj.mjs → ProxyViewerContainer-DlAl0uF-.mjs} +1569 -1646
  29. package/.output/server/_ssr/{ReplayDialog-De9wKKzS.mjs → ReplayDialog-5m2jtlLa.mjs} +18 -18
  30. package/.output/server/_ssr/{RequestAnatomy-Db4MxUYo.mjs → RequestAnatomy--1WtQNY9.mjs} +58 -58
  31. package/.output/server/_ssr/{ResponseView-8rDXOiNB.mjs → ResponseView-CgbqS8sr.mjs} +60 -71
  32. package/.output/server/_ssr/{StreamingChunkSequence-N9teZHkA.mjs → StreamingChunkSequence-BwpnopWx.mjs} +16 -16
  33. package/.output/server/_ssr/{index-D4DvByuW.mjs → index-CNVi1TzT.mjs} +15 -15
  34. package/.output/server/_ssr/index-DMCjH_4V.mjs +81 -0
  35. package/.output/server/_ssr/index.mjs +2 -2
  36. package/.output/server/_ssr/{json-viewer-BjoHRbjS.mjs → json-viewer-DNGN2uoe.mjs} +58 -58
  37. package/.output/server/_ssr/{router-BYlePDj8.mjs → router-CgsqLnk5.mjs} +16007 -12632
  38. package/.output/server/_tanstack-start-manifest_v-BH5f_kWV.mjs +4 -0
  39. package/.output/server/index.mjs +99 -71
  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 +209 -35
  52. package/src/components/pi-agent/piAgentChatLogic.ts +263 -0
  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/LiveConnectionStatus.tsx +186 -0
  60. package/src/components/proxy-viewer/LogEntry.tsx +283 -275
  61. package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
  62. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
  63. package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
  64. package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
  65. package/src/components/proxy-viewer/accessibility.ts +8 -0
  66. package/src/components/proxy-viewer/lazy.ts +4 -0
  67. package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
  68. package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
  69. package/src/components/proxy-viewer/viewerState.ts +2 -6
  70. package/src/components/ui/select.tsx +1 -1
  71. package/src/components/ui/tabs.tsx +3 -3
  72. package/src/components/ui/transient-toast.tsx +7 -6
  73. package/src/lib/apiClient.ts +17 -2
  74. package/src/lib/ecosystemContract.ts +34 -0
  75. package/src/lib/export-logs.ts +1 -1
  76. package/src/lib/piAgentContract.ts +80 -0
  77. package/src/lib/providerContract.ts +70 -4
  78. package/src/lib/providerImportContract.ts +27 -0
  79. package/src/lib/providerModelMetadata.ts +16 -7
  80. package/src/lib/resourceLimits.ts +152 -0
  81. package/src/lib/safeDiagnostic.ts +38 -0
  82. package/src/lib/useProviders.ts +4 -4
  83. package/src/mcp/server.ts +39 -6
  84. package/src/mcp/toolHandlers.ts +131 -4
  85. package/src/proxy/chunkStorage.ts +20 -6
  86. package/src/proxy/config.ts +20 -6
  87. package/src/proxy/dataDir.ts +3 -0
  88. package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
  89. package/src/proxy/ecosystemExecutionRoute.ts +116 -0
  90. package/src/proxy/ecosystemTasks.ts +48 -0
  91. package/src/proxy/evidenceExporter.ts +23 -9
  92. package/src/proxy/groupEvidenceExporter.ts +26 -5
  93. package/src/proxy/groupStore.ts +13 -3
  94. package/src/proxy/handler.ts +103 -20
  95. package/src/proxy/identityProxy.ts +333 -14
  96. package/src/proxy/logFinalizer.ts +73 -6
  97. package/src/proxy/logImportUpload.ts +128 -0
  98. package/src/proxy/logImporter.ts +321 -70
  99. package/src/proxy/logIndex.ts +16 -6
  100. package/src/proxy/logger.ts +294 -41
  101. package/src/proxy/privateDataPath.ts +183 -0
  102. package/src/proxy/providerScanStore.ts +87 -0
  103. package/src/proxy/providerSecretStore.ts +58 -19
  104. package/src/proxy/providers.ts +175 -58
  105. package/src/proxy/rawStreamCapture.ts +66 -5
  106. package/src/proxy/runStore.ts +13 -3
  107. package/src/proxy/runtimeAdmission.ts +52 -0
  108. package/src/proxy/runtimeHealth.ts +206 -0
  109. package/src/proxy/runtimeShutdown.ts +75 -0
  110. package/src/proxy/sessionArchive.ts +15 -2
  111. package/src/proxy/sessionProcess.ts +19 -0
  112. package/src/proxy/sessionRuntime.ts +7 -0
  113. package/src/proxy/shutdownCoordinator.ts +90 -0
  114. package/src/proxy/sqliteLogIndex.ts +18 -2
  115. package/src/proxy/store.ts +18 -2
  116. package/src/routes/__root.tsx +18 -1
  117. package/src/routes/api/alerts.summary.ts +12 -8
  118. package/src/routes/api/alerts.ts +27 -9
  119. package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
  120. package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
  121. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
  122. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
  123. package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
  124. package/src/routes/api/health.ts +9 -2
  125. package/src/routes/api/knowledge.project-context.ts +11 -0
  126. package/src/routes/api/knowledge.search.ts +33 -2
  127. package/src/routes/api/logs.$id.body.ts +16 -2
  128. package/src/routes/api/logs.import.ts +7 -18
  129. package/src/routes/api/logs.stream.ts +147 -69
  130. package/src/routes/api/logs.ts +55 -14
  131. package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
  132. package/src/routes/api/providers.$providerId.ts +37 -6
  133. package/src/routes/api/providers.export.ts +34 -16
  134. package/src/routes/api/providers.import.ts +42 -8
  135. package/src/routes/api/providers.scan.ts +13 -8
  136. package/src/routes/api/providers.ts +36 -5
  137. package/src/routes/api/runs.ts +12 -12
  138. package/src/routes/api/sessions.ts +15 -8
  139. package/src/routes/index.tsx +6 -0
  140. package/src/routes/livez.ts +13 -0
  141. package/src/routes/readyz.ts +18 -0
  142. package/src/routes/session/$sessionId.tsx +6 -0
  143. package/src/services/piAgent.ts +131 -102
  144. package/src/services/piAgentRetrieval.ts +1180 -0
  145. package/styles/globals.css +19 -3
  146. package/.output/public/assets/CompareDrawer-Cb5ccPeX.js +0 -1
  147. package/.output/public/assets/ProxyViewerContainer-COBe5GTT.js +0 -4234
  148. package/.output/public/assets/ResponseView-Cqq87lX_.js +0 -3
  149. package/.output/public/assets/StreamingChunkSequence-CnTa9mCr.js +0 -1
  150. package/.output/public/assets/index-DTLNVcgc.js +0 -76
  151. package/.output/public/assets/index-DliqmwUw.css +0 -1
  152. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  153. package/.output/server/_tanstack-start-manifest_v-DMCA8KK3.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
@@ -0,0 +1,77 @@
1
+ import { type JSX } from "react";
2
+
3
+ import { Button } from "../ui/button";
4
+ import { DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog";
5
+
6
+ export const RAW_EXPORT_DIALOG_CLASS_NAME =
7
+ "w-[calc(100%-1rem)] border-warning/30 bg-popover text-popover-foreground sm:max-w-lg";
8
+
9
+ export const RAW_EXPORT_RISK_ITEMS = [
10
+ {
11
+ label: "Headers",
12
+ detail: "request and response headers, including authorization or cookie values",
13
+ },
14
+ {
15
+ label: "Prompts",
16
+ detail: "system instructions, user prompts, model responses, and conversation history",
17
+ },
18
+ {
19
+ label: "Source content",
20
+ detail: "source code, file contents, tool inputs, and tool output",
21
+ },
22
+ {
23
+ label: "Secrets",
24
+ detail: "API keys, access tokens, credentials, and other sensitive values",
25
+ },
26
+ ] as const;
27
+
28
+ export type RawExportWarningProps = {
29
+ onCancel: () => void;
30
+ onExportRedacted: () => void;
31
+ onConfirmRaw: () => void;
32
+ };
33
+
34
+ export function RawExportWarning({
35
+ onCancel,
36
+ onExportRedacted,
37
+ onConfirmRaw,
38
+ }: RawExportWarningProps): JSX.Element {
39
+ return (
40
+ <>
41
+ <DialogHeader>
42
+ <DialogTitle>Export unredacted logs?</DialogTitle>
43
+ <DialogDescription>
44
+ Raw exports bypass Agent Inspector redaction and may expose sensitive captured data.
45
+ </DialogDescription>
46
+ </DialogHeader>
47
+ <div
48
+ role="alert"
49
+ className="rounded-md border border-warning/35 bg-warning/10 px-3 py-3 text-sm text-foreground"
50
+ >
51
+ <p className="font-medium">The raw archive may include:</p>
52
+ <ul className="mt-2 list-disc space-y-1 pl-5 text-xs leading-relaxed text-muted-foreground">
53
+ {RAW_EXPORT_RISK_ITEMS.map((item) => (
54
+ <li key={item.label}>
55
+ <span className="font-semibold text-foreground">{item.label}:</span> {item.detail}
56
+ </li>
57
+ ))}
58
+ </ul>
59
+ <p className="mt-3 text-xs leading-relaxed text-muted-foreground">
60
+ Only continue if you trust where this file will be stored and everyone it may be shared
61
+ with.
62
+ </p>
63
+ </div>
64
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
65
+ <Button type="button" variant="outline" size="sm" onClick={onCancel}>
66
+ Cancel
67
+ </Button>
68
+ <Button type="button" variant="outline" size="sm" onClick={onExportRedacted}>
69
+ Export redacted instead
70
+ </Button>
71
+ <Button type="button" variant="destructive" size="sm" onClick={onConfirmRaw}>
72
+ Export raw data
73
+ </Button>
74
+ </div>
75
+ </>
76
+ );
77
+ }
@@ -0,0 +1,61 @@
1
+ import { type JSX } from "react";
2
+ import { cn } from "../../lib/utils";
3
+
4
+ export type SessionLoadProgress = {
5
+ label: string;
6
+ elapsedMs: number;
7
+ timeoutMs: number;
8
+ loadedLogs: number;
9
+ loadedPages: number;
10
+ };
11
+
12
+ function formatDurationSeconds(ms: number): string {
13
+ return `${Math.max(0, Math.ceil(ms / 1000)).toString()}s`;
14
+ }
15
+
16
+ export function SessionLoadProgressBar({
17
+ progress,
18
+ }: {
19
+ progress: SessionLoadProgress;
20
+ }): JSX.Element {
21
+ const elapsedRatio = Math.min(1, progress.elapsedMs / progress.timeoutMs);
22
+ const percent = Math.max(4, Math.round(elapsedRatio * 100));
23
+ const remainingMs = Math.max(0, progress.timeoutMs - progress.elapsedMs);
24
+ const nearTimeout = elapsedRatio >= 0.75;
25
+ const details = [
26
+ progress.loadedLogs > 0 ? `${progress.loadedLogs.toString()} logs` : null,
27
+ progress.loadedPages > 0
28
+ ? `${progress.loadedPages.toString()} page${progress.loadedPages === 1 ? "" : "s"}`
29
+ : null,
30
+ `${formatDurationSeconds(remainingMs)} before timeout`,
31
+ ].filter((item): item is string => item !== null);
32
+ const valueText = `${progress.label}: ${details.join(", ")}`;
33
+
34
+ return (
35
+ <div className="min-w-0 space-y-1 px-1 pb-1">
36
+ <div className="flex min-w-0 items-center justify-between gap-3 font-mono text-[10px] text-muted-foreground">
37
+ <span className="min-w-0 truncate">{progress.label}</span>
38
+ <span className={cn("shrink-0 tabular-nums", nearTimeout && "text-amber-300")}>
39
+ {details.join(" / ")}
40
+ </span>
41
+ </div>
42
+ <div
43
+ role="progressbar"
44
+ aria-label={progress.label}
45
+ aria-valuemin={0}
46
+ aria-valuemax={progress.timeoutMs}
47
+ aria-valuenow={Math.min(progress.elapsedMs, progress.timeoutMs)}
48
+ aria-valuetext={valueText}
49
+ className="h-1.5 overflow-hidden rounded-full bg-white/[0.06]"
50
+ >
51
+ <div
52
+ className={cn(
53
+ "h-full rounded-full transition-all duration-300 motion-reduce:transition-none",
54
+ nearTimeout ? "bg-amber-300/80" : "bg-cyan-300/70",
55
+ )}
56
+ style={{ width: `${percent.toString()}%` }}
57
+ />
58
+ </div>
59
+ </div>
60
+ );
61
+ }
@@ -11,6 +11,8 @@ export type ThreadConnectorProps = {
11
11
  isTurnStart: boolean;
12
12
  /** True when this entry is the turn's only request. */
13
13
  isOnlyEntry?: boolean;
14
+ /** True when this is the latest request in a turn that has not reached a real boundary. */
15
+ isOpenTurnTail?: boolean;
14
16
  /** Seed for crab variant selection (0-11). */
15
17
  crabIndex?: number;
16
18
  /** When true the crab is clickable (collapse / expand a turn). */
@@ -42,6 +44,7 @@ export function ThreadConnector({
42
44
  isFirst,
43
45
  isTurnStart,
44
46
  isOnlyEntry = false,
47
+ isOpenTurnTail = false,
45
48
  crabIndex = 0,
46
49
  collapsible = false,
47
50
  onToggle,
@@ -123,6 +126,18 @@ export function ThreadConnector({
123
126
  )}
124
127
  />
125
128
  </span>
129
+ ) : isOpenTurnTail ? (
130
+ <span
131
+ title={isPending ? "Current end of open turn (processing)" : "Current end of open turn"}
132
+ >
133
+ <Crab
134
+ className={cn(
135
+ "size-3.5 text-cyan-300/85",
136
+ isRunning ? "animate-crab-crawl" : "animate-crab-appear",
137
+ "drop-shadow-[0_0_4px_rgba(103,232,249,0.4)]",
138
+ )}
139
+ />
140
+ </span>
126
141
  ) : isRunning ? (
127
142
  <span title="Processing">
128
143
  <Crab className={cn("size-3.5 text-amber-300/80", "animate-crab-crawl")} />
@@ -134,7 +149,7 @@ export function ThreadConnector({
134
149
  )}
135
150
 
136
151
  <div className="flex-1 flex justify-center min-h-0">
137
- {!isBoundary && (
152
+ {!isBoundary && !isOpenTurnTail && (
138
153
  <div
139
154
  className={cn(
140
155
  "w-0.5 h-full",
@@ -69,7 +69,7 @@ export const TurnGroup = memo(function TurnGroup({
69
69
  const isComplete = lastStop !== null ? isTurnBoundary(lastStop) : false;
70
70
  const isPending = entries[lastIdx]?.log.responseStatus === null;
71
71
  const isSingleLog = entries.length === 1;
72
- const collapsible = isTurnCollapsible(entries.length, isComplete, isPending);
72
+ const collapsible = isTurnCollapsible(entries.length);
73
73
  const [collapsed, setCollapsed] = useState(false);
74
74
 
75
75
  // Auto-collapse when the turn finishes (transitions from incomplete to complete)
@@ -233,13 +233,23 @@ export const TurnGroup = memo(function TurnGroup({
233
233
  <div className="flex-1 flex justify-center min-h-0">
234
234
  <div className="w-px bg-muted-foreground/14 h-full" />
235
235
  </div>
236
- {isComplete && (
236
+ {isComplete ? (
237
237
  <CollapsedCrab
238
238
  className={cn(
239
239
  "size-3.5 animate-crab-settle text-amber-400",
240
240
  "drop-shadow-[0_0_4px_rgba(251,191,36,0.45)]",
241
241
  )}
242
242
  />
243
+ ) : (
244
+ <span title="Current end of open turn" aria-label="Current end of open turn">
245
+ <CollapsedCrab
246
+ className={cn(
247
+ "size-3.5 text-cyan-300/85",
248
+ isPending ? "animate-crab-crawl" : "animate-crab-appear",
249
+ "drop-shadow-[0_0_4px_rgba(103,232,249,0.4)]",
250
+ )}
251
+ />
252
+ </span>
243
253
  )}
244
254
  </div>
245
255
 
@@ -343,6 +353,7 @@ export const TurnGroup = memo(function TurnGroup({
343
353
  isFirst={visibleIdx === 0}
344
354
  isTurnStart={isTurnStart}
345
355
  isOnlyEntry={isSingleLog}
356
+ isOpenTurnTail={visibleIdx === lastIdx && !isComplete}
346
357
  crabIndex={log.id % 12}
347
358
  collapsible={collapsible && isTurnStart}
348
359
  onToggle={toggleCollapse}
@@ -0,0 +1,8 @@
1
+ export function preferredScrollBehavior(reducedMotion: boolean): ScrollBehavior {
2
+ return reducedMotion ? "auto" : "smooth";
3
+ }
4
+
5
+ export function browserPrefersReducedMotion(): boolean {
6
+ if (typeof window === "undefined") return false;
7
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8
+ }
@@ -10,6 +10,10 @@ export const LazyCompareDrawer = lazy(() =>
10
10
  import("./CompareDrawer").then((m) => ({ default: m.CompareDrawer })),
11
11
  );
12
12
 
13
+ export const LazyInspectorPet = lazy(() =>
14
+ import("../inspector-pet/InspectorPet").then((m) => ({ default: m.InspectorPet })),
15
+ );
16
+
13
17
  export const LazyReplayDialog = lazy(() =>
14
18
  import("./ReplayDialog").then((m) => ({ default: m.ReplayDialog })),
15
19
  );
@@ -0,0 +1,158 @@
1
+ export type LiveConnectionPhase =
2
+ | "connecting"
3
+ | "live"
4
+ | "reconnecting"
5
+ | "stale"
6
+ | "offline"
7
+ | "error";
8
+
9
+ export type LiveConnectionState = {
10
+ status: LiveConnectionPhase;
11
+ lastSuccessAt: number | null;
12
+ retryCount: number;
13
+ errorMessage: string | null;
14
+ };
15
+
16
+ export type LiveConnectionEvent =
17
+ | { type: "connect-requested" }
18
+ | { type: "message-received"; at: number }
19
+ | { type: "connection-lost"; online: boolean; message: string }
20
+ | { type: "stale-timeout" }
21
+ | { type: "browser-offline" }
22
+ | { type: "stop-retrying" }
23
+ | { type: "recoverable-error"; message: string };
24
+
25
+ export function createInitialLiveConnectionState(): LiveConnectionState {
26
+ return {
27
+ status: "connecting",
28
+ lastSuccessAt: null,
29
+ retryCount: 0,
30
+ errorMessage: null,
31
+ };
32
+ }
33
+
34
+ export function reduceLiveConnectionState(
35
+ state: LiveConnectionState,
36
+ event: LiveConnectionEvent,
37
+ ): LiveConnectionState {
38
+ switch (event.type) {
39
+ case "connect-requested":
40
+ return {
41
+ ...state,
42
+ status:
43
+ state.lastSuccessAt === null
44
+ ? "connecting"
45
+ : state.status === "stale"
46
+ ? "stale"
47
+ : "reconnecting",
48
+ errorMessage: null,
49
+ };
50
+ case "message-received":
51
+ return {
52
+ status: "live",
53
+ lastSuccessAt: event.at,
54
+ retryCount: 0,
55
+ errorMessage: null,
56
+ };
57
+ case "connection-lost": {
58
+ const retryCount = state.retryCount + 1;
59
+ if (!event.online) {
60
+ return {
61
+ ...state,
62
+ status: "offline",
63
+ retryCount,
64
+ errorMessage: "Browser is offline.",
65
+ };
66
+ }
67
+ return {
68
+ ...state,
69
+ status: state.lastSuccessAt === null ? "error" : "reconnecting",
70
+ retryCount,
71
+ errorMessage: event.message,
72
+ };
73
+ }
74
+ case "stale-timeout":
75
+ if (state.lastSuccessAt === null || state.status === "live" || state.status === "offline") {
76
+ return state;
77
+ }
78
+ return { ...state, status: "stale" };
79
+ case "browser-offline":
80
+ return {
81
+ ...state,
82
+ status: "offline",
83
+ errorMessage: "Browser is offline.",
84
+ };
85
+ case "stop-retrying":
86
+ return {
87
+ ...state,
88
+ status: "offline",
89
+ errorMessage: "Automatic live updates are paused.",
90
+ };
91
+ case "recoverable-error":
92
+ return {
93
+ ...state,
94
+ status: "error",
95
+ errorMessage: event.message,
96
+ };
97
+ }
98
+ }
99
+
100
+ export type LiveEmptyState =
101
+ | "connecting"
102
+ | "empty"
103
+ | "reconnecting"
104
+ | "stale"
105
+ | "offline"
106
+ | "error";
107
+
108
+ export function resolveLiveEmptyState(state: LiveConnectionState | undefined): LiveEmptyState {
109
+ if (state === undefined || state.status === "live") return "empty";
110
+ return state.status;
111
+ }
112
+
113
+ export type LiveEmptyStateCopy = {
114
+ title: string;
115
+ description: string;
116
+ showConnectionCommands: boolean;
117
+ };
118
+
119
+ export function getLiveEmptyStateCopy(state: LiveEmptyState): LiveEmptyStateCopy {
120
+ switch (state) {
121
+ case "connecting":
122
+ return {
123
+ title: "Connecting to the live stream...",
124
+ description: "Waiting for the first successful initialization before checking for logs.",
125
+ showConnectionCommands: false,
126
+ };
127
+ case "empty":
128
+ return {
129
+ title: "No logs captured yet.",
130
+ description: "Connect an AI coding tool to capture its first request.",
131
+ showConnectionCommands: true,
132
+ };
133
+ case "reconnecting":
134
+ return {
135
+ title: "Reconnecting to live updates...",
136
+ description: "The stream was interrupted. Agent Inspector is retrying automatically.",
137
+ showConnectionCommands: false,
138
+ };
139
+ case "stale":
140
+ return {
141
+ title: "Live data is stale.",
142
+ description: "No logs are hidden; retry the connection to resume live updates.",
143
+ showConnectionCommands: false,
144
+ };
145
+ case "offline":
146
+ return {
147
+ title: "Live updates are offline.",
148
+ description: "Check the browser and Inspector connection, then retry.",
149
+ showConnectionCommands: false,
150
+ };
151
+ case "error":
152
+ return {
153
+ title: "Live updates could not initialize.",
154
+ description: "The error is recoverable. Retry without reloading the page.",
155
+ showConnectionCommands: false,
156
+ };
157
+ }
158
+ }
@@ -1,4 +1,5 @@
1
1
  import type { CapturedLog } from "../../contracts";
2
+ import type { ExportMode } from "../../lib/export-logs";
2
3
  import type { CaptureMode, TimeDisplayFormat } from "../../lib/runtimeConfig";
3
4
  import { extractStopReason, type StopReason } from "../../lib/stopReason";
4
5
  import { formatTimestampRange } from "../../lib/timeDisplay";
@@ -23,6 +24,11 @@ export type ConversationLogs = {
23
24
  logs: CapturedLog[];
24
25
  };
25
26
 
27
+ export const COPYABLE_COMMAND_CONTAINER_CLASS_NAME =
28
+ "flex w-full min-w-0 items-center gap-2 rounded-md border border-border/70 bg-muted/35 px-3 py-2 sm:w-auto sm:max-w-full";
29
+ export const COPYABLE_COMMAND_TEXT_CLASS_NAME =
30
+ "m-0 min-w-0 flex-1 overflow-x-auto font-mono text-xs text-foreground sm:text-sm";
31
+
26
32
  export type SessionSlateStats = {
27
33
  status: SessionSlateStatus;
28
34
  statusLabel: string;
@@ -74,6 +80,10 @@ export function shouldShowRawExport(captureMode: CaptureMode): boolean {
74
80
  return captureMode === "full";
75
81
  }
76
82
 
83
+ export function exportRequiresConfirmation(mode: ExportMode): boolean {
84
+ return mode === "raw";
85
+ }
86
+
77
87
  export function getExportActionVisibility(
78
88
  captureMode: CaptureMode,
79
89
  logCount: number,
@@ -69,12 +69,8 @@ function responseMayContainToolTrace(log: CapturedLog, format: ResolvedLogFormat
69
69
  }
70
70
  }
71
71
 
72
- export function isTurnCollapsible(
73
- entryCount: number,
74
- isComplete: boolean,
75
- isPending: boolean,
76
- ): boolean {
77
- return entryCount > 1 && isComplete && !isPending;
72
+ export function isTurnCollapsible(entryCount: number): boolean {
73
+ return entryCount > 1;
78
74
  }
79
75
 
80
76
  export function buildTurnGroups(logs: CapturedLog[]): TurnGroupData[] {
@@ -62,7 +62,7 @@ function SelectContent({
62
62
  <SelectPrimitive.Content
63
63
  data-slot="select-content"
64
64
  className={cn(
65
- "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
65
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 motion-reduce:data-[state=open]:animate-none motion-reduce:data-[state=closed]:animate-none relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
66
66
  position === "popper" &&
67
67
  "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
68
68
  className,
@@ -61,9 +61,9 @@ function TabsTrigger({
61
61
  <TabsPrimitive.Trigger
62
62
  data-slot="tabs-trigger"
63
63
  className={cn(
64
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
65
- "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
66
- "data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
64
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-muted-foreground hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
65
+ "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
66
+ "data-[state=active]:border-border data-[state=active]:bg-background data-[state=active]:text-foreground",
67
67
  "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
68
68
  className,
69
69
  )}
@@ -80,19 +80,20 @@ export function TransientToast({
80
80
  <div
81
81
  key={notice.id}
82
82
  className={cn(
83
- "pointer-events-auto flex items-start gap-2 rounded-md border px-3 py-2 text-sm shadow-[0_18px_50px_rgba(0,0,0,0.45)] backdrop-blur",
84
- isError
85
- ? "border-red-400/30 bg-[#16090b]/95 text-red-100"
86
- : "border-cyan-300/25 bg-[#071318]/95 text-cyan-100",
83
+ "pointer-events-auto flex items-start gap-2 rounded-md border bg-popover/95 px-3 py-2 text-sm text-popover-foreground shadow-[0_18px_50px_color-mix(in_oklch,var(--foreground)_16%,transparent)] backdrop-blur",
84
+ isError ? "border-status-danger/35" : "border-status-success/35",
87
85
  )}
88
86
  >
89
87
  <Icon
90
- className={cn("mt-0.5 size-4 shrink-0", isError ? "text-red-300" : "text-cyan-200")}
88
+ className={cn(
89
+ "mt-0.5 size-4 shrink-0",
90
+ isError ? "text-status-danger" : "text-status-success",
91
+ )}
91
92
  />
92
93
  <div className="min-w-0 flex-1 break-words leading-5">{notice.message}</div>
93
94
  <button
94
95
  type="button"
95
- className="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-current/70 transition-colors hover:bg-white/10 hover:text-current"
96
+ className="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-current/70 transition-colors hover:bg-white/10 hover:text-current focus-visible:ring-1 focus-visible:ring-current focus-visible:outline-none"
96
97
  aria-label="Dismiss notification"
97
98
  onClick={onDismiss}
98
99
  >
@@ -5,6 +5,18 @@ const ApiErrorSchema = z.object({
5
5
  error: z.string(),
6
6
  });
7
7
 
8
+ const MUTATION_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
9
+
10
+ export function withBrowserMutationProof(init?: RequestInit): RequestInit | undefined {
11
+ const method = init?.method?.toUpperCase();
12
+ if (method === undefined || !MUTATION_METHODS.has(method)) return init;
13
+ const headers = new Headers(init?.headers);
14
+ if (!headers.has("x-agent-inspector-csrf")) {
15
+ headers.set("x-agent-inspector-csrf", "1");
16
+ }
17
+ return { ...init, headers };
18
+ }
19
+
8
20
  /**
9
21
  * Parse and validate a JSON response at the browser/server trust boundary.
10
22
  *
@@ -40,7 +52,7 @@ export async function fetchJson<T>(
40
52
  init?: RequestInit,
41
53
  errorFallback?: (response: Response) => string,
42
54
  ): Promise<T> {
43
- const response = await fetch(input, init);
55
+ const response = await fetch(input, withBrowserMutationProof(init));
44
56
  if (!response.ok) {
45
57
  const fallback = errorFallback?.(response) ?? `Request failed with status ${response.status}`;
46
58
  throw new Error(await readApiError(response, fallback));
@@ -88,7 +100,10 @@ export async function fetchWithTimeout(
88
100
  }
89
101
 
90
102
  try {
91
- return await fetch(input, { ...init, signal: controller.signal });
103
+ return await fetch(input, {
104
+ ...withBrowserMutationProof(init),
105
+ signal: controller.signal,
106
+ });
92
107
  } catch (error) {
93
108
  if (timedOut && error instanceof DOMException && error.name === "AbortError") {
94
109
  throw new ApiTimeoutError(timeoutMs);
@@ -61,6 +61,38 @@ export const EcosystemTaskActionSchema = z.enum([
61
61
  "recipe",
62
62
  ]);
63
63
 
64
+ export const ECOSYSTEM_EXECUTION_CONFIRMATION_PHRASE = "RUN_LOCAL_COMMAND";
65
+
66
+ export const EcosystemExecutionPlanSchema = z.object({
67
+ action: EcosystemTaskActionSchema,
68
+ targetId: z.string(),
69
+ targetName: z.string(),
70
+ command: z.string(),
71
+ source: z.string(),
72
+ timeoutMs: z.number().int().positive(),
73
+ scope: z.string(),
74
+ });
75
+
76
+ export const EcosystemExecutionConfirmationSchema = z.object({
77
+ nonce: z.string().uuid(),
78
+ phrase: z.literal(ECOSYSTEM_EXECUTION_CONFIRMATION_PHRASE),
79
+ expiresAt: z.string().datetime(),
80
+ plan: EcosystemExecutionPlanSchema,
81
+ });
82
+
83
+ export const EcosystemExecutionConfirmationResponseSchema = z.object({
84
+ confirmation: EcosystemExecutionConfirmationSchema,
85
+ });
86
+
87
+ export const EcosystemExecutionRequestSchema = z.discriminatedUnion("intent", [
88
+ z.object({ intent: z.literal("prepare") }),
89
+ z.object({
90
+ intent: z.literal("confirm"),
91
+ nonce: z.string().uuid(),
92
+ confirmation: z.literal(ECOSYSTEM_EXECUTION_CONFIRMATION_PHRASE),
93
+ }),
94
+ ]);
95
+
64
96
  export const EcosystemTaskStatusSchema = z.enum(["queued", "running", "succeeded", "failed"]);
65
97
 
66
98
  export const EcosystemTaskSchema = z.object({
@@ -92,5 +124,7 @@ export type EcosystemPackage = z.infer<typeof EcosystemPackageSchema>;
92
124
  export type EcosystemRunnerPreset = z.infer<typeof EcosystemRunnerPresetSchema>;
93
125
  export type EcosystemRecipe = z.infer<typeof EcosystemRecipeSchema>;
94
126
  export type EcosystemTaskAction = z.infer<typeof EcosystemTaskActionSchema>;
127
+ export type EcosystemExecutionPlan = z.infer<typeof EcosystemExecutionPlanSchema>;
128
+ export type EcosystemExecutionConfirmation = z.infer<typeof EcosystemExecutionConfirmationSchema>;
95
129
  export type EcosystemTaskStatus = z.infer<typeof EcosystemTaskStatusSchema>;
96
130
  export type EcosystemTask = z.infer<typeof EcosystemTaskSchema>;
@@ -1,4 +1,3 @@
1
- import JSZip from "jszip";
2
1
  import type { CapturedLog } from "../contracts";
3
2
  import { fetchWithTimeout } from "./apiClient";
4
3
  import packageJson from "../../package.json";
@@ -171,6 +170,7 @@ export async function exportLogsAsZip(
171
170
  const limitMessage = exportLimitMessage(logs);
172
171
  if (limitMessage !== null) return { ok: false, message: limitMessage };
173
172
 
173
+ const { default: JSZip } = await import("jszip");
174
174
  const zip = new JSZip();
175
175
  const suffix = mode === "redacted" ? "redacted" : "raw";
176
176
  const streamingLogs = logs.filter((log) => log.streaming).slice(0, MAX_STREAMING_CHUNK_EXPORTS);