@tonyclaw/llm-inspector 1.16.3 → 1.16.5

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 (58) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-C1w4KUGZ.js +1 -0
  3. package/.output/public/assets/ReplayDialog-DR2Sgq_g.js +1 -0
  4. package/.output/public/assets/RequestAnatomy-DAre35kj.js +1 -0
  5. package/.output/public/assets/ResponseView-ackes7_g.js +1 -0
  6. package/.output/public/assets/StreamingChunkSequence-GrXwIGKA.js +1 -0
  7. package/.output/public/assets/index-BGzHFOEX.css +1 -0
  8. package/.output/public/assets/index-DX88k9br.js +101 -0
  9. package/.output/public/assets/json-viewer-C_QUhGeu.js +14 -0
  10. package/.output/public/assets/{main-Cpts3Ifr.js → main-CDMdNDY_.js} +1 -1
  11. package/.output/server/_libs/lucide-react.mjs +96 -76
  12. package/.output/server/_ssr/CompareDrawer-ftkJxyk6.mjs +1040 -0
  13. package/.output/server/_ssr/ReplayDialog-DcmE3lj5.mjs +321 -0
  14. package/.output/server/_ssr/RequestAnatomy-rK_LNMdG.mjs +351 -0
  15. package/.output/server/_ssr/ResponseView-CbQ4n-aJ.mjs +601 -0
  16. package/.output/server/_ssr/StreamingChunkSequence-84FZkIzv.mjs +301 -0
  17. package/.output/server/_ssr/{index-CjvQZBI0.mjs → index-CDjLoMsk.mjs} +1036 -2352
  18. package/.output/server/_ssr/index.mjs +2 -2
  19. package/.output/server/_ssr/json-viewer-B-qpM5xC.mjs +510 -0
  20. package/.output/server/_ssr/{router-CO9_4CVh.mjs → router-BrdjOUEW.mjs} +24 -14
  21. package/.output/server/{_tanstack-start-manifest_v-D-9SW7K3.mjs → _tanstack-start-manifest_v-DmOZEcJ3.mjs} +1 -1
  22. package/.output/server/index.mjs +72 -30
  23. package/package.json +1 -1
  24. package/src/components/OnboardingBanner.tsx +2 -2
  25. package/src/components/ProxyViewer.tsx +38 -26
  26. package/src/components/ProxyViewerContainer.tsx +3 -24
  27. package/src/components/proxy-viewer/CompareDrawer.tsx +6 -6
  28. package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
  29. package/src/components/proxy-viewer/ConversationHeader.tsx +4 -1
  30. package/src/components/proxy-viewer/LogEntry.tsx +230 -163
  31. package/src/components/proxy-viewer/LogEntryHeader.tsx +134 -36
  32. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +1 -1
  33. package/src/components/proxy-viewer/ThreadConnector.tsx +17 -2
  34. package/src/components/proxy-viewer/TurnGroup.tsx +94 -71
  35. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +98 -0
  36. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +196 -0
  37. package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +53 -0
  38. package/src/components/proxy-viewer/anatomy/types.ts +39 -0
  39. package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +114 -0
  40. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +4 -24
  41. package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +21 -0
  42. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +6 -4
  43. package/src/components/proxy-viewer/lazy.ts +37 -0
  44. package/src/components/proxy-viewer/log-formats/anthropic.ts +146 -0
  45. package/src/components/proxy-viewer/log-formats/openai.ts +127 -0
  46. package/src/components/proxy-viewer/log-formats/types.ts +7 -0
  47. package/src/components/proxy-viewer/log-formats/unknown.ts +4 -0
  48. package/src/components/proxy-viewer/logEntryVisibility.ts +39 -0
  49. package/src/components/proxy-viewer/useKeyboardNavigation.ts +190 -0
  50. package/src/components/proxy-viewer/viewerState.ts +8 -0
  51. package/src/components/ui/crab-variants.tsx +11 -0
  52. package/src/components/ui/json-expansion-button.tsx +56 -0
  53. package/src/components/ui/json-viewer-bulk.ts +97 -0
  54. package/src/components/ui/json-viewer.tsx +129 -118
  55. package/src/lib/utils.ts +2 -3
  56. package/src/routes/api/logs.stream.ts +26 -16
  57. package/.output/public/assets/index-DRRCmu5p.css +0 -1
  58. package/.output/public/assets/index-DfjhkDNi.js +0 -107
