@tonyclaw/llm-inspector 1.16.4 → 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 (56) 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-DbWwVQFh.js → main-CDMdNDY_.js} +1 -1
  11. package/.output/server/_libs/lucide-react.mjs +104 -84
  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-C-z-fZtq.mjs → index-CDjLoMsk.mjs} +1026 -2455
  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-CNM9Kbi0.mjs → router-BrdjOUEW.mjs} +24 -14
  21. package/.output/server/{_tanstack-start-manifest_v-BWfLeIsC.mjs → _tanstack-start-manifest_v-DmOZEcJ3.mjs} +1 -1
  22. package/.output/server/index.mjs +68 -26
  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/ConversationGroup.tsx +1 -1
  28. package/src/components/proxy-viewer/ConversationHeader.tsx +4 -1
  29. package/src/components/proxy-viewer/LogEntry.tsx +213 -181
  30. package/src/components/proxy-viewer/LogEntryHeader.tsx +134 -36
  31. package/src/components/proxy-viewer/ThreadConnector.tsx +17 -2
  32. package/src/components/proxy-viewer/TurnGroup.tsx +94 -71
  33. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +98 -0
  34. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +196 -0
  35. package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +53 -0
  36. package/src/components/proxy-viewer/anatomy/types.ts +39 -0
  37. package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +114 -0
  38. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +3 -23
  39. package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +21 -0
  40. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +5 -3
  41. package/src/components/proxy-viewer/lazy.ts +37 -0
  42. package/src/components/proxy-viewer/log-formats/anthropic.ts +146 -0
  43. package/src/components/proxy-viewer/log-formats/openai.ts +127 -0
  44. package/src/components/proxy-viewer/log-formats/types.ts +7 -0
  45. package/src/components/proxy-viewer/log-formats/unknown.ts +4 -0
  46. package/src/components/proxy-viewer/logEntryVisibility.ts +39 -0
  47. package/src/components/proxy-viewer/useKeyboardNavigation.ts +190 -0
  48. package/src/components/proxy-viewer/viewerState.ts +8 -0
  49. package/src/components/ui/crab-variants.tsx +11 -0
  50. package/src/components/ui/json-expansion-button.tsx +56 -0
  51. package/src/components/ui/json-viewer-bulk.ts +97 -0
  52. package/src/components/ui/json-viewer.tsx +58 -183
  53. package/src/lib/utils.ts +2 -3
  54. package/src/routes/api/logs.stream.ts +26 -16
  55. package/.output/public/assets/index-DRRCmu5p.css +0 -1
  56. package/.output/public/assets/index-X7CHS7fS.js +0 -107
@@ -1,25 +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 {
10
- JsonViewer,
11
- JsonViewerFromString,
12
- JsonExpansionButton,
13
- useJsonBulkExpansion,
14
- } from "../ui/json-viewer";
8
+ import { JsonExpansionButton } from "../ui/json-expansion-button";
9
+ import { useJsonBulkExpansion } from "../ui/json-viewer-bulk";
15
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";
16
21
  import { computeHeadersDiff, computeRequestDiff, DiffView } from "./diff";
17
22
  import { LogEntryHeader } from "./LogEntryHeader";
18
- import { ReplayDialog } from "./ReplayDialog";
19
- import { ResponseView } from "./ResponseView";
20
- import { StreamingChunkSequence } from "./StreamingChunkSequence";
21
23
  import type { CacheTrendEntry } from "./cacheTrend";
22
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
+ }
23
38
 
