@tonyclaw/agent-inspector 2.1.3 → 2.1.4

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-BLYcm6Dg.js → CompareDrawer-BihEPd3v.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-DYqXb6WW.js +117 -0
  4. package/.output/public/assets/{ReplayDialog-_kCy9L9E.js → ReplayDialog-DnX-6kY8.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-DWp5pWsv.js → RequestAnatomy-CIkHAxZO.js} +1 -1
  6. package/.output/public/assets/{ResponseView-Ci92o6mt.js → ResponseView-BFc8jjZM.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-C-1ZQl_b.js → StreamingChunkSequence-p8qkpF6K.js} +1 -1
  8. package/.output/public/assets/_sessionId-Csx4T_y6.js +1 -0
  9. package/.output/public/assets/index-Bcq8bZoK.css +1 -0
  10. package/.output/public/assets/index-KMuh-31x.js +1 -0
  11. package/.output/public/assets/{main-RgLvmIQk.js → main-xlSdu7_I.js} +2 -2
  12. package/.output/server/_libs/lucide-react.mjs +204 -173
  13. package/.output/server/_libs/radix-ui__react-dialog.mjs +2 -0
  14. package/.output/server/{_sessionId-DnOYbKO7.mjs → _sessionId-CwVQgxoK.mjs} +3 -3
  15. package/.output/server/_ssr/{CompareDrawer-D44_F13_.mjs → CompareDrawer-Br9Ec0ah.mjs} +3 -3
  16. package/.output/server/_ssr/{ProxyViewerContainer-Cx4p_awW.mjs → ProxyViewerContainer-Oe5Tr3C2.mjs} +982 -70
  17. package/.output/server/_ssr/{ReplayDialog-wScCF3Oc.mjs → ReplayDialog-C1_QsoA-.mjs} +4 -4
  18. package/.output/server/_ssr/{RequestAnatomy-Cf7R7PXS.mjs → RequestAnatomy-cn08QSoz.mjs} +2 -2
  19. package/.output/server/_ssr/{ResponseView-7pVIZLL-.mjs → ResponseView-CIqUCCTG.mjs} +3 -3
  20. package/.output/server/_ssr/{StreamingChunkSequence-Bs57_GbC.mjs → StreamingChunkSequence-0MhVWD2F.mjs} +3 -3
  21. package/.output/server/_ssr/{index-6eY__la9.mjs → index-jpnI5ghp.mjs} +2 -2
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{router-DRZPhZB6.mjs → router-SSOn7c09.mjs} +1970 -221
  24. package/.output/server/_tanstack-start-manifest_v-BCgy_9er.mjs +4 -0
  25. package/.output/server/index.mjs +64 -64
  26. package/package.json +2 -1
  27. package/src/components/ProxyViewer.tsx +87 -1
  28. package/src/components/ProxyViewerContainer.tsx +26 -0
  29. package/src/components/alerts/AlertsDialog.tsx +610 -0
  30. package/src/components/proxy-viewer/LogEntry.tsx +324 -51
  31. package/src/components/ui/dialog.tsx +1 -0
  32. package/src/contracts/index.ts +15 -2
  33. package/src/contracts/log.ts +18 -0
  34. package/src/lib/alertContract.ts +79 -0
  35. package/src/lib/apiClient.ts +39 -0
  36. package/src/lib/export-logs.ts +47 -9
  37. package/src/lib/logImportContract.ts +12 -0
  38. package/src/lib/useAlerts.ts +82 -0
  39. package/src/lib/useGroupEvidence.ts +6 -2
  40. package/src/lib/useGroups.ts +6 -2
  41. package/src/proxy/alerts.ts +664 -0
  42. package/src/proxy/logBodyChunks.ts +91 -0
  43. package/src/proxy/logImporter.ts +491 -0
  44. package/src/proxy/logIndex.ts +63 -2
  45. package/src/proxy/sqliteLogIndex.ts +612 -0
  46. package/src/proxy/store.ts +32 -7
  47. package/src/routes/api/alerts.summary.ts +24 -0
  48. package/src/routes/api/alerts.ts +52 -0
  49. package/src/routes/api/logs.$id.body.ts +44 -0
  50. package/src/routes/api/logs.import.ts +50 -0
  51. package/src/services/alerts.ts +12 -0
  52. package/.output/public/assets/ProxyViewerContainer-Ba5wFn00.js +0 -117
  53. package/.output/public/assets/_sessionId-DKzAmSNP.js +0 -1
  54. package/.output/public/assets/index-B-MZ9lQM.css +0 -1
  55. package/.output/public/assets/index-ZZHwgkWJ.js +0 -1
  56. package/.output/server/_tanstack-start-manifest_v-DfAVFkZ5.mjs +0 -4
