@tonyclaw/llm-inspector 1.16.4 → 1.17.0

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 (62) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-C4fie5g5.js +1 -0
  3. package/.output/public/assets/ReplayDialog-Dme5uOR9.js +1 -0
  4. package/.output/public/assets/RequestAnatomy-ChBLDNFH.js +1 -0
  5. package/.output/public/assets/ResponseView-wGeqBzVU.js +1 -0
  6. package/.output/public/assets/StreamingChunkSequence-zeJZQLqT.js +1 -0
  7. package/.output/public/assets/index-DoGvsnbA.css +1 -0
  8. package/.output/public/assets/index-DpbutOvo.js +101 -0
  9. package/.output/public/assets/json-viewer-BV-WUszW.js +14 -0
  10. package/.output/public/assets/{main-DbWwVQFh.js → main-DRu10KNQ.js} +1 -1
  11. package/.output/server/_libs/lucide-react.mjs +105 -85
  12. package/.output/server/_ssr/CompareDrawer-C4-CQL5w.mjs +1040 -0
  13. package/.output/server/_ssr/ReplayDialog-BTb1Bam8.mjs +321 -0
  14. package/.output/server/_ssr/RequestAnatomy-CZFV1IvL.mjs +351 -0
  15. package/.output/server/_ssr/ResponseView-CTZekh65.mjs +601 -0
  16. package/.output/server/_ssr/StreamingChunkSequence-C38Ynabd.mjs +301 -0
  17. package/.output/server/_ssr/{index-C-z-fZtq.mjs → index-Cnu-QzAy.mjs} +1141 -2443
  18. package/.output/server/_ssr/index.mjs +2 -2
  19. package/.output/server/_ssr/json-viewer-DROqpjS9.mjs +510 -0
  20. package/.output/server/_ssr/{router-CNM9Kbi0.mjs → router-pP4GCTQx.mjs} +42 -18
  21. package/.output/server/{_tanstack-start-manifest_v-BWfLeIsC.mjs → _tanstack-start-manifest_v-CphS4rZd.mjs} +1 -1
  22. package/.output/server/index.mjs +69 -27
  23. package/package.json +1 -1
  24. package/src/components/OnboardingBanner.tsx +2 -2
  25. package/src/components/ProxyViewer.tsx +44 -27
  26. package/src/components/ProxyViewerContainer.tsx +5 -25
  27. package/src/components/providers/SettingsDialog.tsx +52 -1
  28. package/src/components/proxy-viewer/ConversationGroup.tsx +5 -1
  29. package/src/components/proxy-viewer/ConversationHeader.tsx +4 -1
  30. package/src/components/proxy-viewer/LogEntry.tsx +217 -181
  31. package/src/components/proxy-viewer/LogEntryHeader.tsx +181 -40
  32. package/src/components/proxy-viewer/ThreadConnector.tsx +17 -2
  33. package/src/components/proxy-viewer/TurnGroup.tsx +124 -72
  34. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +98 -0
  35. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +196 -0
  36. package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +53 -0
  37. package/src/components/proxy-viewer/anatomy/types.ts +39 -0
  38. package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +114 -0
  39. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +3 -23
  40. package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +21 -0
  41. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +5 -3
  42. package/src/components/proxy-viewer/lazy.ts +37 -0
  43. package/src/components/proxy-viewer/log-formats/anthropic.ts +146 -0
  44. package/src/components/proxy-viewer/log-formats/openai.ts +127 -0
  45. package/src/components/proxy-viewer/log-formats/types.ts +7 -0
  46. package/src/components/proxy-viewer/log-formats/unknown.ts +4 -0
  47. package/src/components/proxy-viewer/logEntryVisibility.ts +39 -0
  48. package/src/components/proxy-viewer/useKeyboardNavigation.ts +190 -0
  49. package/src/components/proxy-viewer/viewerState.ts +8 -0
  50. package/src/components/ui/crab-variants.tsx +11 -0
  51. package/src/components/ui/json-expansion-button.tsx +56 -0
  52. package/src/components/ui/json-viewer-bulk.ts +97 -0
  53. package/src/components/ui/json-viewer.tsx +58 -183
  54. package/src/lib/runtimeConfig.ts +9 -0
  55. package/src/lib/useOnboarding.ts +7 -1
  56. package/src/lib/useStripConfig.ts +33 -2
  57. package/src/lib/utils.ts +2 -3
  58. package/src/proxy/config.ts +17 -7
  59. package/src/routes/api/config.ts +7 -0
  60. package/src/routes/api/logs.stream.ts +26 -16
  61. package/.output/public/assets/index-DRRCmu5p.css +0 -1
  62. 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;