@@ -1,20 +1,40 @@
1
- import { Check, Copy, GitCompareArrows, RotateCcw } from "lucide-react";
2
- import type { JSX } from "react";
1
+ import { Check, Copy, GitCompareArrows } from "lucide-react";
2
+ import { Suspense, type JSX } from "react";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState, memo } from "react";
4
- import { cn } from "../../lib/utils";
5
4
  import type { CapturedLog } from "../../proxy/schemas";
6
5
  import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
7
6
  import { Button } from "../ui/button";
8
7
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
9
- import { JsonViewerFromString } from "../ui/json-viewer";
8
+ import { JsonExpansionButton } from "../ui/json-expansion-button";
9
+ import { useJsonBulkExpansion } from "../ui/json-viewer-bulk";
10
10
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
11
+ import {
12
+ LazyJsonViewer,
13
+ LazyJsonViewerFromString,
14
+ LazyReplayDialog,
15
+ LazyRequestAnatomy,
16
+ LazyResponseView,
17
+ LazyStreamingChunkSequence,
18
+ } from "./lazy";
19
+ import type { AnatomySegment } from "./anatomy/types";
20
+ import { useAnatomyJump } from "./anatomy/useAnatomyJump";
11
21
  import { computeHeadersDiff, computeRequestDiff, DiffView } from "./diff";
12
22
  import { LogEntryHeader } from "./LogEntryHeader";
13
- import { ReplayDialog } from "./ReplayDialog";
14
- import { ResponseView } from "./ResponseView";
15
- import { StreamingChunkSequence } from "./StreamingChunkSequence";
16
23
  import type { CacheTrendEntry } from "./cacheTrend";
17
24
  import { getLogFormatAdapter, resolveLogFormat } from "./log-formats";
25
+ import {
26
+ shouldShowHeadersDiffButton,
27
+ shouldShowRawRequestTab,
28
+ shouldShowRequestDiffButton,
29
+ } from "./logEntryVisibility";
30
+
31
+ /**
32
+ * Lightweight fallback for lazy-loaded tabs. Renders an empty 1px-tall row so
33
+ * the layout doesn't jump while the chunk is fetching.
34
+ */
35
+ function TabFallback(): JSX.Element {
36
+ return <div className="h-1" aria-hidden="true" />;
37
+ }
18
38
 