@@ -1,8 +1,13 @@
1
1
  import { AlertTriangle, GitCompareArrows } from "lucide-react";
2
2
  import { Suspense, type JSX, type ReactNode } from "react";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState, memo } from "react";
4
- import { CapturedLogSchema, type CapturedLog } from "../../contracts";
5
- import { fetchJson } from "../../lib/apiClient";
4
+ import {
5
+ CapturedLogSchema,
6
+ LogBodyChunkSchema,
7
+ type CapturedLog,
8
+ type LogBodyPart,
9
+ } from "../../contracts";
10
+ import { ApiTimeoutError, fetchJsonWithTimeout } from "../../lib/apiClient";
6
11
  import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
7
12
  import { cn } from "../../lib/utils";
8
13
  import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
@@ -53,14 +58,78 @@ function resolveFocusedTab(tab: LogFocusTab, anatomySegments: AnatomySegment[] |
53
58
  }
54
59
 
55
60
  type BodyHydrationState = "idle" | "loading" | "failed";
61
+ type BodyChunkState = "idle" | "loading" | "success" | "failed";
62
+
63
+ type BodyPreviewState = {
64
+ status: BodyChunkState;
65
+ text: string;
66
+ offset: number;
67
+ totalBytes: number | null;
68
+ hasMore: boolean;
69
+ error: string | null;
70
+ };
71
+
72
+ const FULL_LOG_HYDRATION_TIMEOUT_MS = 15_000;
73
+ const BODY_CHUNK_TIMEOUT_MS = 15_000;
74
+ const BODY_CHUNK_BYTES = 256 * 1024;
75
+ const CHUNKED_BODY_THRESHOLD_BYTES = 1024 * 1024;
56
76
 
57
77
  const TAB_TRIGGER_CLASS =
58
78
  "h-9 flex-none rounded-none border-0 border-b-2 border-transparent bg-transparent px-3 text-xs font-semibold text-muted-foreground shadow-none hover:bg-cyan-400/[0.04] hover:text-foreground data-[state=active]:border-cyan-300 data-[state=active]:bg-transparent data-[state=active]:text-cyan-100 data-[state=active]:shadow-none";
59
79
 