@@ -30,6 +45,8 @@ export type LogEntryProps = {
30
45
  * cost).
31
46
  */
32
47
  strip: boolean;
48
+ /** Slow-response threshold in seconds. `0` disables the warning indicator. */
49
+ slowResponseThresholdSeconds: number;
33
50
  /**
34
51
  * Per-log cache token trend, looked up in the viewer-level trend map.
35
52
  * `null` (or absent) means the header should render with no arrows.
@@ -39,46 +56,6 @@ export type LogEntryProps = {
39
56
  onCompareWithPrevious?: (log: CapturedLog) => void;
40
57
  };
41
58
 
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
59
  function CopyButton({
83
60
  text,
84
61
  label,
@@ -92,23 +69,25 @@ function CopyButton({
92
69
  }): JSX.Element | null {
93
70
  if (text === null) return null;
94
71
  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>
72
+ <Tooltip>
73
+ <TooltipTrigger asChild>
74
+ <Button
75
+ variant="outline"
76
+ size="sm"
77
+ className="h-8 text-xs"
78
+ onClick={onCopy}
79
+ aria-label={label}
80
+ >
81
+ {copied ? (
82
+ <Check className="size-3.5 mr-1 text-emerald-500" />
83
+ ) : (
84
+ <Copy className="size-3.5 mr-1" />
85
+ )}
86
+ {copied ? "Copied!" : label}
87
+ </Button>
88
+ </TooltipTrigger>
89
+ <TooltipContent>{copied ? "Copied to clipboard" : label}</TooltipContent>
90
+ </Tooltip>
112
91
  );
113
92
  }
114
93
 
@@ -122,20 +101,16 @@ function DiffToggleButton({
122
101
  return (
123
102
  <Tooltip>
124
103
  <TooltipTrigger asChild>
125
- <button
126
- type="button"
104
+ <Button
105
+ variant={active ? "default" : "outline"}
106
+ size="sm"
107
+ className="h-8 text-xs"
127
108
  onClick={onClick}
128
109
  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
110
  >
136
- <GitCompareArrows className="size-3" />
111
+ <GitCompareArrows className="size-3.5 mr-1" />
137
112
  {active ? "Showing diff" : "Diff with Raw"}
138
- </button>
113
+ </Button>
139
114
  </TooltipTrigger>
140
115
  <TooltipContent>
141
116
  {active ? "Hide diff view" : "Compare proxy output against the original raw version"}
@@ -207,6 +182,7 @@ export const LogEntry = memo(function ({
207
182
  log,
208
183
  viewMode = "simple",
209
184
  strip,
185
+ slowResponseThresholdSeconds,
210
186
  cacheTrend = null,
211
187
  onCompareWithPrevious,
212
188
  }: LogEntryProps): JSX.Element {
@@ -215,6 +191,8 @@ export const LogEntry = memo(function ({
215
191
  const [headersDiff, setHeadersDiff] = useState<boolean>(false);
216
192
  const [requestDiff, setRequestDiff] = useState<boolean>(false);
217
193
  const [activeTab, setActiveTab] = useState("request");
194
+ const [expandToPath, setExpandToPath] = useState<string | null>(null);
195
+ const requestJsonRef = useRef<HTMLDivElement | null>(null);
218
196
  const resolvedFormat = resolveLogFormat(log);
219
197
  const adapter = getLogFormatAdapter(resolvedFormat);
220
198
  const requestAnalysis = useMemo(
@@ -237,6 +215,24 @@ export const LogEntry = memo(function ({
237
215
  const responseCopy = useCopyFeedback(log.responseText);
238
216
  const requestExpansion = useJsonBulkExpansion(displayedRequestBody);
239
217
  const rawRequestExpansion = useJsonBulkExpansion(log.rawRequestBody);
218
+ const anatomySegments = useMemo(
219
+ () =>
220
+ requestExpansion.parsedData !== null
221
+ ? adapter.anatomySegments(requestExpansion.parsedData)
222
+ : null,
223
+ [adapter, requestExpansion.parsedData],
224
+ );
225
+ const anatomyPaths = useMemo(() => {
226
+ if (anatomySegments === null) return undefined;
227
+ return new Set(anatomySegments.map((s) => s.path));
228
+ }, [anatomySegments]);
229
+ const jumpToAnatomySegment = useAnatomyJump({
230
+ containerRef: requestJsonRef,
231
+ setExpandToPath,
232
+ ensureTabActive: () => {
233
+ if (activeTab !== "request") setActiveTab("request");
234
+ },
235
+ });
240
236
 
241
237
  return (
242
238
  <TooltipProvider>
@@ -249,6 +245,31 @@ export const LogEntry = memo(function ({
249
245
  onToggle={() => setExpanded(!expanded)}
250
246
  responseToolNames={responseAnalysis.toolNames}
251
247
  cacheTrend={cacheTrend}
248
+ slowResponseThresholdSeconds={slowResponseThresholdSeconds}
249
+ onReplay={
250
+ onCompareWithPrevious === undefined
251
+ ? undefined
252
+ : () => {
253
+ setReplayOpen(true);
254
+ }
255
+ }
256
+ onCopyRequest={
257
+ displayedRequestBody === null
258
+ ? undefined
259
+ : (e) => {
260
+ requestCopy.copy(e);
261
+ }
262
+ }
263
+ requestCopied={requestCopy.copied}
264
+ onToggleRequestExpansion={requestExpansion.toggle}
265
+ requestExpansionState={
266
+ requestExpansion.policy === null
267
+ ? null
268
+ : {
269
+ isExpanded: requestExpansion.isExpanded,
270
+ isPending: requestExpansion.isPending,
271
+ }
272
+ }
252
273
  />
253
274
 
254
275
  {expanded && (
@@ -261,6 +282,7 @@ export const LogEntry = memo(function ({
261
282
  <TabsTrigger value="raw-request">Raw Request</TabsTrigger>
262
283
  )}
263
284
  <TabsTrigger value="request">Request</TabsTrigger>
285
+ {anatomySegments !== null && <TabsTrigger value="anatomy">Anatomy</TabsTrigger>}
264
286
  {viewMode === "full" && <TabsTrigger value="raw">Raw Response</TabsTrigger>}
265
287
  <TabsTrigger value="parsed">Response</TabsTrigger>
266
288
  </TabsList>
@@ -268,7 +290,7 @@ export const LogEntry = memo(function ({
268
290
  {shouldShowRawRequestTab(resolvedFormat, viewMode, strip) && (
269
291
  <TabsContent value="raw-request">
270
292
  {activeTab === "raw-request" && (
271
- <div className="px-4 py-3">
293
+ <div className="px-4 pt-1 pb-3">
272
294
  <div className="flex justify-end gap-2 mb-2">
273
295
  <JsonExpansionButton
274
296
  policy={rawRequestExpansion.policy}
@@ -286,11 +308,13 @@ export const LogEntry = memo(function ({
286
308
  {log.rawRequestBody === null ? (
287
309
  <p className="text-xs text-muted-foreground italic">No request body</p>
288
310
  ) : rawRequestExpansion.parsedData !== null ? (
289
- <JsonViewer
290
- data={rawRequestExpansion.parsedData}
291
- bulkDepth={rawRequestExpansion.bulkDepth}
292
- bulkRevision={rawRequestExpansion.bulkRevision}
293
- />
311
+ <Suspense fallback={<TabFallback />}>
312
+ <LazyJsonViewer
313
+ data={rawRequestExpansion.parsedData}
314
+ bulkDepth={rawRequestExpansion.bulkDepth}
315
+ bulkRevision={rawRequestExpansion.bulkRevision}
316
+ />
317
+ </Suspense>
294
318
  ) : (
295
319
  <pre className="font-mono text-xs whitespace-pre-wrap break-words">
296
320
  {log.rawRequestBody}
@@ -303,100 +327,104 @@ export const LogEntry = memo(function ({
303
327
 
304
328
  <TabsContent value="request">
305
329
  {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"
330
+ <div className="px-4 pt-1 pb-3">
331
+ {/* Per-tab secondary actions (diff toggles, compare-with-previous).
332
+ Replay / Copy / Expand-all JSON now live in the header. */}
333
+ {(shouldShowRequestDiffButton(
334
+ resolvedFormat,
335
+ viewMode,
336
+ strip,
337
+ log.rawRequestBody !== null,
338
+ ) ||
339
+ onCompareWithPrevious !== undefined) && (
340
+ <div className="flex justify-end gap-2 mb-2">
341
+ {shouldShowRequestDiffButton(
342
+ resolvedFormat,
343
+ viewMode,
344
+ strip,
345
+ log.rawRequestBody !== null,
346
+ ) && (
347
+ <DiffToggleButton
348
+ active={requestDiff}
349
349
  onClick={(e) => {
350
350
  e.stopPropagation();
351
- setReplayOpen(true);
351
+ setRequestDiff(!requestDiff);
352
352
  }}
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>
353
+ />
354
+ )}
355
+ {onCompareWithPrevious !== undefined && (
356
+ <Tooltip>
357
+ <TooltipTrigger asChild>
358
+ <Button
359
+ variant="outline"
360
+ size="sm"
361
+ className="h-8 text-xs"
362
+ onClick={(e) => {
363
+ e.stopPropagation();
364
+ onCompareWithPrevious(log);
365
+ }}
366
+ >
367
+ <GitCompareArrows className="size-3 mr-1" />
368
+ Diff with Previous
369
+ </Button>
370
+ </TooltipTrigger>
371
+ <TooltipContent>
372
+ Compare this request with the immediately preceding one
373
+ </TooltipContent>
374
+ </Tooltip>
375
+ )}
376
+ </div>
377
+ )}
373
378
  {requestDiff ? (
374
379
  <RequestDiffContent
375
380
  rawBody={log.rawRequestBody}
376
381
  displayedBody={displayedRequestBody}
377
382
  emptyLabel="No transformation applied — raw and sent request bodies are identical."
378
383
  />
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
384
  ) : (
388
- <pre className="font-mono text-xs whitespace-pre-wrap break-words">
389
- {displayedRequestBody}
390
- </pre>
385
+ <div ref={requestJsonRef}>
386
+ {displayedRequestBody === null ? (
387
+ <p className="text-xs text-muted-foreground italic">No request body</p>
388
+ ) : requestExpansion.parsedData !== null ? (
389
+ <Suspense fallback={<TabFallback />}>
390
+ <LazyJsonViewer
391
+ data={requestExpansion.parsedData}
392
+ bulkDepth={requestExpansion.bulkDepth}
393
+ bulkRevision={requestExpansion.bulkRevision}
394
+ anatomyPaths={anatomyPaths}
395
+ expandToPath={expandToPath}
396
+ />
397
+ </Suspense>
398
+ ) : (
399
+ <pre className="font-mono text-xs whitespace-pre-wrap break-words">
400
+ {displayedRequestBody}
401
+ </pre>
402
+ )}
403
+ </div>
391
404
  )}
