@tonyclaw/agent-inspector 2.0.39 → 2.0.41

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 (44) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-DCg6S2cl.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-DsU6QETm.js +115 -0
  4. package/.output/public/assets/{ReplayDialog-B58TbTtY.js → ReplayDialog-ClVgjSDE.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-d4P6JNiP.js → RequestAnatomy-BhqvLQp9.js} +1 -1
  6. package/.output/public/assets/ResponseView-BZAJoK6B.js +1 -0
  7. package/.output/public/assets/StreamingChunkSequence-CaORmoKX.js +1 -0
  8. package/.output/public/assets/_sessionId-BHqywvM3.js +1 -0
  9. package/.output/public/assets/index-B_LPYuM0.js +1 -0
  10. package/.output/public/assets/{main-DK9PoBjx.js → main-CI3HFEcV.js} +2 -2
  11. package/.output/server/{_sessionId-DjarbXpT.mjs → _sessionId-DQ0ljHQ8.mjs} +2 -2
  12. package/.output/server/_ssr/{CompareDrawer-C5Z6AlUk.mjs → CompareDrawer-nkVa9epn.mjs} +4 -3
  13. package/.output/server/_ssr/{ProxyViewerContainer-4pXsBywn.mjs → ProxyViewerContainer-CXA7iH3H.mjs} +140 -45
  14. package/.output/server/_ssr/{ReplayDialog-w1vc1p5e.mjs → ReplayDialog-CK71-ucq.mjs} +3 -3
  15. package/.output/server/_ssr/{RequestAnatomy-2HaBhsNV.mjs → RequestAnatomy-DDCUJJ3X.mjs} +2 -2
  16. package/.output/server/_ssr/{ResponseView-SO_Y_W85.mjs → ResponseView-C6ZA7V3B.mjs} +2 -2
  17. package/.output/server/_ssr/{StreamingChunkSequence-CXEa2DkO.mjs → StreamingChunkSequence-DW5v_o6G.mjs} +2 -2
  18. package/.output/server/_ssr/{index-DuCGt4a2.mjs → index-Ckex98yq.mjs} +2 -2
  19. package/.output/server/_ssr/index.mjs +2 -2
  20. package/.output/server/_ssr/{router-D0BaSzkZ.mjs → router-ChJIVv7-.mjs} +434 -6
  21. package/.output/server/{_tanstack-start-manifest_v-6N4SODyd.mjs → _tanstack-start-manifest_v-CJ4__weU.mjs} +1 -1
  22. package/.output/server/index.mjs +62 -62
  23. package/package.json +1 -1
  24. package/src/components/ProxyViewer.tsx +5 -2
  25. package/src/components/providers/ProviderCard.tsx +6 -3
  26. package/src/components/providers/ProviderForm.tsx +6 -3
  27. package/src/components/providers/ProvidersPanel.tsx +6 -1
  28. package/src/components/providers/SettingsDialog.tsx +5 -2
  29. package/src/components/proxy-viewer/CompareDrawer.tsx +3 -1
  30. package/src/components/proxy-viewer/LogEntry.tsx +23 -6
  31. package/src/components/proxy-viewer/LogEntryHeader.tsx +20 -0
  32. package/src/components/proxy-viewer/useCopyFeedback.ts +3 -1
  33. package/src/components/ui/json-viewer.tsx +7 -2
  34. package/src/contracts/log.ts +1 -0
  35. package/src/lib/clipboard.ts +67 -0
  36. package/src/proxy/logFinalizer.ts +58 -0
  37. package/src/proxy/logSearch.ts +1 -0
  38. package/src/proxy/toolSchemaWarnings.ts +515 -0
  39. package/.output/public/assets/CompareDrawer-Jfe6M6DQ.js +0 -1
  40. package/.output/public/assets/ProxyViewerContainer-CQKV0Bqq.js +0 -115
  41. package/.output/public/assets/ResponseView-Bum1a3tG.js +0 -1
  42. package/.output/public/assets/StreamingChunkSequence-CjBDp5E9.js +0 -1
  43. package/.output/public/assets/_sessionId-BupV2gtf.js +0 -1
  44. package/.output/public/assets/index-DHSww5em.js +0 -1
@@ -35,103 +35,103 @@ const headers = ((m) => function headersRouteRule(event) {
35
35
  }
36
36
  });