60
- function shouldHydrateLogBody(log: CapturedLog): boolean {
80
+ function createEmptyBodyPreviewState(totalBytes: number | null = null): BodyPreviewState {
81
+ return {
82
+ status: "idle",
83
+ text: "",
84
+ offset: 0,
85
+ totalBytes,
86
+ hasMore: totalBytes !== null && totalBytes > 0,
87
+ error: null,
88
+ };
89
+ }
90
+
91
+ function bodyByteCount(value: number | null | undefined): number {
92
+ return value ?? 0;
93
+ }
94
+
95
+ function isCompactBody(log: CapturedLog): boolean {
61
96
  return log.bodyContentMode === "compact" || log.bodyContentMode === "truncated";
62
97
  }
63
98
 
99
+ function shouldUseChunkedBodyLoading(log: CapturedLog): boolean {
100
+ if (!isCompactBody(log)) return false;
101
+ return (
102
+ bodyByteCount(log.rawRequestBodyBytes) > CHUNKED_BODY_THRESHOLD_BYTES ||
103
+ bodyByteCount(log.responseTextBytes) > CHUNKED_BODY_THRESHOLD_BYTES
104
+ );
105
+ }
106
+
107
+ function shouldHydrateLogBody(log: CapturedLog): boolean {
108
+ return isCompactBody(log) && !shouldUseChunkedBodyLoading(log);
109
+ }
110
+
111
+ function formatBytes(bytes: number): string {
112
+ if (bytes < 1024) return `${bytes.toLocaleString()} B`;
113
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
114
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
115
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GiB`;
116
+ }
117
+
118
+ function bodyPreviewErrorMessage(error: unknown): string {
119
+ if (error instanceof ApiTimeoutError) {
120
+ return "Body preview timed out. The log is still available on disk; try loading again.";
121
+ }
122
+ if (error instanceof Error) return error.message;
123
+ return "Body preview could not be loaded.";
124
+ }
125
+
126
+ function bodyPreviewTextForView(enabled: boolean, state: BodyPreviewState): string | null {
127
+ if (!enabled) return null;
128
+ if (state.text !== "") return state.text;
129
+ if (state.status === "success") return "";
130
+ return null;
131
+ }
132
+
64
133
  function ExpandedPanel({
65
134
  children,
66
135
  className,
@@ -126,6 +195,53 @@ function NoticeBlock({
126
195
  );
127
196
  }
128
197
 
198
+ function BodyPreviewNotice({
199
+ label,
200
+ state,
201
+ expectedBytes,
202
+ onLoadMore,
203
+ }: {
204
+ label: string;
205
+ state: BodyPreviewState;
206
+ expectedBytes: number | null;
207
+ onLoadMore: () => void;
208
+ }): JSX.Element {
209
+ const totalBytes = state.totalBytes ?? expectedBytes;
210
+ const loadedLabel =
211
+ totalBytes === null
212
+ ? formatBytes(state.offset)
213
+ : `${formatBytes(state.offset)} / ${formatBytes(totalBytes)}`;
214
+ const showLoadMore = state.status !== "loading" && state.hasMore;
215
+ const actionLabel = state.status === "failed" ? "Retry" : "Load more";
216
+
217
+ return (
218
+ <NoticeBlock
219
+ tone={state.status === "failed" ? "warning" : "loading"}
220
+ title={`${label} body preview`}
221
+ >
222
+ <div className="flex flex-wrap items-center justify-between gap-2">
223
+ <span>
224
+ {state.status === "idle" && totalBytes !== null
225
+ ? `${label} body is ${formatBytes(totalBytes)}. Loading the first preview chunk.`
226
+ : `Loaded ${loadedLabel}.`}
227
+ {state.status === "failed" && state.error !== null && ` ${state.error}`}
228
+ </span>
229
+ {showLoadMore && (
230
+ <Button
231
+ type="button"
232
+ variant="outline"
233
+ size="sm"
234
+ className="h-7 border-white/10 bg-black/20 text-xs text-cyan-100 hover:border-cyan-300/35 hover:bg-cyan-400/10"
235
+ onClick={onLoadMore}
236
+ >
237
+ {actionLabel}
238
+ </Button>
239
+ )}
240
+ </div>
241
+ </NoticeBlock>
242
+ );
243
+ }
244
+
129
245
  function HeaderRows({
130
246
  headers,
131
247
  emptyLabel,
@@ -260,8 +376,30 @@ export const LogEntry = memo(function ({
260
376
  const [expandToPath, setExpandToPath] = useState<string | null>(null);
261
377
  const [hydratedLog, setHydratedLog] = useState<CapturedLog | null>(null);
262
378
  const [bodyHydrationState, setBodyHydrationState] = useState<BodyHydrationState>("idle");
379
+ const [requestPreview, setRequestPreview] = useState<BodyPreviewState>(() =>
380
+ createEmptyBodyPreviewState(log.rawRequestBodyBytes ?? null),
381
+ );
382
+ const [responsePreview, setResponsePreview] = useState<BodyPreviewState>(() =>
383
+ createEmptyBodyPreviewState(log.responseTextBytes ?? null),
384
+ );
263
385
  const requestJsonRef = useRef<HTMLDivElement | null>(null);
386
+ const logIdRef = useRef(log.id);
264
387
  const displayLog = hydratedLog ?? log;
388
+ const useChunkedBody = hydratedLog === null && shouldUseChunkedBodyLoading(log);
389
+ const requestPreviewBody = bodyPreviewTextForView(useChunkedBody, requestPreview);
390
+ const responsePreviewBody = bodyPreviewTextForView(useChunkedBody, responsePreview);
391
+ const rawRequestBodyForView = displayLog.rawRequestBody ?? requestPreviewBody;
392
+ const responseTextForView = displayLog.responseText ?? responsePreviewBody;
393
+ const requestPreviewIsActive = displayLog.rawRequestBody === null && requestPreviewBody !== null;
394
+ const responsePreviewIsActive = displayLog.responseText === null && responsePreviewBody !== null;
395
+ const requestPreviewShouldParse =
396
+ !requestPreviewIsActive ||
397
+ (requestPreview.hasMore === false &&
398
+ bodyByteCount(requestPreview.totalBytes) <= CHUNKED_BODY_THRESHOLD_BYTES);
399
+ const responsePreviewShouldParse =
400
+ !responsePreviewIsActive ||
401
+ (responsePreview.hasMore === false &&
402
+ bodyByteCount(responsePreview.totalBytes) <= CHUNKED_BODY_THRESHOLD_BYTES);
265
403
  const resolvedFormat = resolveLogFormat(displayLog);
266
404
  const adapter = getLogFormatAdapter(resolvedFormat);
267
405
  const requestAnalysis = useMemo(
@@ -278,10 +416,16 @@ export const LogEntry = memo(function ({
278
416
  }
279
417
  return stripClaudeCodeBillingHeader(displayLog.rawRequestBody).body;
280
418
  }, [displayLog.rawRequestBody, resolvedFormat, strip]);
281
- const displayedRequestBody = strippedRequestBody ?? displayLog.rawRequestBody;
282
- const requestExpansion = useJsonBulkExpansion(displayedRequestBody);
283
- const rawRequestExpansion = useJsonBulkExpansion(displayLog.rawRequestBody);
284
- const responseExpansion = useJsonBulkExpansion(displayLog.responseText);
419
+ const displayedRequestBody = strippedRequestBody ?? rawRequestBodyForView;
420
+ const requestExpansion = useJsonBulkExpansion(
421
+ requestPreviewShouldParse ? displayedRequestBody : null,
422
+ );
423
+ const rawRequestExpansion = useJsonBulkExpansion(
424
+ requestPreviewShouldParse ? rawRequestBodyForView : null,
425
+ );
426
+ const responseExpansion = useJsonBulkExpansion(
427
+ responsePreviewShouldParse ? responseTextForView : null,
428
+ );
285
429
 
286
430
  // Headers are rendered as a flat list, so we copy them as pretty-printed JSON.
287
431
  // Only build the string when there's at least one entry — empty headers would
@@ -304,10 +448,10 @@ export const LogEntry = memo(function ({
304
448
  // One copy-feedback hook per JSON-bearing tab, so each tab can surface its
305
449
  // own Copy button in the header with independent "Copied!" feedback.
306
450
  const requestCopy = useCopyFeedback(displayedRequestBody);
307
- const rawRequestCopy = useCopyFeedback(displayLog.rawRequestBody);
451
+ const rawRequestCopy = useCopyFeedback(rawRequestBodyForView);
308
452
  const headersCopy = useCopyFeedback(headersText);
309
453
  const rawHeadersCopy = useCopyFeedback(rawHeadersText);
310
- const responseCopy = useCopyFeedback(displayLog.responseText);
454
+ const responseCopy = useCopyFeedback(responseTextForView);
311
455
 
312
456
  // Per-tab action bundles consumed by the header. The header renders the
313
457
  // entry whose key matches `activeTab`. Tabs without an entry (Context,
@@ -315,15 +459,17 @@ export const LogEntry = memo(function ({
315
459
  const tabActions: HeaderTabActions = useMemo(
316
460
  () => ({
317
461
  request: {
318
- copyLabel: "Copy request body",
462
+ copyLabel: requestPreviewIsActive ? "Copy loaded request preview" : "Copy request body",
319
463
  copyText: displayedRequestBody,
320
464
  copyCopied: requestCopy.copied,
321
465
  onCopy: requestCopy.copy,
322
- expansion: {
323
- isExpanded: requestExpansion.isExpanded,
324
- isPending: requestExpansion.isPending,
325
- onToggle: requestExpansion.toggle,
326
- },
466
+ expansion: requestPreviewShouldParse
467
+ ? {
468
+ isExpanded: requestExpansion.isExpanded,
469
+ isPending: requestExpansion.isPending,
470
+ onToggle: requestExpansion.toggle,
471
+ }
472
+ : null,
327
473
  // "Diff with Raw" only makes sense when there's a raw body to compare
328
474
  // against (Anthropic + strip pipeline produces one). "Diff with
329
475
  // Previous" only exists when the parent wired up the compare drawer.
@@ -343,15 +489,17 @@ export const LogEntry = memo(function ({
343
489
  },
344
490
  },
345
491
  "raw-request": {
346
- copyLabel: "Copy raw request",
347
- copyText: displayLog.rawRequestBody,
492
+ copyLabel: requestPreviewIsActive ? "Copy loaded raw request preview" : "Copy raw request",
493
+ copyText: rawRequestBodyForView,
348
494
  copyCopied: rawRequestCopy.copied,
349
495
  onCopy: rawRequestCopy.copy,
350
- expansion: {
351
- isExpanded: rawRequestExpansion.isExpanded,
352
- isPending: rawRequestExpansion.isPending,
353
- onToggle: rawRequestExpansion.toggle,
354
- },
496
+ expansion: requestPreviewShouldParse
497
+ ? {
498
+ isExpanded: rawRequestExpansion.isExpanded,
499
+ isPending: rawRequestExpansion.isPending,
500
+ onToggle: rawRequestExpansion.toggle,
501
+ }
502
+ : null,
355
503
  },
356
504
  headers: {
357
505
  copyLabel: "Copy headers",
@@ -369,24 +517,29 @@ export const LogEntry = memo(function ({
369
517
  expansion: null,
370
518
  },
371
519
  raw: {
372
- copyLabel: "Copy response",
373
- copyText: displayLog.responseText,
520
+ copyLabel: responsePreviewIsActive ? "Copy loaded response preview" : "Copy response",
521
+ copyText: responseTextForView,
374
522
  copyCopied: responseCopy.copied,
375
523
  onCopy: responseCopy.copy,
376
- expansion: {
377
- isExpanded: responseExpansion.isExpanded,
378
- isPending: responseExpansion.isPending,
379
- onToggle: responseExpansion.toggle,
380
- },
524
+ expansion: responsePreviewShouldParse
525
+ ? {
526
+ isExpanded: responseExpansion.isExpanded,
527
+ isPending: responseExpansion.isPending,
528
+ onToggle: responseExpansion.toggle,
529
+ }
530
+ : null,
381
531
  },
382
532
  }),
383
533
  [
384
534
  displayedRequestBody,
535
+ requestPreviewIsActive,
536
+ requestPreviewShouldParse,
385
537
  requestCopy,
386
538
  requestExpansion,
387
539
  requestDiff,
388
540
  displayLog,
389
541
  displayLog.rawRequestBody,
542
+ rawRequestBodyForView,
390
543
  rawRequestCopy,
391
544
  rawRequestExpansion,
392
545
  headersText,
@@ -394,6 +547,9 @@ export const LogEntry = memo(function ({
394
547
  rawHeadersText,
395
548
  rawHeadersCopy,
396
549
  displayLog.responseText,
550
+ responsePreviewIsActive,
551
+ responsePreviewShouldParse,
552
+ responseTextForView,
397
553
  responseCopy,
398
554
  responseExpansion,
399
555
  resolvedFormat,
@@ -425,6 +581,48 @@ export const LogEntry = memo(function ({
425
581
  if (activeTab !== "request") setActiveTab("request");
426
582
  },
427
583
  });
584
+ const loadBodyChunk = useCallback(
585
+ (part: LogBodyPart, offset: number) => {
586
+ const setPreview = part === "request" ? setRequestPreview : setResponsePreview;
587
+ setPreview((current) => ({
588
+ ...current,
589
+ status: "loading",
590
+ error: null,
591
+ }));
592
+
593
+ const params = new URLSearchParams({
594
+ part,
595
+ offset: String(offset),
596
+ limit: String(BODY_CHUNK_BYTES),
597
+ });
598
+
599
+ void fetchJsonWithTimeout(
600
+ `/api/logs/${String(log.id)}/body?${params.toString()}`,
601
+ LogBodyChunkSchema,
602
+ BODY_CHUNK_TIMEOUT_MS,
603
+ )
604
+ .then((chunk) => {
605
+ if (logIdRef.current !== log.id) return;
606
+ setPreview((current) => ({
607
+ status: "success",
608
+ text: chunk.offset === 0 ? chunk.text : `${current.text}${chunk.text}`,
609
+ offset: chunk.nextOffset ?? chunk.totalBytes,
610
+ totalBytes: chunk.totalBytes,
611
+ hasMore: chunk.hasMore,
612
+ error: null,
613
+ }));
614
+ })
615
+ .catch((error: unknown) => {
616
+ if (logIdRef.current !== log.id) return;
617
+ setPreview((current) => ({
618
+ ...current,
619
+ status: "failed",
620
+ error: bodyPreviewErrorMessage(error),
621
+ }));
622
+ });
623
+ },
624
+ [log.id],
625
+ );
428
626
 
429
627
  const handleToggleExpanded = useCallback(() => {
430
628
  const nextExpanded = !expanded;
@@ -435,9 +633,12 @@ export const LogEntry = memo(function ({
435
633
  }, [anatomySegments, expanded]);
436
634
 
437
635
  useEffect(() => {
636
+ logIdRef.current = log.id;
438
637
  setHydratedLog(null);
439
638
  setBodyHydrationState("idle");
440
- }, [log.id]);
639
+ setRequestPreview(createEmptyBodyPreviewState(log.rawRequestBodyBytes ?? null));
640
+ setResponsePreview(createEmptyBodyPreviewState(log.responseTextBytes ?? null));
641
+ }, [log.id, log.rawRequestBodyBytes, log.responseTextBytes]);
441
642
 
442
643
  useEffect(() => {
443
644
  if (!expanded) return;
@@ -446,7 +647,11 @@ export const LogEntry = memo(function ({
446
647
 
447
648
  let cancelled = false;
448
649
  setBodyHydrationState("loading");
449
- void fetchJson(`/api/logs/${String(log.id)}`, CapturedLogSchema)
650
+ void fetchJsonWithTimeout(
651
+ `/api/logs/${String(log.id)}`,
652
+ CapturedLogSchema,
653
+ FULL_LOG_HYDRATION_TIMEOUT_MS,
654
+ )
450
655
  .then((fullLog) => {
451
656
  if (cancelled) return;
452
657
  setHydratedLog(fullLog);
@@ -460,7 +665,31 @@ export const LogEntry = memo(function ({
460
665
  return () => {
461
666
  cancelled = true;
462
667
  };
463
- }, [bodyHydrationState, expanded, hydratedLog, log.bodyContentMode, log.id]);
668
+ }, [
669
+ bodyHydrationState,
670
+ expanded,
671
+ hydratedLog,
672
+ log.bodyContentMode,
673
+ log.id,
674
+ log.rawRequestBodyBytes,
675
+ log.responseTextBytes,
676
+ ]);
677
+
678
+ useEffect(() => {
679
+ if (!expanded) return;
680
+ if (!useChunkedBody) return;
681
+ if (requestPreview.status !== "idle") return;
682
+ if (activeTab !== "request" && activeTab !== "raw-request" && activeTab !== "anatomy") return;
683
+ loadBodyChunk("request", 0);
684
+ }, [activeTab, expanded, loadBodyChunk, requestPreview.status, useChunkedBody]);
685
+
686
+ useEffect(() => {
687
+ if (!expanded) return;
688
+ if (!useChunkedBody) return;
689
+ if (responsePreview.status !== "idle") return;
690
+ if (activeTab !== "parsed" && activeTab !== "raw") return;
691
+ loadBodyChunk("response", 0);
692
+ }, [activeTab, expanded, loadBodyChunk, responsePreview.status, useChunkedBody]);
464
693
 
465
694
  useEffect(() => {
466
695
  const handleLogFocusRequest = (event: Event): void => {
@@ -555,8 +784,18 @@ export const LogEntry = memo(function ({
555
784
  <TabsContent value="raw-request">
556
785
  {activeTab === "raw-request" && (
557
786
  <ExpandedPanel>
558
- {displayLog.rawRequestBody === null ? (
559
- <EmptyState>No request body</EmptyState>
787
+ {useChunkedBody && (
788
+ <BodyPreviewNotice
789
+ label="Request"
790
+ state={requestPreview}
791
+ expectedBytes={log.rawRequestBodyBytes ?? null}
792
+ onLoadMore={() => loadBodyChunk("request", requestPreview.offset)}
793
+ />
794
+ )}
795
+ {rawRequestBodyForView === null ? (
796
+ <EmptyState>
797
+ {useChunkedBody ? "Request preview is loading" : "No request body"}
798
+ </EmptyState>
560
799
  ) : rawRequestExpansion.parsedData !== null ? (
561
800
  <Suspense fallback={<TabFallback />}>
562
801
  <LazyJsonViewer
@@ -566,7 +805,7 @@ export const LogEntry = memo(function ({
566
805
  />
567
806
  </Suspense>
568
807
  ) : (
569
- <RawTextBlock>{displayLog.rawRequestBody}</RawTextBlock>
808
+ <RawTextBlock>{rawRequestBodyForView}</RawTextBlock>
570
809
  )}
571
810
  </ExpandedPanel>
572
811
  )}
@@ -587,6 +826,14 @@ export const LogEntry = memo(function ({
587
826
  still available.
588
827
  </NoticeBlock>
589
828
  )}
829
+ {useChunkedBody && (
830
+ <BodyPreviewNotice
831
+ label="Request"
832
+ state={requestPreview}
833
+ expectedBytes={log.rawRequestBodyBytes ?? null}
834
+ onLoadMore={() => loadBodyChunk("request", requestPreview.offset)}
835
+ />
836
+ )}
590
837
  {warnings.length > 0 && (
591
838
  <NoticeBlock tone="warning">
592
839
  <div className="flex items-center gap-2 font-semibold text-amber-300">
@@ -616,7 +863,9 @@ export const LogEntry = memo(function ({
616
863
  ) : (
617
864
  <div ref={requestJsonRef}>
618
865
  {displayedRequestBody === null ? (
619
- <EmptyState>No request body</EmptyState>
866
+ <EmptyState>
867
+ {useChunkedBody ? "Request preview is loading" : "No request body"}
868
+ </EmptyState>
620
869
  ) : requestExpansion.parsedData !== null ? (
621
870
  <Suspense fallback={<TabFallback />}>
622
871
  <LazyJsonViewer
@@ -705,22 +954,34 @@ export const LogEntry = memo(function ({
705
954
  <TabsContent value="raw">
706
955
  {activeTab === "raw" && (
707
956
  <ExpandedPanel className="space-y-3">
957
+ {useChunkedBody && (
958
+ <BodyPreviewNotice
959
+ label="Response"
960
+ state={responsePreview}
961
+ expectedBytes={log.responseTextBytes ?? null}
962
+ onLoadMore={() => loadBodyChunk("response", responsePreview.offset)}
963
+ />
964
+ )}
708
965
  {displayLog.error !== undefined && displayLog.error !== null && (
709
966
  <NoticeBlock tone="danger" title="SSE Error">
710
967
  <span className="font-mono">{displayLog.error}</span>
711
968
  </NoticeBlock>
712
969
  )}
713
- {displayLog.responseText !== null ? (
970
+ {responseTextForView !== null && responsePreviewShouldParse ? (
714
971
  <Suspense fallback={<TabFallback />}>
715
972
  <LazyJsonViewerFromString
716
- text={displayLog.responseText}
973
+ text={responseTextForView}
717
974
  defaultExpandDepth={0}
718
975
  bulkDepth={responseExpansion.bulkDepth}
719
976
  bulkRevision={responseExpansion.bulkRevision}
720
977
  />
721
978
  </Suspense>
979
+ ) : responseTextForView !== null ? (
980
+ <RawTextBlock>{responseTextForView}</RawTextBlock>
722
981
  ) : (
723
- <EmptyState>No response</EmptyState>
982
+ <EmptyState>
983
+ {useChunkedBody ? "Response preview is loading" : "No response"}
984
+ </EmptyState>
724
985
  )}
725
986
  {displayLog.streaming === true && (
726
987
  <Suspense fallback={<TabFallback />}>
@@ -737,19 +998,31 @@ export const LogEntry = memo(function ({
737
998
  <TabsContent value="parsed">
738
999
  {activeTab === "parsed" && (
739
1000
  <ExpandedPanel>
740
- <Suspense fallback={<TabFallback />}>
741
- <LazyResponseView
742
- responseText={displayLog.responseText}
743
- responseStatus={displayLog.responseStatus}
744
- streaming={displayLog.streaming}
745
- inputTokens={displayLog.inputTokens}
746
- outputTokens={displayLog.outputTokens}
747
- cacheCreationInputTokens={displayLog.cacheCreationInputTokens}
748
- cacheReadInputTokens={displayLog.cacheReadInputTokens}
749
- apiFormat={resolvedFormat}
750
- error={displayLog.error}
1001
+ {useChunkedBody && (
1002
+ <BodyPreviewNotice
1003
+ label="Response"
1004
+ state={responsePreview}
1005
+ expectedBytes={log.responseTextBytes ?? null}
1006
+ onLoadMore={() => loadBodyChunk("response", responsePreview.offset)}
751
1007
  />
752
- </Suspense>
1008
+ )}
1009
+ {responsePreviewIsActive && !responsePreviewShouldParse ? (
1010
+ <RawTextBlock>{responseTextForView}</RawTextBlock>
1011
+ ) : (
1012
+ <Suspense fallback={<TabFallback />}>
1013
+ <LazyResponseView
1014
+ responseText={responseTextForView}
1015
+ responseStatus={displayLog.responseStatus}
1016
+ streaming={displayLog.streaming}
1017
+ inputTokens={displayLog.inputTokens}
1018
+ outputTokens={displayLog.outputTokens}
1019
+ cacheCreationInputTokens={displayLog.cacheCreationInputTokens}
1020
+ cacheReadInputTokens={displayLog.cacheReadInputTokens}
1021
+ apiFormat={resolvedFormat}
1022
+ error={displayLog.error}
1023
+ />
1024
+ </Suspense>
1025
+ )}
753
1026
  </ExpandedPanel>
754
1027
  )}
755
1028
  </TabsContent>
@@ -125,5 +125,6 @@ export {
125
125
  DialogTrigger,
126
126
  DialogContent,
127
127
  DialogHeader,
128
+ DialogDescription,
128
129
  DialogTitle,
129
130
  };
@@ -1,8 +1,21 @@
1
1
  export { JsonValueSchema, trustAsJsonValue } from "./json";
2
2
  export type { JsonValue } from "./json";
3
3
 
4
- export { CapturedLogSchema, StreamingChunkSchema } from "./log";
5
- export type { ApiFormat, CapturedLog, RequestFormat, StreamingChunk, TokenUsage } from "./log";
4
+ export {
5
+ CapturedLogSchema,
6
+ LogBodyChunkSchema,
7
+ LogBodyPartSchema,
8
+ StreamingChunkSchema,
9
+ } from "./log";
10
+ export type {
11
+ ApiFormat,
12
+ CapturedLog,
13
+ LogBodyChunk,
14
+ LogBodyPart,
15
+ RequestFormat,
16
+ StreamingChunk,
17
+ TokenUsage,
18
+ } from "./log";
6
19
 
7
20
  export {
8
21
  AnthropicRequestSchema,
@@ -21,6 +21,24 @@ const StreamingChunksArraySchema = z.object({
21
21
  truncated: z.boolean().optional().default(false),
22
22
  });
23
23
 
24
+ export const LogBodyPartSchema = z.enum(["request", "response"]);
25
+ export type LogBodyPart = z.infer<typeof LogBodyPartSchema>;
26
+
27
+ export const LogBodyChunkSchema = z.object({
28
+ logId: z.number().int().positive(),
29
+ part: LogBodyPartSchema,
30
+ text: z.string(),
31
+ offset: z.number().int().nonnegative(),
32
+ limit: z.number().int().positive(),
33
+ totalBytes: z.number().int().nonnegative(),
34
+ textBytes: z.number().int().nonnegative(),
35
+ nextOffset: z.number().int().nonnegative().nullable(),
36
+ hasMore: z.boolean(),
37
+ contentMode: z.enum(["empty", "partial", "full"]),
38
+ });
39
+
40
+ export type LogBodyChunk = z.infer<typeof LogBodyChunkSchema>;
41
+
24
42
  export const CapturedLogSchema = z.object({
25
43
  id: z.number(),
26
44
  timestamp: z.string(),