392
405
  </div>
393
406
  )}
394
407
  </TabsContent>
395
408
 
409
+ {anatomySegments !== null && (
410
+ <TabsContent value="anatomy">
411
+ {activeTab === "anatomy" && (
412
+ <Suspense fallback={<TabFallback />}>
413
+ <LazyRequestAnatomy
414
+ parsed={null}
415
+ inputTokens={log.inputTokens ?? null}
416
+ segments={anatomySegments}
417
+ onSegmentActivate={jumpToAnatomySegment}
418
+ />
419
+ </Suspense>
420
+ )}
421
+ </TabsContent>
422
+ )}
423
+
396
424
  {viewMode === "full" && (
397
425
  <TabsContent value="headers">
398
426
  {activeTab === "headers" && (
399
- <div className="px-4 py-3">
427
+ <div className="px-4 pt-1 pb-3">
400
428
  <div className="flex justify-end gap-2 mb-2">
401
429
  {shouldShowHeadersDiffButton(
402
430
  viewMode,
@@ -443,7 +471,7 @@ export const LogEntry = memo(function ({
443
471
  {viewMode === "full" && (
444
472
  <TabsContent value="raw-headers">
445
473
  {activeTab === "raw-headers" && (
446
- <div className="px-4 py-3">
474
+ <div className="px-4 pt-1 pb-3">
447
475
  {log.rawHeaders && Object.keys(log.rawHeaders).length > 0 ? (
448
476
  <div className="space-y-1 font-mono text-xs">
449
477
  {Object.entries(log.rawHeaders)
@@ -471,7 +499,7 @@ export const LogEntry = memo(function ({
471
499
 
472
500
  <TabsContent value="raw">
473
501
  {activeTab === "raw" && (
474
- <div className="px-4 py-3 space-y-3">
502
+ <div className="px-4 pt-1 pb-3 space-y-3">
475
503
  {log.error !== undefined && log.error !== null && (
476
504
  <div className="rounded border border-destructive/50 bg-destructive/10 p-3 text-xs">
477
505
  <div className="font-semibold text-destructive mb-1">SSE Error</div>
@@ -487,15 +515,19 @@ export const LogEntry = memo(function ({
487
515
  />
488
516
  </div>
489
517
  {log.responseText !== null ? (
490
- <JsonViewerFromString text={log.responseText} defaultExpandDepth={0} />
518
+ <Suspense fallback={<TabFallback />}>
519
+ <LazyJsonViewerFromString text={log.responseText} defaultExpandDepth={0} />
520
+ </Suspense>
491
521
  ) : (
492
522
  <p className="text-xs text-muted-foreground italic">No response</p>
493
523
  )}
494
524
  {log.streaming === true && (
495
- <StreamingChunkSequence
496
- logId={log.id}
497
- truncated={log.streamingChunksPath !== null}
498
- />
525
+ <Suspense fallback={<TabFallback />}>
526
+ <LazyStreamingChunkSequence
527
+ logId={log.id}
528
+ truncated={log.streamingChunksPath !== null}
529
+ />
530
+ </Suspense>
499
531
  )}
500
532
  </div>
501
533
  )}
@@ -503,18 +535,20 @@ export const LogEntry = memo(function ({
503
535
 
504
536
  <TabsContent value="parsed">
505
537
  {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
- />
538
+ <div className="px-4 pt-1 pb-3">
539
+ <Suspense fallback={<TabFallback />}>
540
+ <LazyResponseView
541
+ responseText={log.responseText}
542
+ responseStatus={log.responseStatus}
543
+ streaming={log.streaming}
544
+ inputTokens={log.inputTokens}
545
+ outputTokens={log.outputTokens}
546
+ cacheCreationInputTokens={log.cacheCreationInputTokens}
547
+ cacheReadInputTokens={log.cacheReadInputTokens}
548
+ apiFormat={resolvedFormat}
549
+ error={log.error}
550
+ />
551
+ </Suspense>
518
552
  </div>
519
553
  )}
520
554
  </TabsContent>
@@ -522,7 +556,9 @@ export const LogEntry = memo(function ({
522
556
  </div>
523
557
  )}
524
558
  </div>
525
- <ReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
559
+ <Suspense fallback={null}>
560
+ <LazyReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
561
+ </Suspense>
526
562
  </TooltipProvider>
527
563
  );
528
564
  });