@tonyclaw/agent-inspector 3.0.8 → 3.0.10

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 (45) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Bth_Ywyt.js → CompareDrawer-BxLP7jmX.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-D_2ez29e.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-C2YYAuTe.js → ReplayDialog-Btm-wONI.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-8k9dAX2j.js → RequestAnatomy-C9eZJN9E.js} +1 -1
  6. package/.output/public/assets/{ResponseView-DEL6JcXm.js → ResponseView-9R2MW1kp.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-DngmoR9j.js → StreamingChunkSequence-zxqGpFfv.js} +1 -1
  8. package/.output/public/assets/{_sessionId-4vk9RMZ5.js → _sessionId-C0tPTQlC.js} +1 -1
  9. package/.output/public/assets/agent-inspector-58K1_MsC.ico +0 -0
  10. package/.output/public/assets/index-BIgsQvaF.css +1 -0
  11. package/.output/public/assets/index-DgVWrzsQ.js +1 -0
  12. package/.output/public/assets/{index-DINLvnm9.js → index-WiDkyjq8.js} +1 -1
  13. package/.output/public/assets/{json-viewer-CH5dvS2Y.js → json-viewer-BAmkfRDE.js} +1 -1
  14. package/.output/public/assets/{main-GWkZn4xy.js → main-2feHoEI6.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +2 -2
  16. package/.output/server/_libs/radix-ui__react-dialog.mjs +2 -2
  17. package/.output/server/{_sessionId-EgsjdkN7.mjs → _sessionId-BkoyescL.mjs} +2 -2
  18. package/.output/server/_ssr/{CompareDrawer-CaM8FYuD.mjs → CompareDrawer-irfFdkJ8.mjs} +3 -3
  19. package/.output/server/_ssr/{ProxyViewerContainer-EJIogcGd.mjs → ProxyViewerContainer-CcJr9STz.mjs} +574 -324
  20. package/.output/server/_ssr/{ReplayDialog-BPpJ2-q8.mjs → ReplayDialog-CJnmXQGT.mjs} +3 -3
  21. package/.output/server/_ssr/{RequestAnatomy-DeQAWnZx.mjs → RequestAnatomy-CupP0Wdb.mjs} +2 -2
  22. package/.output/server/_ssr/{ResponseView-Dshm8iQ7.mjs → ResponseView-Bp-0t5cd.mjs} +2 -2
  23. package/.output/server/_ssr/{StreamingChunkSequence-32yIU2hn.mjs → StreamingChunkSequence-BXSn4kMH.mjs} +2 -2
  24. package/.output/server/_ssr/{index-XRcx9blU.mjs → index-KVltJBV8.mjs} +2 -2
  25. package/.output/server/_ssr/index.mjs +2 -2
  26. package/.output/server/_ssr/{json-viewer-C0aX5rqC.mjs → json-viewer-EYQ5eG2Q.mjs} +2 -2
  27. package/.output/server/_ssr/{router-BI-Mvxz9.mjs → router-_cAQddXC.mjs} +39 -36
  28. package/.output/server/{_tanstack-start-manifest_v-jKScklL_.mjs → _tanstack-start-manifest_v-BfiF5iov.mjs} +1 -1
  29. package/.output/server/index.mjs +72 -65
  30. package/package.json +1 -1
  31. package/src/components/ProxyViewer.tsx +194 -29
  32. package/src/components/ProxyViewerContainer.tsx +145 -3
  33. package/src/components/clients/ClientLogo.tsx +16 -1
  34. package/src/components/providers/ProviderCard.tsx +83 -91
  35. package/src/components/providers/ProvidersPanel.tsx +7 -7
  36. package/src/components/providers/SettingsDialog.tsx +17 -22
  37. package/src/components/proxy-viewer/TurnGroup.tsx +0 -16
  38. package/src/components/proxy-viewer/log-formats/anthropic.ts +5 -5
  39. package/src/components/proxy-viewer/log-formats/openai.ts +7 -10
  40. package/src/components/proxy-viewer/requestTools.ts +5 -2
  41. package/src/routes/api/logs.ts +14 -5
  42. package/src/routes/api/providers.$providerId.test.log.ts +28 -24
  43. package/.output/public/assets/ProxyViewerContainer-C73EU9Jq.js +0 -106
  44. package/.output/public/assets/index-DBEB1rHe.css +0 -1
  45. package/.output/public/assets/index-LPIGiUMP.js +0 -1
@@ -4,8 +4,15 @@ import ClaudeCodeLogoSvg from "../../assets/IDE/claudecode-color.svg";
4
4
  import CodexLogoSvg from "../../assets/IDE/codex-color.svg";
5
5
  import OpenCodeLogoSvg from "../../assets/IDE/opencode.svg";
6
6
  import MiMoCodeLogoSvg from "../../assets/IDE/xiaomimimo.svg";
7
+ import AgentInspectorLogo from "../../assets/agent-inspector.ico";
7
8
 
8
- export type ClientApp = "codex" | "opencode" | "mimo-code" | "claude-code" | "unknown";
9
+ export type ClientApp =
10
+ | "agent-inspector"
11
+ | "codex"
12
+ | "opencode"
13
+ | "mimo-code"
14
+ | "claude-code"
15
+ | "unknown";
9
16
 
10
17
  export type ClientAppDetectionInput = {
11
18
  userAgent?: string | null;
@@ -14,6 +21,7 @@ export type ClientAppDetectionInput = {
14
21
  };
15
22
 
16
23
  const CLIENT_LABELS: Record<Exclude<ClientApp, "unknown">, string> = {
24
+ "agent-inspector": "Inspector",
17
25
  codex: "Codex",
18
26
  opencode: "OpenCode",
19
27
  "mimo-code": "MiMo Code",
@@ -38,6 +46,9 @@ export function detectClientApp(input: ClientAppDetectionInput): ClientApp {
38
46
  normalize(input.clientProjectFolder),
39
47
  ].join(" ");
40
48
 
49
+ if (matchesAny(haystack, ["agent-inspector", "agent inspector", "provider-test"])) {
50
+ return "agent-inspector";
51
+ }
41
52
  if (matchesAny(haystack, ["mimocode", "mimo-code", "mimo code"])) return "mimo-code";
42
53
  if (matchesAny(haystack, ["opencode", "open-code", "open code"])) return "opencode";
43
54
  if (matchesAny(haystack, ["claude-code", "claude code", "claudecode"])) return "claude-code";
@@ -50,6 +61,7 @@ export function detectClientApp(input: ClientAppDetectionInput): ClientApp {
50
61
 
51
62
  export function clientAppLabel(client: ClientApp): string | null {
52
63
  switch (client) {
64
+ case "agent-inspector":
53
65
  case "codex":
54
66
  case "opencode":
55
67
  case "mimo-code":
@@ -61,6 +73,7 @@ export function clientAppLabel(client: ClientApp): string | null {
61
73
  }
62
74
 
63
75
  const CLIENT_LOGOS: Record<Exclude<ClientApp, "unknown">, string> = {
76
+ "agent-inspector": AgentInspectorLogo,
64
77
  codex: CodexLogoSvg,
65
78
  opencode: OpenCodeLogoSvg,
66
79
  "mimo-code": MiMoCodeLogoSvg,
@@ -68,6 +81,7 @@ const CLIENT_LOGOS: Record<Exclude<ClientApp, "unknown">, string> = {
68
81
  };
69
82
 
70
83
  const CLIENT_LOGO_FRAMES: Record<Exclude<ClientApp, "unknown">, string> = {
84
+ "agent-inspector": "border-orange-200/25 bg-orange-300/[0.12]",
71
85
  codex: "border-cyan-200/20 bg-cyan-300/[0.08]",
72
86
  opencode: "border-sky-200/25 bg-sky-300/[0.12]",
73
87
  "mimo-code": "border-orange-200/25 bg-orange-400/[0.12]",
@@ -75,6 +89,7 @@ const CLIENT_LOGO_FRAMES: Record<Exclude<ClientApp, "unknown">, string> = {
75
89
  };
76
90
 
77
91
  const CLIENT_LOGO_IMAGES: Record<Exclude<ClientApp, "unknown">, string> = {
92
+ "agent-inspector": "p-0.5 drop-shadow-[0_0_8px_rgba(251,146,60,0.35)]",
78
93
  codex: "p-0.5",
79
94
  opencode: "p-1 drop-shadow-[0_0_8px_rgba(125,211,252,0.35)]",
80
95
  "mimo-code": "p-0.5 drop-shadow-[0_0_8px_rgba(255,105,0,0.35)]",
@@ -31,13 +31,6 @@ import {
31
31
  providerHasContextMetadata,
32
32
  } from "../../lib/providerModelMetadata";
33
33
  import { formatContextWindowTokens } from "../../lib/utils";
34
- import {
35
- PATH_V1_CHAT_COMPLETIONS,
36
- PATH_V1_MESSAGES,
37
- PATH_V1_RESPONSES,
38
- previewUpstreamUrl,
39
- type UpstreamUrlDiagnostic,
40
- } from "../../lib/upstreamUrl";
41
34
  import type {
42
35
  ProviderFormatTestResults,
43
36
  ProviderTestErrorType as ErrorType,
@@ -225,51 +218,16 @@ function ProviderFormatTestStatus({
225
218
  );
226
219
  }
227
220
 
228
- function EndpointDiagnosticLine({
229
- diagnostic,
230
- }: {
231
- diagnostic: UpstreamUrlDiagnostic;
232
- }): JSX.Element {
233
- return (
234
- <div className={diagnostic.severity === "warning" ? "text-amber-200" : "text-muted-foreground"}>
235
- {diagnostic.message}
236
- </div>
237
- );
238
- }
239
-
240
- function ProviderEndpointSummary({
221
+ function ProviderFormatStatusBlock({
241
222
  label,
242
- baseUrl,
243
- endpoint,
244
- providerName,
245
223
  children,
246
224
  }: {
247
225
  label: string;
248
- baseUrl: string;
249
- endpoint: string;
250
- providerName: string;
251
226
  children?: ReactNode;
252
227
  }): JSX.Element {
253
- const preview = previewUpstreamUrl(baseUrl, endpoint, { providerName });
254
-
255
228
  return (
256
- <div className="flex min-w-0 flex-col gap-2 border-t border-border/60 pt-2 first:border-t-0 first:pt-0">
257
- <div className="grid min-w-0 gap-1 text-xs sm:grid-cols-[104px_minmax(0,1fr)]">
258
- <span className="font-medium text-muted-foreground">{label}</span>
259
- <code className="min-w-0 break-all font-mono text-foreground/80">{baseUrl}</code>
260
- <span className="text-[10px] uppercase text-muted-foreground/80">Final</span>
261
- <code className="min-w-0 break-all font-mono text-foreground">{preview.finalUrl}</code>
262
- </div>
263
- {preview.diagnostics.length > 0 && (
264
- <div className="space-y-1 text-xs">
265
- {preview.diagnostics.map((diagnostic) => (
266
- <EndpointDiagnosticLine
267
- key={`${diagnostic.severity}:${diagnostic.message}`}
268
- diagnostic={diagnostic}
269
- />
270
- ))}
271
- </div>
272
- )}
229
+ <div className="min-w-0 space-y-2 rounded-md border border-border/60 bg-muted/15 p-2">
230
+ <div className="text-xs font-medium text-foreground/80">{label}</div>
273
231
  {children}
274
232
  </div>
275
233
  );
@@ -293,9 +251,38 @@ function providerTestTargets(provider: ProviderConfig): string[] {
293
251
  return targets;
294
252
  }
295
253
 
296
- function ProviderTestCoverage({ provider }: { provider: ProviderConfig }): JSX.Element {
297
- const targets = providerTestTargets(provider);
298
- if (targets.length === 0) {
254
+ type CapabilityChipTone = "active" | "quiet" | "response";
255
+
256
+ function CapabilityChip({
257
+ label,
258
+ enabled,
259
+ tone,
260
+ }: {
261
+ label: string;
262
+ enabled: boolean;
263
+ tone?: CapabilityChipTone;
264
+ }): JSX.Element {
265
+ const resolvedTone: CapabilityChipTone = tone ?? (enabled ? "active" : "quiet");
266
+ const className =
267
+ resolvedTone === "response" && enabled
268
+ ? "border-sky-300/30 bg-sky-300/[0.09] text-sky-100"
269
+ : enabled
270
+ ? "border-border/70 bg-muted/25 text-foreground/80"
271
+ : "border-border/50 bg-transparent text-muted-foreground/70";
272
+ return (
273
+ <span className={`inline-flex items-center gap-1 rounded border px-1.5 py-0.5 ${className}`}>
274
+ {enabled ? <CheckCircle className="size-3" /> : <Minus className="size-3" />}
275
+ {label}
276
+ </span>
277
+ );
278
+ }
279
+
280
+ function ProviderCapabilitySummary({ provider }: { provider: ProviderConfig }): JSX.Element {
281
+ const hasAnthropic = providerHasEndpoint(provider.anthropicBaseUrl);
282
+ const hasChat = providerHasEndpoint(provider.openaiBaseUrl);
283
+ const hasResponses = providerHasEndpoint(provider.openaiResponsesBaseUrl);
284
+
285
+ if (!hasAnthropic && !hasChat && !hasResponses) {
299
286
  return (
300
287
  <div className="text-xs text-muted-foreground">
301
288
  Test requires at least one configured endpoint.
@@ -305,16 +292,15 @@ function ProviderTestCoverage({ provider }: { provider: ProviderConfig }): JSX.E
305
292
 
306
293
  return (
307
294
  <div className="flex min-w-0 flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
308
- <span>Test covers</span>
309
- {targets.map((target) => (
310
- <span
311
- key={target}
312
- className="rounded border border-border/70 bg-muted/25 px-1.5 py-0.5 text-foreground/80"
313
- >
314
- {target}
315
- </span>
316
- ))}
317
- <span>non-stream + stream</span>
295
+ <span>Capabilities</span>
296
+ {hasAnthropic && <CapabilityChip label="Anthropic" enabled={true} />}
297
+ {hasChat && <CapabilityChip label="OpenAI Chat" enabled={true} />}
298
+ <CapabilityChip
299
+ label={hasResponses ? "OpenAI Responses" : "No OpenAI Responses"}
300
+ enabled={hasResponses}
301
+ tone="response"
302
+ />
303
+ <span>tested non-stream + stream</span>
318
304
  </div>
319
305
  );
320
306
  }
@@ -762,7 +748,7 @@ export function ProviderCard({
762
748
  </div>
763
749
 
764
750
  <div className="flex flex-col gap-2 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
765
- <ProviderTestCoverage provider={provider} />
751
+ <ProviderCapabilitySummary provider={provider} />
766
752
  <div className="flex flex-wrap items-center gap-2">
767
753
  <span>Key configured</span>
768
754
  <span>{provider.authHeader === "x-api-key" ? "x-api-key" : "Bearer"}</span>
@@ -838,43 +824,49 @@ export function ProviderCard({
838
824
  </div>
839
825
  )}
840
826
 
841
- {providerHasEndpoint(provider.anthropicBaseUrl) && (
842
- <ProviderEndpointSummary
843
- label="Anthropic"
844
- baseUrl={provider.anthropicBaseUrl}
845
- endpoint={PATH_V1_MESSAGES}
846
- providerName={provider.name}
847
- >
848
- {testResults !== undefined && (
849
- <ProviderFormatTestStatus results={testResults.anthropic} />
827
+ {(providerHasEndpoint(provider.anthropicBaseUrl) ||
828
+ providerHasEndpoint(provider.openaiBaseUrl) ||
829
+ providerHasEndpoint(provider.openaiResponsesBaseUrl)) && (
830
+ <div className="grid gap-2 lg:grid-cols-3">
831
+ {providerHasEndpoint(provider.anthropicBaseUrl) && (
832
+ <ProviderFormatStatusBlock label="Anthropic">
833
+ {testResults !== undefined && (
834
+ <ProviderFormatTestStatus results={testResults.anthropic} />
835
+ )}
836
+ {testResults === undefined && (
837
+ <div className="text-xs text-muted-foreground">Not tested yet</div>
838
+ )}
839
+ </ProviderFormatStatusBlock>
850
840
  )}
851
- </ProviderEndpointSummary>
852
- )}
853
841
 
854
- {providerHasEndpoint(provider.openaiBaseUrl) && (
855
- <ProviderEndpointSummary
856
- label="Chat"
857
- baseUrl={provider.openaiBaseUrl}
858
- endpoint={PATH_V1_CHAT_COMPLETIONS}
859
- providerName={provider.name}
860
- >
861
- {testResults !== undefined && (
862
- <ProviderFormatTestStatus results={testResults.openaiChat} />
842
+ {providerHasEndpoint(provider.openaiBaseUrl) && (
843
+ <ProviderFormatStatusBlock label="OpenAI Chat">
844
+ {testResults !== undefined && (
845
+ <ProviderFormatTestStatus results={testResults.openaiChat} />
846
+ )}
847
+ {testResults === undefined && (
848
+ <div className="text-xs text-muted-foreground">Not tested yet</div>
849
+ )}
850
+ </ProviderFormatStatusBlock>
863
851
  )}
864
- </ProviderEndpointSummary>
865
- )}
866
852
 
867
- {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
868
- <ProviderEndpointSummary
869
- label="Responses"
870
- baseUrl={provider.openaiResponsesBaseUrl}
871
- endpoint={PATH_V1_RESPONSES}
872
- providerName={provider.name}
873
- >
874
- {testResults !== undefined && (
875
- <ProviderFormatTestStatus results={testResults.openaiResponses} />
853
+ {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
854
+ <ProviderFormatStatusBlock label="OpenAI Responses">
855
+ {testResults !== undefined && (
856
+ <ProviderFormatTestStatus results={testResults.openaiResponses} />
857
+ )}
858
+ {testResults === undefined && (
859
+ <div className="text-xs text-muted-foreground">Not tested yet</div>
860
+ )}
861
+ </ProviderFormatStatusBlock>
876
862
  )}
877
- </ProviderEndpointSummary>
863
+
864
+ {!providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
865
+ <div className="min-w-0 rounded-md border border-border/50 bg-transparent p-2 text-xs text-muted-foreground/75">
866
+ OpenAI Responses not configured
867
+ </div>
868
+ )}
869
+ </div>
878
870
  )}
879
871
 
880
872
  {testResults?.models !== undefined && Object.keys(testResults.models).length > 0 && (
@@ -535,7 +535,7 @@ export function ProvidersPanel({
535
535
  if (showForm || editingProvider) {
536
536
  return (
537
537
  <div className="space-y-4">
538
- <div className="border border-border bg-background/95 shadow-sm sticky top-0 z-10 flex items-center justify-between rounded-[8px] px-3 py-2">
538
+ <div className="sticky top-0 z-10 flex items-center justify-between gap-3 rounded-[8px] bg-background/95 px-1 py-1 backdrop-blur">
539
539
  <h3 className="text-lg font-medium">
540
540
  {editingProvider ? "Edit Provider" : "Add New Provider"}
541
541
  </h3>
@@ -554,7 +554,7 @@ export function ProvidersPanel({
554
554
 
555
555
  return (
556
556
  <div className="space-y-4">
557
- <div className="border border-border bg-background/95 shadow-sm sticky top-0 z-10 flex items-center justify-between rounded-[8px] px-3 py-2">
557
+ <div className="sticky top-0 z-10 flex items-center justify-between gap-3 rounded-[8px] bg-background/95 px-1 py-1 backdrop-blur">
558
558
  <h3 className="text-lg font-medium">Providers</h3>
559
559
  <div className="flex items-center gap-2">
560
560
  <TooltipProvider>
@@ -622,7 +622,7 @@ export function ProvidersPanel({
622
622
  </div>
623
623
 
624
624
  {configPath !== null && (
625
- <div className="border border-border bg-card shadow-sm flex items-center gap-2 rounded-[8px] px-3 py-2 text-xs text-muted-foreground">
625
+ <div className="flex items-center gap-2 rounded-[8px] bg-muted/15 px-3 py-2 text-xs text-muted-foreground">
626
626
  <span className="shrink-0">Config:</span>
627
627
  <span className="font-mono truncate" title={configPath}>
628
628
  {configPath}
@@ -674,7 +674,7 @@ export function ProvidersPanel({
674
674
  </div>
675
675
  ) : (
676
676
  <div className="space-y-3">
677
- <div className="border border-border bg-muted/40 flex gap-1 rounded-[8px] p-1">
677
+ <div className="inline-flex w-fit gap-1 rounded-[8px] bg-muted/20 p-1">
678
678
  {(["all", "personal", "company"] as const).map((tab) => (
679
679
  <TooltipProvider key={tab}>
680
680
  <Tooltip>
@@ -682,10 +682,10 @@ export function ProvidersPanel({
682
682
  <button
683
683
  type="button"
684
684
  onClick={() => setSourceFilter(tab)}
685
- className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
685
+ className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
686
686
  sourceFilter === tab
687
- ? "border-primary text-primary"
688
- : "border-transparent text-muted-foreground hover:text-foreground"
687
+ ? "bg-background text-foreground shadow-sm"
688
+ : "text-muted-foreground hover:text-foreground"
689
689
  }`}
690
690
  >
691
691
  {tab === "all" ? "All" : tab === "personal" ? "Personal" : "Company"}
@@ -74,13 +74,13 @@ export function SettingsDialog(): JSX.Element {
74
74
  <span className="sr-only">Settings</span>
75
75
  </Button>
76
76
  </DialogTrigger>
77
- <DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
77
+ <DialogContent className="flex max-h-[80vh] max-w-2xl flex-col overflow-hidden">
78
78
  <DialogHeader>
79
79
  <DialogTitle>Settings</DialogTitle>
80
80
  </DialogHeader>
81
81
 
82
82
  <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 overflow-hidden">
83
- <TabsList>
83
+ <TabsList className="w-full justify-start bg-muted/35">
84
84
  <TabsTrigger value="providers">Providers</TabsTrigger>
85
85
  <TabsTrigger value="proxy">Proxy</TabsTrigger>
86
86
  <TabsTrigger value="storage">Storage</TabsTrigger>
@@ -88,7 +88,7 @@ export function SettingsDialog(): JSX.Element {
88
88
  <TabsTrigger value="onboarding">Onboarding</TabsTrigger>
89
89
  </TabsList>
90
90
 
91
- <div className="mt-4 flex-1 overflow-x-hidden overflow-y-auto pr-3">
91
+ <div className="mt-4 flex-1 overflow-x-hidden overflow-y-auto pr-2">
92
92
  <TabsContent value="providers">
93
93
  <ProvidersPanel
94
94
  externalProviders={providers}
@@ -198,7 +198,7 @@ function StorageSettingsTab(): JSX.Element {
198
198
  return (
199
199
  <div className="space-y-4">
200
200
  <div className="flex items-center justify-between gap-3">
201
- <div>
201
+ <div className="min-w-0">
202
202
  <h3 className="text-sm font-semibold">Log storage</h3>
203
203
  <p className="text-xs text-muted-foreground">
204
204
  Inspect local log and streaming chunk footprint before exporting or clearing sessions.
@@ -232,25 +232,25 @@ function StorageSettingsTab(): JSX.Element {
232
232
  copiedId={copiedId}
233
233
  onCopy={handleCopy}
234
234
  />
235
- <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
235
+ <div className="rounded-md bg-muted/20 px-3 py-2">
236
236
  <div className="font-medium">In-memory logs</div>
237
237
  <div className="mt-1 font-mono text-muted-foreground">
238
238
  {stats.memoryCount.toLocaleString()} loaded
239
239
  </div>
240
240
  </div>
241
- <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
241
+ <div className="rounded-md bg-muted/20 px-3 py-2">
242
242
  <div className="font-medium">Log files</div>
243
243
  <div className="mt-1 font-mono text-muted-foreground">
244
244
  {stats.logFileCount.toLocaleString()} files / {formatBytes(stats.logBytes)}
245
245
  </div>
246
246
  </div>
247
- <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
247
+ <div className="rounded-md bg-muted/20 px-3 py-2">
248
248
  <div className="font-medium">Streaming chunks</div>
249
249
  <div className="mt-1 font-mono text-muted-foreground">
250
250
  {stats.chunkFileCount.toLocaleString()} files / {formatBytes(stats.chunkBytes)}
251
251
  </div>
252
252
  </div>
253
- <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
253
+ <div className="rounded-md bg-muted/20 px-3 py-2">
254
254
  <div className="font-medium">Session archives</div>
255
255
  <div className="mt-1 font-mono text-muted-foreground">
256
256
  {stats.sessionArchiveFileCount.toLocaleString()} files /{" "}
@@ -280,7 +280,7 @@ function CopyableSetupValue({
280
280
  }): JSX.Element {
281
281
  const copied = copiedId === id;
282
282
  return (
283
- <div className="border border-border bg-muted/40 min-w-0 rounded-md px-3 py-2">
283
+ <div className="min-w-0 rounded-md border border-border/45 bg-muted/15 px-3 py-2 transition-colors hover:bg-muted/25">
284
284
  <div className="mb-1 text-xs font-medium text-muted-foreground">{label}</div>
285
285
  <div className="flex min-w-0 items-center gap-2">
286
286
  <code className="min-w-0 flex-1 truncate font-mono text-xs text-foreground">{value}</code>
@@ -383,7 +383,7 @@ function OnboardingSettingsTab(): JSX.Element {
383
383
  />
384
384
  ))}
385
385
  </div>
386
- <div className="grid gap-2 rounded-md border border-border bg-background px-3 py-2 text-xs text-muted-foreground">
386
+ <div className="grid gap-2 rounded-md bg-muted/15 px-3 py-2 text-xs text-muted-foreground">
387
387
  <div className="flex items-center gap-2">
388
388
  <Check className="size-3.5 text-emerald-500" />
389
389
  <span>Provider test creates a traceable memory probe session.</span>
@@ -574,7 +574,7 @@ function McpSettingsTab(): JSX.Element {
574
574
  <Terminal className="size-4 text-muted-foreground" />
575
575
  <h3 className="text-sm font-semibold">MCP access</h3>
576
576
  </div>
577
- <div className="grid gap-2 rounded-md border border-border bg-background px-3 py-2 text-xs text-muted-foreground">
577
+ <div className="grid gap-2 rounded-md bg-muted/15 px-3 py-2 text-xs text-muted-foreground">
578
578
  <div>
579
579
  Use Streamable HTTP when the client supports URL-based MCP. Use the stdio bridge only for
580
580
  stdio-only clients. Configure one entry for the same Inspector instance.
@@ -680,12 +680,8 @@ function ProxySettingsTab(): JSX.Element {
680
680
  <div className="space-y-1">
681
681
  <h3 className="text-sm font-semibold">Claude Code billing header</h3>
682
682
  <p className="text-xs text-muted-foreground">
683
- When enabled, the proxy strips the synthetic <code>x-anthropic-billing-header</code> text
684
- block that Claude Code prepends to the system prompt before forwarding to upstream.
685
- Improves prefix-cache hit rates against third-party upstreams. The default at startup is
686
- controlled by the <code>AGENT_INSPECTOR_STRIP_CLAUDE_CODE_BILLING_HEADER</code>{" "}
687
- environment variable; your choice is persisted to <code>config.json</code> and overrides
688
- the env var for subsequent requests.
683
+ Strip Claude Code's synthetic billing block before upstream forwarding. This can improve
684
+ prefix-cache hits with third-party providers and is persisted to <code>config.json</code>.
689
685
  </p>
690
686
  </div>
691
687
 
@@ -710,8 +706,8 @@ function ProxySettingsTab(): JSX.Element {
710
706
  Slow response threshold
711
707
  </label>
712
708
  <p className="text-xs text-muted-foreground">
713
- Logs whose elapsed time exceeds this many seconds show a warning icon next to the
714
- duration. Set to <code>0</code> to disable the indicator.
709
+ Show a slow marker when elapsed time exceeds this many seconds. Use <code>0</code> to
710
+ disable it.
715
711
  </p>
716
712
  <div className="flex items-center gap-2">
717
713
  <input
@@ -739,8 +735,7 @@ function ProxySettingsTab(): JSX.Element {
739
735
  Provider test timeout
740
736
  </label>
741
737
  <p className="text-xs text-muted-foreground">
742
- Provider Test waits this many seconds per configured model before aborting slow probes.
743
- The full test window scales with model count.
738
+ Abort slow Provider Test probes after this many seconds per model.
744
739
  </p>
745
740
  <div className="flex items-center gap-2">
746
741
  <input
@@ -768,7 +763,7 @@ function ProxySettingsTab(): JSX.Element {
768
763
  Time display
769
764
  </label>
770
765
  <p className="text-xs text-muted-foreground">
771
- Controls timestamps in session summaries, conversation headers, and log rows.
766
+ Controls timestamps in session summaries, headers, and log rows.
772
767
  </p>
773
768
  <select
774
769
  id="time-display-format"
@@ -217,13 +217,6 @@ export const TurnGroup = memo(function TurnGroup({
217
217
  }
218
218
  }}
219
219
  >
220
- {/* Turn number */}
221
- <div className="w-5 shrink-0 flex items-start pt-1.5">
222
- <span className="text-[10px] text-muted-foreground/50 font-mono tabular-nums leading-none select-none">
223
- {turnIndex + 1}
224
- </span>
225
- </div>
226
-
227
220
  <div className="w-6 shrink-0 flex flex-col items-center pt-1.5 pb-1.5">
228
221
  <div className="flex justify-center h-[calc(0.75rem-8px)]" />
229
222
  <CollapsedCrab
@@ -344,15 +337,6 @@ export const TurnGroup = memo(function TurnGroup({
344
337
 
345
338
  return (
346
339
  <div key={log.id} className="flex items-stretch">
347
- {isTurnStart ? (
348
- <div className="w-5 shrink-0 flex items-start pt-1.5">
349
- <span className="text-[10px] text-muted-foreground/50 font-mono tabular-nums leading-none select-none">
350
- {turnIndex + 1}
351
- </span>
352
- </div>
353
- ) : (
354
- <div className="w-5 shrink-0" />
355
- )}
356
340
  <ThreadConnector
357
341
  stopReason={reason}
358
342
  isPending={log.responseStatus === null}
@@ -1,6 +1,7 @@
1
1
  import { AnthropicRequestSchema, AnthropicResponseSchema } from "../../../contracts/anthropic";
2
2
  import type { AnatomySegment } from "../anatomy/types";
3
3
  import { countCharacters, estimateTokens } from "../anatomy/tokenEstimate";
4
+ import { parseRequestTools } from "../requestTools";
4
5
  import type { LogFormatAdapter } from "./types";
5
6
  import { emptyRequestAnalysis, EMPTY_RESPONSE_ANALYSIS } from "./types";
6
7
 
@@ -113,16 +114,15 @@ export const anthropicLogFormatAdapter: LogFormatAdapter = {
113
114
  analyzeRequest(rawBody) {
114
115
  if (rawBody === null) return emptyRequestAnalysis(rawBody);
115
116
  try {
116
- const result = AnthropicRequestSchema.safeParse(JSON.parse(rawBody));
117
+ const parsedBody: unknown = JSON.parse(rawBody);
118
+ const toolCount = parseRequestTools(parsedBody)?.tools.length ?? null;
119
+ const result = AnthropicRequestSchema.safeParse(parsedBody);
117
120
  if (!result.success) return emptyRequestAnalysis(rawBody);
118
121
  return {
119
122
  parsed: result.data,
120
123
  comparisonValue: result.data,
121
124
  messageCount: result.data.messages.length,
122
- toolCount:
123
- result.data.tools !== undefined && result.data.tools.length > 0
124
- ? result.data.tools.length
125
- : null,
125
+ toolCount,
126
126
  };
127
127
  } catch {
128
128
  return emptyRequestAnalysis(rawBody);
@@ -7,6 +7,7 @@ import {
7
7
  import { safeGetOwnProperty } from "../../../lib/objectUtils";
8
8
  import type { AnatomySegment } from "../anatomy/types";
9
9
  import { countCharacters, estimateTokens } from "../anatomy/tokenEstimate";
10
+ import { parseRequestTools } from "../requestTools";
10
11
  import type { LogFormatAdapter } from "./types";
11
12
  import { emptyRequestAnalysis, EMPTY_RESPONSE_ANALYSIS } from "./types";
12
13
 
@@ -146,28 +147,24 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
146
147
  analyzeRequest(rawBody) {
147
148
  if (rawBody === null) return emptyRequestAnalysis(rawBody);
148
149
  try {
149
- const result = OpenAIRequestSchema.safeParse(JSON.parse(rawBody));
150
+ const parsedBody: unknown = JSON.parse(rawBody);
151
+ const toolCount = parseRequestTools(parsedBody)?.tools.length ?? null;
152
+ const result = OpenAIRequestSchema.safeParse(parsedBody);
150
153
  if (!result.success) {
151
- const responsesResult = OpenAIResponsesRequestSchema.safeParse(JSON.parse(rawBody));
154
+ const responsesResult = OpenAIResponsesRequestSchema.safeParse(parsedBody);
152
155
  if (!responsesResult.success) return emptyRequestAnalysis(rawBody);
153
156
  return {
154
157
  parsed: responsesResult.data,
155
158
  comparisonValue: responsesResult.data,
156
159
  messageCount: countResponsesInput(responsesResult.data.input),
157
- toolCount:
158
- responsesResult.data.tools !== undefined && responsesResult.data.tools.length > 0
159
- ? responsesResult.data.tools.length
160
- : null,
160
+ toolCount,
161
161
  };
162
162
  }
163
163
  return {
164
164
  parsed: result.data,
165
165
  comparisonValue: result.data,
166
166
  messageCount: result.data.messages.length,
167
- toolCount:
168
- result.data.tools !== undefined && result.data.tools.length > 0
169
- ? result.data.tools.length
170
- : null,
167
+ toolCount,
171
168
  };
172
169
  } catch {
173
170
  return emptyRequestAnalysis(rawBody);
@@ -145,8 +145,11 @@ function parseAnthropicTool(tool: unknown): RequestToolDefinition | null {
145
145
  function parseOpenAITool(tool: unknown): RequestToolDefinition | null {
146
146
  const fn = safeGetOwnProperty(tool, "function");
147
147
  const toolType = safeGetOwnProperty(tool, "type");
148
- if (fn === undefined && toolType !== "function") return null;
149
- const name = safeGetOwnProperty(fn, "name") ?? safeGetOwnProperty(tool, "name");
148
+ if (fn === undefined && typeof toolType !== "string") return null;
149
+ const name =
150
+ safeGetOwnProperty(fn, "name") ??
151
+ safeGetOwnProperty(tool, "name") ??
152
+ (typeof toolType === "string" ? toolType : undefined);
150
153
  if (typeof name !== "string" || name.length === 0) return null;
151
154
  const description =
152
155
  safeGetOwnProperty(fn, "description") ?? safeGetOwnProperty(tool, "description");
@@ -14,9 +14,9 @@ import {
14
14
  searchLogsPage,
15
15
  } from "../../proxy/logSearch";
16
16
 
17
- const DeleteBodySchema = z
17
+ export const DeleteLogsBodySchema = z
18
18
  .object({
19
- ids: z.array(z.number().int().positive()).optional(),
19
+ ids: z.array(z.number().int().nonnegative()).optional(),
20
20
  })
21
21
  .optional();
22
22
 
@@ -114,12 +114,12 @@ export const Route = createFileRoute("/api/logs")({
114
114
  );
115
115
  },
116
116
  DELETE: async ({ request }: { request: Request }) => {
117
- let body: z.infer<typeof DeleteBodySchema> = undefined;
117
+ let body: z.infer<typeof DeleteLogsBodySchema> = undefined;
118
118
  try {
119
119
  const raw = await request.text();
120
120
  if (raw !== "") {
121
121
  const parsed: unknown = JSON.parse(raw);
122
- const result = DeleteBodySchema.safeParse(parsed);
122
+ const result = DeleteLogsBodySchema.safeParse(parsed);
123
123
  if (!result.success) {
124
124
  return Response.json(
125
125
  { error: "Invalid request body", details: result.error.format() },
@@ -132,7 +132,16 @@ export const Route = createFileRoute("/api/logs")({
132
132
  return Response.json({ error: "Invalid JSON body" }, { status: 400 });
133
133
  }
134
134
 
135
- if (body?.ids !== undefined && body.ids.length > 0) {
135
+ if (body?.ids !== undefined) {
136
+ if (body.ids.length === 0) {
137
+ return Response.json({
138
+ success: true,
139
+ cleared: 0,
140
+ logFilesRewritten: 0,
141
+ chunkFilesDeleted: 0,
142
+ sessionArchiveLogsDeleted: 0,
143
+ });
144
+ }
136
145
  const result = await clearPersistedLogsByIds(body.ids);
137
146
  return Response.json({
138
147
  success: true,