24
39
  export type LogEntryProps = {
25
40
  log: CapturedLog;
@@ -39,46 +54,6 @@ export type LogEntryProps = {
39
54
  onCompareWithPrevious?: (log: CapturedLog) => void;
40
55
  };
41
56
 
42
- /**
43
- * Pure visibility rule for the "Raw Request" tab. Extracted so it can be
44
- * unit-tested without rendering React. The tab appears only when all three
45
- * are true: anthropic format, full view mode, strip toggle enabled.
46
- */
47
- export function shouldShowRawRequestTab(
48
- apiFormat: string,
49
- viewMode: "simple" | "full",
50
- strip: boolean,
51
- ): boolean {
52
- return apiFormat === "anthropic" && viewMode === "full" && strip;
53
- }
54
-
55
- /**
56
- * Pure visibility rule for the "Diff with Raw" button in the Headers tab.
57
- * The button only makes sense when the user is in full mode (where the
58
- * `Raw Headers` tab is shown) and we actually captured raw headers.
59
- */
60
- export function shouldShowHeadersDiffButton(
61
- viewMode: "simple" | "full",
62
- hasRawHeaders: boolean,
63
- ): boolean {
64
- return viewMode === "full" && hasRawHeaders;
65
- }
66
-
67
- /**
68
- * Pure visibility rule for the "Diff with Raw" button in the Request tab.
69
- * Mirrors the conditions for the `Raw Request` tab itself: full mode plus
70
- * the strip toggle being on for an anthropic-format request. We also need
71
- * an actual raw request body to diff against.
72
- */
73
- export function shouldShowRequestDiffButton(
74
- apiFormat: string,
75
- viewMode: "simple" | "full",
76
- strip: boolean,
77
- hasRawRequest: boolean,
78
- ): boolean {
79
- return apiFormat === "anthropic" && viewMode === "full" && strip && hasRawRequest;
80
- }
81
-
82
57
  function CopyButton({
83
58
  text,
84
59
  label,
@@ -92,23 +67,25 @@ function CopyButton({
92
67
  }): JSX.Element | null {
93
68
  if (text === null) return null;
94
69
  return (
95
- <button
96
- type="button"
97
- onClick={onCopy}
98
- 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"
99
- >
100
- {copied ? (
101
- <>
102
- <Check className="size-3 text-green-500" />
103
- <span className="text-green-500">Copied!</span>
104
- </>
105
- ) : (
106
- <>
107
- <Copy className="size-3" />
108
- <span>{label}</span>
109
- </>
110
- )}
111
- </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>
112
89
  );
113
90
  }
114
91
 
@@ -122,20 +99,16 @@ function DiffToggleButton({
122
99
  return (
123
100
  <Tooltip>
124
101
  <TooltipTrigger asChild>
125
- <button
126
- type="button"
102
+ <Button
103
+ variant={active ? "default" : "outline"}
104
+ size="sm"
105
+ className="h-8 text-xs"
127
106
  onClick={onClick}
128
107
  aria-pressed={active}
129
- className={cn(
130
- "flex items-center gap-1.5 text-xs px-2 py-1 rounded transition-colors",
131
- active
132
- ? "bg-primary/10 text-primary"
133
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
134
- )}
135
108
  >
136
- <GitCompareArrows className="size-3" />
109
+ <GitCompareArrows className="size-3.5 mr-1" />
137
110
  {active ? "Showing diff" : "Diff with Raw"}
138
- </button>
111
+ </Button>
139
112
  </TooltipTrigger>
140
113
  <TooltipContent>
141
114
  {active ? "Hide diff view" : "Compare proxy output against the original raw version"}
@@ -215,6 +188,8 @@ export const LogEntry = memo(function ({
215
188
  const [headersDiff, setHeadersDiff] = useState<boolean>(false);
216
189
  const [requestDiff, setRequestDiff] = useState<boolean>(false);
217
190
  const [activeTab, setActiveTab] = useState("request");
191
+ const [expandToPath, setExpandToPath] = useState<string | null>(null);
192
+ const requestJsonRef = useRef<HTMLDivElement | null>(null);
218
193
  const resolvedFormat = resolveLogFormat(log);
219
194
  const adapter = getLogFormatAdapter(resolvedFormat);
220
195
  const requestAnalysis = useMemo(
@@ -237,6 +212,24 @@ export const LogEntry = memo(function ({
237
212
  const responseCopy = useCopyFeedback(log.responseText);
238
213
  const requestExpansion = useJsonBulkExpansion(displayedRequestBody);
239
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
+ });
240
233
 
241
234
  return (
242
235
  <TooltipProvider>
@@ -249,6 +242,30 @@ export const LogEntry = memo(function ({
249
242
  onToggle={() => setExpanded(!expanded)}
250
243
  responseToolNames={responseAnalysis.toolNames}
251
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
+ }
252
269
  />
253
270
 
254
271
  {expanded && (
@@ -261,6 +278,7 @@ export const LogEntry = memo(function ({
261
278
  <TabsTrigger value="raw-request">Raw Request</TabsTrigger>
262
279
  )}
263
280
  <TabsTrigger value="request">Request</TabsTrigger>
281
+ {anatomySegments !== null && <TabsTrigger value="anatomy">Anatomy</TabsTrigger>}
264
282
  {viewMode === "full" && <TabsTrigger value="raw">Raw Response</TabsTrigger>}
265
283
  <TabsTrigger value="parsed">Response</TabsTrigger>
266
284
  </TabsList>
@@ -268,7 +286,7 @@ export const LogEntry = memo(function ({
268
286
  {shouldShowRawRequestTab(resolvedFormat, viewMode, strip) && (
269
287
  <TabsContent value="raw-request">
270
288
  {activeTab === "raw-request" && (
271
- <div className="px-4 py-3">
289
+ <div className="px-4 pt-1 pb-3">
272
290
  <div className="flex justify-end gap-2 mb-2">
273
291
  <JsonExpansionButton
274
292
  policy={rawRequestExpansion.policy}
@@ -286,11 +304,13 @@ export const LogEntry = memo(function ({
286
304
  {log.rawRequestBody === null ? (
287
305
  <p className="text-xs text-muted-foreground italic">No request body</p>
288
306
  ) : rawRequestExpansion.parsedData !== null ? (
289
- <JsonViewer
290
- data={rawRequestExpansion.parsedData}
291
- bulkDepth={rawRequestExpansion.bulkDepth}
292
- bulkRevision={rawRequestExpansion.bulkRevision}
293
- />
307
+ <Suspense fallback={<TabFallback />}>
308
+ <LazyJsonViewer
309
+ data={rawRequestExpansion.parsedData}
310
+ bulkDepth={rawRequestExpansion.bulkDepth}
311
+ bulkRevision={rawRequestExpansion.bulkRevision}
312
+ />
313
+ </Suspense>
294
314
  ) : (
295
315
  <pre className="font-mono text-xs whitespace-pre-wrap break-words">
296
316
  {log.rawRequestBody}
@@ -303,100 +323,104 @@ export const LogEntry = memo(function ({
303
323
 
304
324
  <TabsContent value="request">
305
325
  {activeTab === "request" && (
306
- <div className="px-4 py-3">
307
- <div className="flex justify-end gap-2 mb-2">
308
- {shouldShowRequestDiffButton(
309
- resolvedFormat,
310
- viewMode,
311
- strip,
312
- log.rawRequestBody !== null,
313
- ) && (
314
- <DiffToggleButton
315
- active={requestDiff}
316
- onClick={(e) => {
317
- e.stopPropagation();
318
- setRequestDiff(!requestDiff);
319
- }}
320
- />
321
- )}
322
- {onCompareWithPrevious !== undefined && (
323
- <Tooltip>
324
- <TooltipTrigger asChild>
325
- <Button
326
- variant="outline"
327
- size="sm"
328
- className="h-7 text-xs"
329
- onClick={(e) => {
330
- e.stopPropagation();
331
- onCompareWithPrevious(log);
332
- }}
333
- >
334
- <GitCompareArrows className="size-3 mr-1" />
335
- Diff with Previous
336
- </Button>
337
- </TooltipTrigger>
338
- <TooltipContent>
339
- Compare this request with the immediately preceding one
340
- </TooltipContent>
341
- </Tooltip>
342
- )}
343
- <Tooltip>
344
- <TooltipTrigger asChild>
345
- <Button
346
- variant="outline"
347
- size="sm"
348
- 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}
349
345
  onClick={(e) => {
350
346
  e.stopPropagation();
351
- setReplayOpen(true);
347
+ setRequestDiff(!requestDiff);
352
348
  }}
353
- >
354
- <RotateCcw className="size-3 mr-1" />
355
- Replay
356
- </Button>
357
- </TooltipTrigger>
358
- <TooltipContent>Re-send this request to the provider</TooltipContent>
359
- </Tooltip>
360
- <JsonExpansionButton
361
- policy={requestExpansion.policy}
362
- isExpanded={requestExpansion.isExpanded}
363
- isPending={requestExpansion.isPending}
364
- onToggle={requestExpansion.toggle}
365
- />
366
- <CopyButton
367
- text={displayedRequestBody}
368
- label="Copy"
369
- copied={requestCopy.copied}
370
- onCopy={requestCopy.copy}
371
- />
372
- </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
+ )}
373
374
  {requestDiff ? (
374
375
  <RequestDiffContent
375
376
  rawBody={log.rawRequestBody}
376
377
  displayedBody={displayedRequestBody}
377
378
  emptyLabel="No transformation applied — raw and sent request bodies are identical."
378
379
  />
379
- ) : displayedRequestBody === null ? (
380
- <p className="text-xs text-muted-foreground italic">No request body</p>
381
- ) : requestExpansion.parsedData !== null ? (
382
- <JsonViewer
383
- data={requestExpansion.parsedData}
384
- bulkDepth={requestExpansion.bulkDepth}
385
- bulkRevision={requestExpansion.bulkRevision}
386
- />
387
380
  ) : (
388
- <pre className="font-mono text-xs whitespace-pre-wrap break-words">
389
- {displayedRequestBody}
390
- </pre>
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>
391
400
  )}
392
401
  </div>
393
402
  )}
394
403
  </TabsContent>
395
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
+
396
420
  {viewMode === "full" && (
397
421
  <TabsContent value="headers">
398
422
  {activeTab === "headers" && (
399
- <div className="px-4 py-3">
423
+ <div className="px-4 pt-1 pb-3">
400
424
  <div className="flex justify-end gap-2 mb-2">
401
425
  {shouldShowHeadersDiffButton(
402
426
  viewMode,
@@ -443,7 +467,7 @@ export const LogEntry = memo(function ({
443
467
  {viewMode === "full" && (
444
468
  <TabsContent value="raw-headers">
445
469
  {activeTab === "raw-headers" && (
446
- <div className="px-4 py-3">
470
+ <div className="px-4 pt-1 pb-3">
447
471
  {log.rawHeaders && Object.keys(log.rawHeaders).length > 0 ? (
448
472
  <div className="space-y-1 font-mono text-xs">
449
473
  {Object.entries(log.rawHeaders)
@@ -471,7 +495,7 @@ export const LogEntry = memo(function ({
471
495
 
472
496
  <TabsContent value="raw">
473
497
  {activeTab === "raw" && (
474
- <div className="px-4 py-3 space-y-3">
498
+ <div className="px-4 pt-1 pb-3 space-y-3">
475
499
  {log.error !== undefined && log.error !== null && (
476
500
  <div className="rounded border border-destructive/50 bg-destructive/10 p-3 text-xs">
477
501
  <div className="font-semibold text-destructive mb-1">SSE Error</div>
@@ -487,15 +511,19 @@ export const LogEntry = memo(function ({
487
511
  />
488
512
  </div>
489
513
  {log.responseText !== null ? (
490
- <JsonViewerFromString text={log.responseText} defaultExpandDepth={0} />
514
+ <Suspense fallback={<TabFallback />}>
515
+ <LazyJsonViewerFromString text={log.responseText} defaultExpandDepth={0} />
516
+ </Suspense>
491
517
  ) : (
492
518
  <p className="text-xs text-muted-foreground italic">No response</p>
493
519
  )}
494
520
  {log.streaming === true && (
495
- <StreamingChunkSequence
496
- logId={log.id}
497
- truncated={log.streamingChunksPath !== null}
498
- />
521
+ <Suspense fallback={<TabFallback />}>
522
+ <LazyStreamingChunkSequence
523
+ logId={log.id}
524
+ truncated={log.streamingChunksPath !== null}
525
+ />
526
+ </Suspense>
499
527
  )}
500
528
  </div>
501
529
  )}
@@ -503,18 +531,20 @@ export const LogEntry = memo(function ({
503
531
 
504
532
  <TabsContent value="parsed">
505
533
  {activeTab === "parsed" && (
506
- <div className="px-4 py-3">
507
- <ResponseView
508
- responseText={log.responseText}
509
- responseStatus={log.responseStatus}
510
- streaming={log.streaming}
511
- inputTokens={log.inputTokens}
512
- outputTokens={log.outputTokens}
513
- cacheCreationInputTokens={log.cacheCreationInputTokens}
514
- cacheReadInputTokens={log.cacheReadInputTokens}
515
- apiFormat={resolvedFormat}
516
- error={log.error}
517
- />
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>
518
548
  </div>
519
549
  )}
520
550
  </TabsContent>
@@ -522,7 +552,9 @@ export const LogEntry = memo(function ({
522
552
  </div>
523
553
  )}
524
554
  </div>
525
- <ReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
555
+ <Suspense fallback={null}>
556
+ <LazyReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
557
+ </Suspense>
526
558
  </TooltipProvider>
527
559
  );
528
560
  });