37
37
  const assets = {
38
+ "/assets/CompareDrawer-DCg6S2cl.js": {
39
+ "type": "text/javascript; charset=utf-8",
40
+ "etag": '"49e8-k5Sysa3YIYWHG3hgZOKGI6S/VSQ"',
41
+ "mtime": "2026-07-02T06:48:19.896Z",
42
+ "size": 18920,
43
+ "path": "../public/assets/CompareDrawer-DCg6S2cl.js"
44
+ },
38
45
  "/assets/alibaba-TTwafVwX.svg": {
39
46
  "type": "image/svg+xml",
40
47
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
- "mtime": "2026-07-01T08:15:03.700Z",
48
+ "mtime": "2026-07-02T06:48:19.896Z",
42
49
  "size": 5915,
43
50
  "path": "../public/assets/alibaba-TTwafVwX.svg"
44
51
  },
45
- "/assets/index-BfGJEb-2.css": {
46
- "type": "text/css; charset=utf-8",
47
- "etag": '"1acc9-JrjZs7fIRGuBr66cqP9miLHcLKk"',
48
- "mtime": "2026-07-01T08:15:03.700Z",
49
- "size": 109769,
50
- "path": "../public/assets/index-BfGJEb-2.css"
52
+ "/assets/minimax-BPMzvuL-.jpeg": {
53
+ "type": "image/jpeg",
54
+ "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
55
+ "mtime": "2026-07-02T06:48:19.896Z",
56
+ "size": 6918,
57
+ "path": "../public/assets/minimax-BPMzvuL-.jpeg"
51
58
  },
52
- "/assets/CompareDrawer-Jfe6M6DQ.js": {
59
+ "/assets/RequestAnatomy-BhqvLQp9.js": {
53
60
  "type": "text/javascript; charset=utf-8",
54
- "etag": '"49fe-cZOHUuR1k4rWdDq/tpP3ItQh8XQ"',
55
- "mtime": "2026-07-01T08:15:03.700Z",
56
- "size": 18942,
57
- "path": "../public/assets/CompareDrawer-Jfe6M6DQ.js"
61
+ "etag": '"4485-e5cA5GSbVksNziulI0+GKBLNU8I"',
62
+ "mtime": "2026-07-02T06:48:19.896Z",
63
+ "size": 17541,
64
+ "path": "../public/assets/RequestAnatomy-BhqvLQp9.js"
58
65
  },
59
- "/assets/ReplayDialog-B58TbTtY.js": {
66
+ "/assets/ReplayDialog-ClVgjSDE.js": {
60
67
  "type": "text/javascript; charset=utf-8",
61
- "etag": '"318c-bhy3YBiteXvsEiEFJJ5jKXid1do"',
62
- "mtime": "2026-07-01T08:15:03.700Z",
68
+ "etag": '"318c-LpwaR4Dkb9DojATSYhkNfnmy2zI"',
69
+ "mtime": "2026-07-02T06:48:19.896Z",
63
70
  "size": 12684,
64
- "path": "../public/assets/ReplayDialog-B58TbTtY.js"
71
+ "path": "../public/assets/ReplayDialog-ClVgjSDE.js"
65
72
  },
66
- "/assets/index-DHSww5em.js": {
73
+ "/assets/index-B_LPYuM0.js": {
67
74
  "type": "text/javascript; charset=utf-8",
68
- "etag": '"74-XnA+tuBqoE04/VDIU5skdKnqMeo"',
69
- "mtime": "2026-07-01T08:15:03.700Z",
75
+ "etag": '"74-nFohMzCw3y6hRBAH/I/1aAKk9qA"',
76
+ "mtime": "2026-07-02T06:48:19.896Z",
70
77
  "size": 116,
71
- "path": "../public/assets/index-DHSww5em.js"
72
- },
73
- "/assets/minimax-BPMzvuL-.jpeg": {
74
- "type": "image/jpeg",
75
- "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
76
- "mtime": "2026-07-01T08:15:03.700Z",
77
- "size": 6918,
78
- "path": "../public/assets/minimax-BPMzvuL-.jpeg"
78
+ "path": "../public/assets/index-B_LPYuM0.js"
79
79
  },
80
- "/assets/main-DK9PoBjx.js": {
81
- "type": "text/javascript; charset=utf-8",
82
- "etag": '"517d9-Rb3G8Owiwv/6jNymVmF2TN5klug"',
83
- "mtime": "2026-07-01T08:15:03.700Z",
84
- "size": 333785,
85
- "path": "../public/assets/main-DK9PoBjx.js"
80
+ "/assets/index-BfGJEb-2.css": {
81
+ "type": "text/css; charset=utf-8",
82
+ "etag": '"1acc9-JrjZs7fIRGuBr66cqP9miLHcLKk"',
83
+ "mtime": "2026-07-02T06:48:19.896Z",
84
+ "size": 109769,
85
+ "path": "../public/assets/index-BfGJEb-2.css"
86
86
  },
87
- "/assets/ResponseView-Bum1a3tG.js": {
87
+ "/assets/StreamingChunkSequence-CaORmoKX.js": {
88
88
  "type": "text/javascript; charset=utf-8",
89
- "etag": '"30d0-nm7943Zf3wkOX8Qkx5tFeTPBdhs"',
90
- "mtime": "2026-07-01T08:15:03.702Z",
91
- "size": 12496,
92
- "path": "../public/assets/ResponseView-Bum1a3tG.js"
89
+ "etag": '"d5c-CFWA5sGEVjRZL7Tc8f3pZV/+3pw"',
90
+ "mtime": "2026-07-02T06:48:19.896Z",
91
+ "size": 3420,
92
+ "path": "../public/assets/StreamingChunkSequence-CaORmoKX.js"
93
93
  },
94
- "/assets/RequestAnatomy-d4P6JNiP.js": {
94
+ "/assets/ResponseView-BZAJoK6B.js": {
95
95
  "type": "text/javascript; charset=utf-8",
96
- "etag": '"4485-R6aSqQKKsXZ9KJifvk5uB/aAP/w"',
97
- "mtime": "2026-07-01T08:15:03.700Z",
98
- "size": 17541,
99
- "path": "../public/assets/RequestAnatomy-d4P6JNiP.js"
96
+ "etag": '"30d1-1lX2qKD6ndedO8OL4mCAFqTi2c4"',
97
+ "mtime": "2026-07-02T06:48:19.896Z",
98
+ "size": 12497,
99
+ "path": "../public/assets/ResponseView-BZAJoK6B.js"
100
100
  },
101
101
  "/assets/zhipuai-BPNAnxo-.svg": {
102
102
  "type": "image/svg+xml",
103
103
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
104
- "mtime": "2026-07-01T08:15:03.700Z",
104
+ "mtime": "2026-07-02T06:48:19.896Z",
105
105
  "size": 11256,
106
106
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
107
107
  },
108
- "/assets/StreamingChunkSequence-CjBDp5E9.js": {
108
+ "/assets/_sessionId-BHqywvM3.js": {
109
+ "type": "text/javascript; charset=utf-8",
110
+ "etag": '"d2-aRnjHQXX3+PkuHFc37DXAEd2jHY"',
111
+ "mtime": "2026-07-02T06:48:19.896Z",
112
+ "size": 210,
113
+ "path": "../public/assets/_sessionId-BHqywvM3.js"
114
+ },
115
+ "/assets/main-CI3HFEcV.js": {
109
116
  "type": "text/javascript; charset=utf-8",
110
- "etag": '"d57-japFPQBd84wFo2PfIzD3rpUCwE0"',
111
- "mtime": "2026-07-01T08:15:03.700Z",
112
- "size": 3415,
113
- "path": "../public/assets/StreamingChunkSequence-CjBDp5E9.js"
117
+ "etag": '"517d9-uRZNMUg8U43tHnhzFFtGrUV430s"',
118
+ "mtime": "2026-07-02T06:48:19.896Z",
119
+ "size": 333785,
120
+ "path": "../public/assets/main-CI3HFEcV.js"
114
121
  },
115
122
  "/assets/qwen-CONDcHqt.png": {
116
123
  "type": "image/png",
117
124
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
118
- "mtime": "2026-07-01T08:15:03.700Z",
125
+ "mtime": "2026-07-02T06:48:19.896Z",
119
126
  "size": 357059,
120
127
  "path": "../public/assets/qwen-CONDcHqt.png"
121
128
  },
122
- "/assets/_sessionId-BupV2gtf.js": {
123
- "type": "text/javascript; charset=utf-8",
124
- "etag": '"d2-yDl+GeqeLZbuzelLqReyuecyYVA"',
125
- "mtime": "2026-07-01T08:15:03.700Z",
126
- "size": 210,
127
- "path": "../public/assets/_sessionId-BupV2gtf.js"
128
- },
129
- "/assets/ProxyViewerContainer-CQKV0Bqq.js": {
129
+ "/assets/ProxyViewerContainer-DsU6QETm.js": {
130
130
  "type": "text/javascript; charset=utf-8",
131
- "etag": '"b5e33-yXZRXjn1n1qIfzezs39GrqNlfhg"',
132
- "mtime": "2026-07-01T08:15:03.702Z",
133
- "size": 745011,
134
- "path": "../public/assets/ProxyViewerContainer-CQKV0Bqq.js"
131
+ "etag": '"b645c-NZxv8v6zEqGaTkLxqNhFQyxqtsc"',
132
+ "mtime": "2026-07-02T06:48:19.896Z",
133
+ "size": 746588,
134
+ "path": "../public/assets/ProxyViewerContainer-DsU6QETm.js"
135
135
  }
136
136
  };
137
137
  function readAsset(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/agent-inspector",
3
- "version": "2.0.39",
3
+ "version": "2.0.41",
4
4
  "type": "module",
5
5
  "description": "Agent observability and knowledge capture layer for AI coding tools.",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@ import {
13
13
 
14
14
  import type { CapturedLog } from "../contracts";
15
15
  import { exportLogsAsZip, type ExportMode } from "../lib/export-logs";
16
+ import { copyTextToClipboard } from "../lib/clipboard";
16
17
  import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
17
18
  import { formatTimestampRange } from "../lib/timeDisplay";
18
19
  import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
@@ -276,7 +277,8 @@ function CopyableCommand({ command }: { command: string }): JSX.Element {
276
277
  const [copied, setCopied] = useState(false);
277
278
 
278
279
  const handleCopy = useCallback(() => {
279
- void window.navigator.clipboard.writeText(command).then(() => {
280
+ void copyTextToClipboard(command).then((success) => {
281
+ if (!success) return;
280
282
  setCopied(true);
281
283
  setTimeout(() => setCopied(false), 2000);
282
284
  });
@@ -557,7 +559,8 @@ function SessionContextBar({
557
559
  const userAgent = useMemo(() => getFirstUserAgent(logs), [logs]);
558
560
 
559
561
  const handleCopyLink = useCallback(() => {
560
- void window.navigator.clipboard.writeText(window.location.href).then(() => {
562
+ void copyTextToClipboard(window.location.href).then((success) => {
563
+ if (!success) return;
561
564
  setCopied(true);
562
565
  setTimeout(() => setCopied(false), 2000);
563
566
  });
@@ -24,6 +24,7 @@ import {
24
24
  ChevronRight,
25
25
  } from "lucide-react";
26
26
  import type { ProviderConfig } from "../../proxy/providers";
27
+ import { copyTextToClipboard } from "../../lib/clipboard";
27
28
  import { maskApiKey } from "../../lib/mask";
28
29
  import {
29
30
  findProviderModelMetadata,
@@ -422,9 +423,11 @@ export function ProviderCard({
422
423
  const [showModelResults, setShowModelResults] = useState(false);
423
424
 
424
425
  function handleCopy() {
425
- navigator.clipboard.writeText(provider.apiKey).catch(() => {});
426
- setCopied(true);
427
- setTimeout(() => setCopied(false), 2000);
426
+ void copyTextToClipboard(provider.apiKey).then((success) => {
427
+ if (!success) return;
428
+ setCopied(true);
429
+ setTimeout(() => setCopied(false), 2000);
430
+ });
428
431
  }
429
432
 
430
433
  // Get docs URL: use provider.apiDocsUrl if configured, otherwise check KNOWN_PROVIDER_DOCS
@@ -4,6 +4,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
4
4
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
5
5
  import { Eye, EyeOff, Copy, Check, ChevronDown } from "lucide-react";
6
6
  import type { ProviderConfig, ProviderModelMetadata } from "../../lib/providerContract";
7
+ import { copyTextToClipboard } from "../../lib/clipboard";
7
8
  import { maskApiKey } from "../../lib/mask";
8
9
  import { formatContextWindowInput, parseContextWindowTokensInput } from "../../lib/utils";
9
10
 
@@ -316,9 +317,11 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
316
317
  provider?.modelMetadata?.some((metadata) => metadata.contextWindow !== undefined) ?? false;
317
318
 
318
319
  function handleCopy() {
319
- navigator.clipboard.writeText(apiKey).catch(() => {});
320
- setCopied(true);
321
- setTimeout(() => setCopied(false), 2000);
320
+ void copyTextToClipboard(apiKey).then((success) => {
321
+ if (!success) return;
322
+ setCopied(true);
323
+ setTimeout(() => setCopied(false), 2000);
324
+ });
322
325
  }
323
326
 
324
327
  useEffect(() => {
@@ -8,6 +8,7 @@ import { ConfirmDialog } from "../ui/confirm-dialog";
8
8
  import { ProviderCard } from "./ProviderCard";
9
9
  import { ProviderForm } from "./ProviderForm";
10
10
  import { parseJsonResponse, readApiError } from "../../lib/apiClient";
11
+ import { copyTextToClipboard } from "../../lib/clipboard";
11
12
  import {
12
13
  ProviderConfigSchema,
13
14
  type ProviderConfig,
@@ -614,12 +615,16 @@ export function ProvidersPanel({
614
615
  <button
615
616
  type="button"
616
617
  onClick={() => {
617
- void window.navigator.clipboard.writeText(configPath).then(() => {
618
+ void copyTextToClipboard(configPath).then((success) => {
619
+ if (!success) return;
618
620
  setConfigPathCopied(true);
619
621
  setTimeout(() => setConfigPathCopied(false), 2000);
620
622
  });
621
623
  }}
622
624
  className="shrink-0 ml-auto text-muted-foreground hover:text-foreground transition-colors"
625
+ aria-label={
626
+ configPathCopied ? "Copied config file path" : "Copy config file path"
627
+ }
623
628
  >
624
629
  {configPathCopied ? (
625
630
  <Check className="size-3 text-green-500" />
@@ -7,6 +7,7 @@ import { Button } from "../ui/button";
7
7
  import { ProvidersPanel } from "./ProvidersPanel";
8
8
  import { useProviders } from "../../lib/useProviders";
9
9
  import { useStripConfig } from "../../lib/useStripConfig";
10
+ import { copyTextToClipboard } from "../../lib/clipboard";
10
11
  import {
11
12
  MAX_PROVIDER_TEST_TIMEOUT_SECONDS,
12
13
  MAX_SLOW_RESPONSE_THRESHOLD_SECONDS,
@@ -171,7 +172,8 @@ function StorageSettingsTab(): JSX.Element {
171
172
  }, [refresh]);
172
173
 
173
174
  const handleCopy = useCallback((id: string, value: string) => {
174
- void window.navigator.clipboard.writeText(value).then(() => {
175
+ void copyTextToClipboard(value).then((success) => {
176
+ if (!success) return;
175
177
  setCopiedId(id);
176
178
  setTimeout(() => setCopiedId(null), 1600);
177
179
  });
@@ -314,7 +316,8 @@ function OnboardingSettingsTab(): JSX.Element {
314
316
  );
315
317
 
316
318
  const handleCopy = useCallback((id: string, value: string) => {
317
- void window.navigator.clipboard.writeText(value).then(() => {
319
+ void copyTextToClipboard(value).then((success) => {
320
+ if (!success) return;
318
321
  setCopiedId(id);
319
322
  setTimeout(() => setCopiedId(null), 1600);
320
323
  });
@@ -13,6 +13,7 @@ import {
13
13
  X,
14
14
  } from "lucide-react";
15
15
  import { cn, formatTokens } from "../../lib/utils";
16
+ import { copyTextToClipboard } from "../../lib/clipboard";
16
17
  import type { CapturedLog } from "../../contracts";
17
18
  import {
18
19
  type DiffOp,
@@ -656,7 +657,8 @@ export function CompareDrawer({ left, right, onClose }: CompareDrawerProps): JSX
656
657
  const [copiedPath, setCopiedPath] = useState<string | null>(null);
657
658
  const copyResetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
658
659
  const onCopyPath = (path: string) => {
659
- void window.navigator.clipboard.writeText(path).then(() => {
660
+ void copyTextToClipboard(path).then((success) => {
661
+ if (!success) return;
660
662
  setCopiedPath(path);
661
663
  if (copyResetTimer.current !== null) clearTimeout(copyResetTimer.current);
662
664
  copyResetTimer.current = setTimeout(() => setCopiedPath(null), 1500);
@@ -1,6 +1,6 @@
1
- import { GitCompareArrows } from "lucide-react";
1
+ import { AlertTriangle, GitCompareArrows } from "lucide-react";
2
2
  import { Suspense, type JSX } from "react";
3
- import { useEffect, useMemo, useRef, useState, memo } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState, memo } from "react";
4
4
  import type { CapturedLog } from "../../contracts";
5
5
  import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
6
6
  import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
@@ -295,6 +295,7 @@ export const LogEntry = memo(function ({
295
295
  () => parseRequestTools(requestExpansion.parsedData),
296
296
  [requestExpansion.parsedData],
297
297
  );
298
+ const warnings = log.warnings ?? [];
298
299
  const anatomyPaths = useMemo(() => {
299
300
  if (anatomySegments === null) return undefined;
300
301
  return new Set(anatomySegments.map((s) => s.path));
@@ -307,9 +308,12 @@ export const LogEntry = memo(function ({
307
308
  },
308
309
  });
309
310
 
310
- useEffect(() => {
311
- if (!expanded) return;
312
- setActiveTab(anatomySegments !== null ? "anatomy" : "request");
311
+ const handleToggleExpanded = useCallback(() => {
312
+ const nextExpanded = !expanded;
313
+ if (nextExpanded) {
314
+ setActiveTab(anatomySegments !== null ? "anatomy" : "request");
315
+ }
316
+ setExpanded(nextExpanded);
313
317
  }, [anatomySegments, expanded]);
314
318
 
315
319
  useEffect(() => {
@@ -335,7 +339,7 @@ export const LogEntry = memo(function ({
335
339
  messageCount={requestAnalysis.messageCount}
336
340
  toolCount={requestAnalysis.toolCount}
337
341
  expanded={expanded}
338
- onToggle={() => setExpanded(!expanded)}
342
+ onToggle={handleToggleExpanded}
339
343
  cacheTrend={cacheTrend}
340
344
  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
341
345
  timeDisplayFormat={timeDisplayFormat}
@@ -392,6 +396,19 @@ export const LogEntry = memo(function ({
392
396
  <TabsContent value="request">
393
397
  {activeTab === "request" && (
394
398
  <div className="px-4 pt-1 pb-3">
399
+ {warnings.length > 0 && (
400
+ <div className="mb-3 rounded border border-amber-500/30 bg-amber-500/10 p-3 text-xs">
401
+ <div className="flex items-center gap-2 font-semibold text-amber-300">
402
+ <AlertTriangle className="size-3.5" />
403
+ Tool schema warnings
404
+ </div>
405
+ <ul className="mt-2 space-y-1 text-muted-foreground">
406
+ {warnings.map((warning) => (
407
+ <li key={warning}>{warning}</li>
408
+ ))}
409
+ </ul>
410
+ </div>
411
+ )}
395
412
  <RequestTools
396
413
  summary={requestTools}
397
414
  calledToolNames={responseAnalysis.toolNames ?? []}
@@ -159,6 +159,7 @@ export const LogEntryHeader = memo(function ({
159
159
  const firstChunkLabel = firstChunkMs === null ? null : formatElapsed(firstChunkMs);
160
160
  const tokenRateLabel =
161
161
  tokensPerSecond === null || tokensPerSecond <= 0 ? null : formatTokenRate(tokensPerSecond);
162
+ const warningCount = log.warnings?.length ?? 0;
162
163
 
163
164
  return (
164
165
  <TooltipProvider>
@@ -229,6 +230,25 @@ export const LogEntryHeader = memo(function ({
229
230
  </Badge>
230
231
  )}
231
232
 
233
+ {warningCount > 0 && (
234
+ <Tooltip>
235
+ <TooltipTrigger asChild>
236
+ <Badge
237
+ variant="outline"
238
+ className="text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums gap-1 bg-amber-500/15 text-amber-400 border-amber-500/25"
239
+ >
240
+ <AlertTriangle className="size-3" aria-label="Tool schema warnings" />
241
+ {warningCount}
242
+ </Badge>
243
+ </TooltipTrigger>
244
+ <TooltipContent>
245
+ {warningCount === 1
246
+ ? "1 tool schema warning"
247
+ : `${warningCount} tool schema warnings`}
248
+ </TooltipContent>
249
+ </Tooltip>
250
+ )}
251
+
232
252
  {/* Elapsed time */}
233
253
  {log.elapsedMs !== null && (
234
254
  <Tooltip>
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState, type MouseEvent } from "react";
2
+ import { copyTextToClipboard } from "../../lib/clipboard";
2
3
 
3
4
  /**
4
5
  * Clipboard write with a 2s "Copied!" indicator. Returns null-safe
@@ -23,7 +24,8 @@ export function useCopyFeedback(text: string | null): {
23
24
  (event: MouseEvent) => {
24
25
  event.stopPropagation();
25
26
  if (text === null) return;
26
- void window.navigator.clipboard.writeText(text).then(() => {
27
+ void copyTextToClipboard(text).then((success) => {
28
+ if (!success) return;
27
29
  setCopied(true);
28
30
  if (timerRef.current !== null) clearTimeout(timerRef.current);
29
31
  timerRef.current = setTimeout(() => setCopied(false), 2000);
@@ -1,6 +1,7 @@
1
1
  import { Check, ChevronDown, ChevronRight, ChevronsDown, Copy } from "lucide-react";
2
2
  import { type JSX, memo, useEffect, useMemo, useState } from "react";
3
3
  import ReactMarkdown from "react-markdown";
4
+ import { copyTextToClipboard } from "../../lib/clipboard";
4
5
  import { cn } from "../../lib/utils";
5
6
  import { Button } from "./button";
6
7
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
@@ -134,7 +135,8 @@ function CopyValueButton({ value }: { value: JsonValue }): JSX.Element {
134
135
 
135
136
  function handleCopy(e: React.MouseEvent): void {
136
137
  e.stopPropagation();
137
- void window.navigator.clipboard.writeText(JSON.stringify(value, null, 2)).then(() => {
138
+ void copyTextToClipboard(JSON.stringify(value, null, 2)).then((success) => {
139
+ if (!success) return;
138
140
  setCopied(true);
139
141
  setTimeout(() => {
140
142
  setCopied(false);
@@ -147,6 +149,7 @@ function CopyValueButton({ value }: { value: JsonValue }): JSX.Element {
147
149
  type="button"
148
150
  onClick={handleCopy}
149
151
  className="inline-flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground transition-colors"
152
+ aria-label={copied ? "Copied JSON" : "Copy JSON"}
150
153
  title="Copy JSON"
151
154
  >
152
155
  {copied ? <Check className="size-3 text-green-500" /> : <Copy className="size-3" />}
@@ -159,7 +162,8 @@ function CopyButton({ value }: { value: JsonValue }): JSX.Element {
159
162
 
160
163
  function handleCopy(e: React.MouseEvent): void {
161
164
  e.stopPropagation();
162
- void window.navigator.clipboard.writeText(JSON.stringify(value, null, 2)).then(() => {
165
+ void copyTextToClipboard(JSON.stringify(value, null, 2)).then((success) => {
166
+ if (!success) return;
163
167
  setCopied(true);
164
168
  setTimeout(() => {
165
169
  setCopied(false);
@@ -172,6 +176,7 @@ function CopyButton({ value }: { value: JsonValue }): JSX.Element {
172
176
  type="button"
173
177
  onClick={handleCopy}
174
178
  className="opacity-0 group-hover/row:opacity-100 hover:bg-muted p-0.5 rounded transition-opacity shrink-0"
179
+ aria-label={copied ? "Copied JSON value" : "Copy JSON value"}
175
180
  title="Copy to clipboard"
176
181
  >
177
182
  {copied ? (
@@ -58,6 +58,7 @@ export const CapturedLogSchema = z.object({
58
58
  rawRequestBodyBytes: z.number().int().nonnegative().nullable().optional(),
59
59
  responseTextBytes: z.number().int().nonnegative().nullable().optional(),
60
60
  bodyContentMode: z.enum(["full", "compact", "truncated"]).optional(),
61
+ warnings: z.array(z.string()).optional(),
61
62
  /** Error message from streaming response (e.g., SSE error event) */
62
63
  error: z.string().nullable().optional(),
63
64
  });
@@ -0,0 +1,67 @@
1
+ function restoreSelection(selection: Selection | null, ranges: readonly Range[]): void {
2
+ if (selection === null) return;
3
+ selection.removeAllRanges();
4
+ for (const range of ranges) {
5
+ selection.addRange(range);
6
+ }
7
+ }
8
+
9
+ function readSelectionRanges(selection: Selection | null): Range[] {
10
+ if (selection === null) return [];
11
+ const ranges: Range[] = [];
12
+ for (let index = 0; index < selection.rangeCount; index++) {
13
+ ranges.push(selection.getRangeAt(index));
14
+ }
15
+ return ranges;
16
+ }
17
+
18
+ function copyWithTextareaFallback(text: string): boolean {
19
+ if (typeof document === "undefined") return false;
20
+
21
+ const activeElement = document.activeElement;
22
+ const selection = document.getSelection();
23
+ const ranges = readSelectionRanges(selection);
24
+ const textarea = document.createElement("textarea");
25
+
26
+ textarea.value = text;
27
+ textarea.setAttribute("readonly", "");
28
+ textarea.style.position = "fixed";
29
+ textarea.style.top = "0";
30
+ textarea.style.left = "-9999px";
31
+ textarea.style.opacity = "0";
32
+
33
+ document.body.appendChild(textarea);
34
+ textarea.focus();
35
+ textarea.select();
36
+ textarea.setSelectionRange(0, text.length);
37
+
38
+ let copied = false;
39
+ try {
40
+ copied = document.execCommand("copy");
41
+ } catch {
42
+ // Keep the false default.
43
+ } finally {
44
+ document.body.removeChild(textarea);
45
+ restoreSelection(selection, ranges);
46
+ if (typeof HTMLElement !== "undefined" && activeElement instanceof HTMLElement) {
47
+ activeElement.focus({ preventScroll: true });
48
+ }
49
+ }
50
+
51
+ return copied;
52
+ }
53
+
54
+ export async function copyTextToClipboard(text: string): Promise<boolean> {
55
+ const clipboard = globalThis.navigator?.clipboard;
56
+ if (clipboard !== undefined) {
57
+ try {
58
+ await clipboard.writeText(text);
59
+ return true;
60
+ } catch {
61
+ // Fall back below. Some embedded browsers expose Clipboard API but reject
62
+ // writes outside a secure context or without an explicit permission grant.
63
+ }
64
+ }
65
+
66
+ return copyWithTextareaFallback(text);
67
+ }
@@ -9,6 +9,7 @@ import type { CapturedLog } from "./schemas";
9
9
  import { getSessionProcess, isSessionProcessAvailable } from "./sessionProcess";
10
10
  import { finalizeLogUpdate } from "./store";
11
11
  import { computeTokensPerSecond } from "./streamTiming";
12
+ import { analyzeToolSchemaWarnings } from "./toolSchemaWarnings";
12
13
 
13
14
  type BaseFinalizeLogJob = {
14
15
  log: CapturedLog;
@@ -91,6 +92,7 @@ export function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record
91
92
  clientProjectFolder: log.clientProjectFolder,
92
93
  streamingChunks: log.streamingChunks,
93
94
  streamingChunksPath: log.streamingChunksPath,
95
+ warnings: log.warnings,
94
96
  error: log.error,
95
97
  };
96
98
  }
@@ -152,6 +154,57 @@ function applyOptionalStreamingTiming(log: CapturedLog, job: FinalizeLogJob): vo
152
154
  }
153
155
  }
154
156
 
157
+ type ToolSchemaWarningAnalysisJob = {
158
+ log: CapturedLog;
159
+ upstreamUrl: string;
160
+ error: string | null;
161
+ };
162
+
163
+ let toolSchemaWarningQueue: Promise<void> = Promise.resolve();
164
+
165
+ function deferToolSchemaWarningAnalysis(): Promise<void> {
166
+ return new Promise((resolve) => {
167
+ setTimeout(resolve, 0);
168
+ });
169
+ }
170
+
171
+ function withToolSchemaWarnings(log: CapturedLog): CapturedLog | null {
172
+ const warnings = analyzeToolSchemaWarnings({
173
+ rawRequestBody: log.rawRequestBody,
174
+ responseText: log.responseText,
175
+ apiFormat: log.apiFormat,
176
+ });
177
+ if (warnings.length === 0) return null;
178
+ const mergedWarnings = Array.from(new Set([...(log.warnings ?? []), ...warnings]));
179
+ if (mergedWarnings.length === (log.warnings?.length ?? 0)) return null;
180
+ return { ...log, warnings: mergedWarnings };
181
+ }
182
+
183
+ async function runToolSchemaWarningAnalysis(job: ToolSchemaWarningAnalysisJob): Promise<void> {
184
+ await deferToolSchemaWarningAnalysis();
185
+ const updatedLog = withToolSchemaWarnings(job.log);
186
+ if (updatedLog === null) return;
187
+
188
+ appendLogEntry({ ...buildFileLogEntry(updatedLog, job.upstreamUrl), error: job.error });
189
+ finalizeLogUpdate(updatedLog);
190
+ }
191
+
192
+ function scheduleToolSchemaWarningAnalysis(job: ToolSchemaWarningAnalysisJob): void {
193
+ toolSchemaWarningQueue = toolSchemaWarningQueue
194
+ .catch(() => undefined)
195
+ .then(() => runToolSchemaWarningAnalysis(job))
196
+ .catch((err: unknown) => {
197
+ logger.warn(
198
+ `[logFinalizer] Tool schema warning analysis failed for log #${job.log.id}:`,
199
+ errorMessage(err),
200
+ );
201
+ });
202
+ }
203
+
204
+ export async function flushToolSchemaWarningAnalysis(): Promise<void> {
205
+ await toolSchemaWarningQueue;
206
+ }
207
+
155
208
  function finalizeNonStreaming(
156
209
  job: FinalizeNonStreamingLogJob,
157
210
  log: CapturedLog,
@@ -384,6 +437,11 @@ export function buildFinalizeLogResult(job: FinalizeLogJob): FinalizeLogResult {
384
437
  export function commitFinalizeLogResult(result: FinalizeLogResult): void {
385
438
  appendLogEntry({ ...buildFileLogEntry(result.log, result.upstreamUrl), error: result.error });
386
439
  finalizeLogUpdate(result.log);
440
+ scheduleToolSchemaWarningAnalysis({
441
+ log: cloneLog(result.log),
442
+ upstreamUrl: result.upstreamUrl,
443
+ error: result.error,
444
+ });
387
445
  if (result.cleanupRawStreamPath !== undefined && result.cleanupRawStreamPath !== null) {
388
446
  try {
389
447
  rmSync(result.cleanupRawStreamPath, { force: true });
@@ -70,6 +70,7 @@ function searchableFields(log: CapturedLog): string[] {
70
70
  optionalText(log.clientCwd),
71
71
  optionalText(log.clientProjectFolder),
72
72
  optionalText(log.error),
73
+ ...(log.warnings ?? []),
73
74
  ].filter((value): value is string => value !== null && value.length > 0);
74
75
  }
75
76