19
39
  export type LogEntryProps = {
20
40
  log: CapturedLog;
@@ -34,46 +54,6 @@ export type LogEntryProps = {
34
54
  onCompareWithPrevious?: (log: CapturedLog) => void;
35
55
  };
36
56
 
37
- /**
38
- * Pure visibility rule for the "Raw Request" tab. Extracted so it can be
39
- * unit-tested without rendering React. The tab appears only when all three
40
- * are true: anthropic format, full view mode, strip toggle enabled.
41
- */
42
- export function shouldShowRawRequestTab(
43
- apiFormat: string,
44
- viewMode: "simple" | "full",
45
- strip: boolean,
46
- ): boolean {
47
- return apiFormat === "anthropic" && viewMode === "full" && strip;
48
- }
49
-
50
- /**
51
- * Pure visibility rule for the "Diff with Raw" button in the Headers tab.
52
- * The button only makes sense when the user is in full mode (where the
53
- * `Raw Headers` tab is shown) and we actually captured raw headers.
54
- */
55
- export function shouldShowHeadersDiffButton(
56
- viewMode: "simple" | "full",
57
- hasRawHeaders: boolean,
58
- ): boolean {
59
- return viewMode === "full" && hasRawHeaders;
60
- }
61
-
62
- /**
63
- * Pure visibility rule for the "Diff with Raw" button in the Request tab.
64
- * Mirrors the conditions for the `Raw Request` tab itself: full mode plus
65
- * the strip toggle being on for an anthropic-format request. We also need
66
- * an actual raw request body to diff against.
67
- */
68
- export function shouldShowRequestDiffButton(
69
- apiFormat: string,
70
- viewMode: "simple" | "full",
71
- strip: boolean,
72
- hasRawRequest: boolean,
73
- ): boolean {
74
- return apiFormat === "anthropic" && viewMode === "full" && strip && hasRawRequest;
75
- }
76
-
77
57
  function CopyButton({
78
58
  text,
79
59
  label,
@@ -87,23 +67,25 @@ function CopyButton({
87
67
  }): JSX.Element | null {
88
68
  if (text === null) return null;
89
69
  return (
90
- <button
91
- type="button"
92
- onClick={onCopy}
93
- className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded hover:bg-muted"
94
- >
95
- {copied ? (
96
- <>
97
- <Check className="size-3 text-green-500" />
98
- <span className="text-green-500">Copied!</span>
99
- </>
100
- ) : (
101
- <>
102
- <Copy className="size-3" />
103
- <span>{label}</span>
104
- </>
105
- )}
106
- </button>
70
+ <Tooltip>
71
+ <TooltipTrigger asChild>
72
+ <Button
73
+ variant="outline"
74
+ size="sm"
75
+ className="h-8 text-xs"
76
+ onClick={onCopy}
77
+ aria-label={label}
78
+ >
79
+ {copied ? (
80
+ <Check className="size-3.5 mr-1 text-emerald-500" />
81
+ ) : (
82
+ <Copy className="size-3.5 mr-1" />
83
+ )}
84
+ {copied ? "Copied!" : label}
85
+ </Button>
86
+ </TooltipTrigger>
87
+ <TooltipContent>{copied ? "Copied to clipboard" : label}</TooltipContent>
88
+ </Tooltip>
107
89
  );
108
90
  }
109
91
 
@@ -117,20 +99,16 @@ function DiffToggleButton({
117
99
  return (
118
100
  <Tooltip>
119
101
  <TooltipTrigger asChild>
120
- <button
121
- type="button"
102
+ <Button
103
+ variant={active ? "default" : "outline"}
104
+ size="sm"
105
+ className="h-8 text-xs"
122
106
  onClick={onClick}
123
107
  aria-pressed={active}
124
- className={cn(
125
- "flex items-center gap-1.5 text-xs px-2 py-1 rounded transition-colors",
126
- active
127
- ? "bg-primary/10 text-primary"
128
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
129
- )}
130
108
  >
131
- <GitCompareArrows className="size-3" />
109
+ <GitCompareArrows className="size-3.5 mr-1" />
132
110
  {active ? "Showing diff" : "Diff with Raw"}
133
- </button>
111
+ </Button>
134
112
  </TooltipTrigger>
135
113
  <TooltipContent>
136
114
  {active ? "Hide diff view" : "Compare proxy output against the original raw version"}
@@ -210,6 +188,8 @@ export const LogEntry = memo(function ({
210
188
  const [headersDiff, setHeadersDiff] = useState<boolean>(false);
211
189
  const [requestDiff, setRequestDiff] = useState<boolean>(false);
212
190
  const [activeTab, setActiveTab] = useState("request");
191
+ const [expandToPath, setExpandToPath] = useState<string | null>(null);
192
+ const requestJsonRef = useRef<HTMLDivElement | null>(null);
213
193
  const resolvedFormat = resolveLogFormat(log);
214
194
  const adapter = getLogFormatAdapter(resolvedFormat);
215
195
  const requestAnalysis = useMemo(
@@ -230,6 +210,26 @@ export const LogEntry = memo(function ({
230
210
  const requestCopy = useCopyFeedback(displayedRequestBody);
231
211
  const rawRequestCopy = useCopyFeedback(log.rawRequestBody);
232
212
  const responseCopy = useCopyFeedback(log.responseText);
213
+ const requestExpansion = useJsonBulkExpansion(displayedRequestBody);
214
+ const rawRequestExpansion = useJsonBulkExpansion(log.rawRequestBody);
215
+ const anatomySegments = useMemo(
216
+ () =>
217
+ requestExpansion.parsedData !== null
218
+ ? adapter.anatomySegments(requestExpansion.parsedData)
219
+ : null,
220
+ [adapter, requestExpansion.parsedData],
221
+ );
222
+ const anatomyPaths = useMemo(() => {
223
+ if (anatomySegments === null) return undefined;
224
+ return new Set(anatomySegments.map((s) => s.path));
225
+ }, [anatomySegments]);
226
+ const jumpToAnatomySegment = useAnatomyJump({
227
+ containerRef: requestJsonRef,
228
+ setExpandToPath,
229
+ ensureTabActive: () => {
230
+ if (activeTab !== "request") setActiveTab("request");
231
+ },
232
+ });
233
233
 
234
234
  return (
235
235
  <TooltipProvider>
@@ -242,6 +242,30 @@ export const LogEntry = memo(function ({
242
242
  onToggle={() => setExpanded(!expanded)}
243
243
  responseToolNames={responseAnalysis.toolNames}
244
244
  cacheTrend={cacheTrend}
245
+ onReplay={
246
+ onCompareWithPrevious === undefined
247
+ ? undefined
248
+ : () => {
249
+ setReplayOpen(true);
250
+ }
251
+ }
252
+ onCopyRequest={
253
+ displayedRequestBody === null
254
+ ? undefined
255
+ : (e) => {
256
+ requestCopy.copy(e);
257
+ }
258
+ }
259
+ requestCopied={requestCopy.copied}
260
+ onToggleRequestExpansion={requestExpansion.toggle}
261
+ requestExpansionState={
262
+ requestExpansion.policy === null
263
+ ? null
264
+ : {
265
+ isExpanded: requestExpansion.isExpanded,
266
+ isPending: requestExpansion.isPending,
267
+ }
268
+ }
245
269
  />
246
270
 
247
271
  {expanded && (
@@ -254,6 +278,7 @@ export const LogEntry = memo(function ({
254
278
  <TabsTrigger value="raw-request">Raw Request</TabsTrigger>
255
279
  )}
256
280
  <TabsTrigger value="request">Request</TabsTrigger>
281
+ {anatomySegments !== null && <TabsTrigger value="anatomy">Anatomy</TabsTrigger>}
257
282
  {viewMode === "full" && <TabsTrigger value="raw">Raw Response</TabsTrigger>}
258
283
  <TabsTrigger value="parsed">Response</TabsTrigger>
259
284
  </TabsList>
@@ -261,19 +286,35 @@ export const LogEntry = memo(function ({
261
286
  {shouldShowRawRequestTab(resolvedFormat, viewMode, strip) && (
262
287
  <TabsContent value="raw-request">
263
288
  {activeTab === "raw-request" && (
264
- <div className="px-4 py-3">
265
- <div className="flex justify-end mb-2">
289
+ <div className="px-4 pt-1 pb-3">
290
+ <div className="flex justify-end gap-2 mb-2">
291
+ <JsonExpansionButton
292
+ policy={rawRequestExpansion.policy}
293
+ isExpanded={rawRequestExpansion.isExpanded}
294
+ isPending={rawRequestExpansion.isPending}
295
+ onToggle={rawRequestExpansion.toggle}
296
+ />
266
297
  <CopyButton
267
298
  text={log.rawRequestBody}
268
- label="Copy Raw Request"
299
+ label="Copy"
269
300
  copied={rawRequestCopy.copied}
270
301
  onCopy={rawRequestCopy.copy}
271
302
  />
272
303
  </div>
273
- {log.rawRequestBody !== null ? (
274
- <JsonViewerFromString text={log.rawRequestBody} defaultExpandDepth={1} />
275
- ) : (
304
+ {log.rawRequestBody === null ? (
276
305
  <p className="text-xs text-muted-foreground italic">No request body</p>
306
+ ) : rawRequestExpansion.parsedData !== null ? (
307
+ <Suspense fallback={<TabFallback />}>
308
+ <LazyJsonViewer
309
+ data={rawRequestExpansion.parsedData}
310
+ bulkDepth={rawRequestExpansion.bulkDepth}
311
+ bulkRevision={rawRequestExpansion.bulkRevision}
312
+ />
313
+ </Suspense>
314
+ ) : (
315
+ <pre className="font-mono text-xs whitespace-pre-wrap break-words">
316
+ {log.rawRequestBody}
317
+ </pre>
277
318
  )}
278
319
  </div>
279
320
  )}
@@ -282,86 +323,104 @@ export const LogEntry = memo(function ({
282
323
 
283
324
  <TabsContent value="request">
284
325
  {activeTab === "request" && (
285
- <div className="px-4 py-3">
286
- <div className="flex justify-end gap-2 mb-2">
287
- {shouldShowRequestDiffButton(
288
- resolvedFormat,
289
- viewMode,
290
- strip,
291
- log.rawRequestBody !== null,
292
- ) && (
293
- <DiffToggleButton
294
- active={requestDiff}
295
- onClick={(e) => {
296
- e.stopPropagation();
297
- setRequestDiff(!requestDiff);
298
- }}
299
- />
300
- )}
301
- {onCompareWithPrevious !== undefined && (
302
- <Tooltip>
303
- <TooltipTrigger asChild>
304
- <Button
305
- variant="outline"
306
- size="sm"
307
- className="h-7 text-xs"
308
- onClick={(e) => {
309
- e.stopPropagation();
310
- onCompareWithPrevious(log);
311
- }}
312
- >
313
- <GitCompareArrows className="size-3 mr-1" />
314
- Diff with Previous
315
- </Button>
316
- </TooltipTrigger>
317
- <TooltipContent>
318
- Compare this request with the immediately preceding one
319
- </TooltipContent>
320
- </Tooltip>
321
- )}
322
- <Tooltip>
323
- <TooltipTrigger asChild>
324
- <Button
325
- variant="outline"
326
- size="sm"
327
- className="h-7 text-xs"
326
+ <div className="px-4 pt-1 pb-3">
327
+ {/* Per-tab secondary actions (diff toggles, compare-with-previous).
328
+ Replay / Copy / Expand-all JSON now live in the header. */}
329
+ {(shouldShowRequestDiffButton(
330
+ resolvedFormat,
331
+ viewMode,
332
+ strip,
333
+ log.rawRequestBody !== null,
334
+ ) ||
335
+ onCompareWithPrevious !== undefined) && (
336
+ <div className="flex justify-end gap-2 mb-2">
337
+ {shouldShowRequestDiffButton(
338
+ resolvedFormat,
339
+ viewMode,
340
+ strip,
341
+ log.rawRequestBody !== null,
342
+ ) && (
343
+ <DiffToggleButton
344
+ active={requestDiff}
328
345
  onClick={(e) => {
329
346
  e.stopPropagation();
330
- setReplayOpen(true);
347
+ setRequestDiff(!requestDiff);
331
348
  }}
332
- >
333
- <RotateCcw className="size-3 mr-1" />
334
- Replay
335
- </Button>
336
- </TooltipTrigger>
337
- <TooltipContent>Re-send this request to the provider</TooltipContent>
338
- </Tooltip>
339
- <CopyButton
340
- text={displayedRequestBody}
341
- label="Copy Request"
342
- copied={requestCopy.copied}
343
- onCopy={requestCopy.copy}
344
- />
345
- </div>
349
+ />
350
+ )}
351
+ {onCompareWithPrevious !== undefined && (
352
+ <Tooltip>
353
+ <TooltipTrigger asChild>
354
+ <Button
355
+ variant="outline"
356
+ size="sm"
357
+ className="h-8 text-xs"
358
+ onClick={(e) => {
359
+ e.stopPropagation();
360
+ onCompareWithPrevious(log);
361
+ }}
362
+ >
363
+ <GitCompareArrows className="size-3 mr-1" />
364
+ Diff with Previous
365
+ </Button>
366
+ </TooltipTrigger>
367
+ <TooltipContent>
368
+ Compare this request with the immediately preceding one
369
+ </TooltipContent>
370
+ </Tooltip>
371
+ )}
372
+ </div>
373
+ )}
346
374
  {requestDiff ? (
347
375
  <RequestDiffContent
348
376
  rawBody={log.rawRequestBody}
349
377
  displayedBody={displayedRequestBody}
350
378
  emptyLabel="No transformation applied — raw and sent request bodies are identical."
351
379
  />
352
- ) : displayedRequestBody !== null ? (
353
- <JsonViewerFromString text={displayedRequestBody} defaultExpandDepth={1} />
354
380
  ) : (
355
- <p className="text-xs text-muted-foreground italic">No request body</p>
381
+ <div ref={requestJsonRef}>
382
+ {displayedRequestBody === null ? (
383
+ <p className="text-xs text-muted-foreground italic">No request body</p>
384
+ ) : requestExpansion.parsedData !== null ? (
385
+ <Suspense fallback={<TabFallback />}>
386
+ <LazyJsonViewer
387
+ data={requestExpansion.parsedData}
388
+ bulkDepth={requestExpansion.bulkDepth}
389
+ bulkRevision={requestExpansion.bulkRevision}
390
+ anatomyPaths={anatomyPaths}
391
+ expandToPath={expandToPath}
392
+ />
393
+ </Suspense>
394
+ ) : (
395
+ <pre className="font-mono text-xs whitespace-pre-wrap break-words">
396
+ {displayedRequestBody}
397
+ </pre>
398
+ )}
399
+ </div>
356
400
  )}
357
401
  </div>
358
402
  )}
359
403
  </TabsContent>
360
404
 
405
+ {anatomySegments !== null && (
406
+ <TabsContent value="anatomy">
407
+ {activeTab === "anatomy" && (
408
+ <Suspense fallback={<TabFallback />}>
409
+ <LazyRequestAnatomy
410
+ parsed={null}
411
+ inputTokens={log.inputTokens ?? null}
412
+ segments={anatomySegments}
413
+ onSegmentActivate={jumpToAnatomySegment}
414
+ />
415
+ </Suspense>
416
+ )}
417
+ </TabsContent>
418
+ )}
419
+
361
420
  {viewMode === "full" && (
362
421
  <TabsContent value="headers">
363
422
  {activeTab === "headers" && (
364
- <div className="px-4 py-3">
423
+ <div className="px-4 pt-1 pb-3">
365
424
  <div className="flex justify-end gap-2 mb-2">
366
425
  {shouldShowHeadersDiffButton(
367
426
  viewMode,
@@ -408,7 +467,7 @@ export const LogEntry = memo(function ({
408
467
  {viewMode === "full" && (
409
468
  <TabsContent value="raw-headers">
410
469
  {activeTab === "raw-headers" && (
411
- <div className="px-4 py-3">
470
+ <div className="px-4 pt-1 pb-3">
412
471
  {log.rawHeaders && Object.keys(log.rawHeaders).length > 0 ? (
413
472
  <div className="space-y-1 font-mono text-xs">
414
473
  {Object.entries(log.rawHeaders)
@@ -436,7 +495,7 @@ export const LogEntry = memo(function ({
436
495
 
437
496
  <TabsContent value="raw">
438
497
  {activeTab === "raw" && (
439
- <div className="px-4 py-3 space-y-3">
498
+ <div className="px-4 pt-1 pb-3 space-y-3">
440
499
  {log.error !== undefined && log.error !== null && (
441
500
  <div className="rounded border border-destructive/50 bg-destructive/10 p-3 text-xs">
442
501
  <div className="font-semibold text-destructive mb-1">SSE Error</div>
@@ -446,21 +505,25 @@ export const LogEntry = memo(function ({
446
505
  <div className="flex justify-end">
447
506
  <CopyButton
448
507
  text={log.responseText}
449
- label="Copy Response"
508
+ label="Copy"
450
509
  copied={responseCopy.copied}
451
510
  onCopy={responseCopy.copy}
452
511
  />
453
512
  </div>
454
513
  {log.responseText !== null ? (
455
- <JsonViewerFromString text={log.responseText} defaultExpandDepth={1} />
514
+ <Suspense fallback={<TabFallback />}>
515
+ <LazyJsonViewerFromString text={log.responseText} defaultExpandDepth={0} />
516
+ </Suspense>
456
517
  ) : (
457
518
  <p className="text-xs text-muted-foreground italic">No response</p>
458
519
  )}
459
520
  {log.streaming === true && (
460
- <StreamingChunkSequence
461
- logId={log.id}
462
- truncated={log.streamingChunksPath !== null}
463
- />
521
+ <Suspense fallback={<TabFallback />}>
522
+ <LazyStreamingChunkSequence
523
+ logId={log.id}
524
+ truncated={log.streamingChunksPath !== null}
525
+ />
526
+ </Suspense>
464
527
  )}
465
528
  </div>
466
529
  )}
@@ -468,18 +531,20 @@ export const LogEntry = memo(function ({
468
531
 
469
532
  <TabsContent value="parsed">
470
533
  {activeTab === "parsed" && (
471
- <div className="px-4 py-3">
472
- <ResponseView
473
- responseText={log.responseText}
474
- responseStatus={log.responseStatus}
475
- streaming={log.streaming}
476
- inputTokens={log.inputTokens}
477
- outputTokens={log.outputTokens}
478
- cacheCreationInputTokens={log.cacheCreationInputTokens}
479
- cacheReadInputTokens={log.cacheReadInputTokens}
480
- apiFormat={resolvedFormat}
481
- error={log.error}
482
- />
534
+ <div className="px-4 pt-1 pb-3">
535
+ <Suspense fallback={<TabFallback />}>
536
+ <LazyResponseView
537
+ responseText={log.responseText}
538
+ responseStatus={log.responseStatus}
539
+ streaming={log.streaming}
540
+ inputTokens={log.inputTokens}
541
+ outputTokens={log.outputTokens}
542
+ cacheCreationInputTokens={log.cacheCreationInputTokens}
543
+ cacheReadInputTokens={log.cacheReadInputTokens}
544
+ apiFormat={resolvedFormat}
545
+ error={log.error}
546
+ />
547
+ </Suspense>
483
548
  </div>
484
549
  )}
485
550
  </TabsContent>
@@ -487,7 +552,9 @@ export const LogEntry = memo(function ({
487
552
  </div>
488
553
  )}
489
554
  </div>
490
- <ReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
555
+ <Suspense fallback={null}>
556
+ <LazyReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
557
+ </Suspense>
491
558
  </TooltipProvider>
492
559
  );
493
560
  });