@tonyclaw/agent-inspector 2.1.2 → 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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BULPis8W.js → CompareDrawer-BihEPd3v.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DYqXb6WW.js +117 -0
- package/.output/public/assets/{ReplayDialog-3ln-D7OS.js → ReplayDialog-DnX-6kY8.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-D8d1xV8O.js → RequestAnatomy-CIkHAxZO.js} +1 -1
- package/.output/public/assets/{ResponseView-CJijikn-.js → ResponseView-BFc8jjZM.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-C-RCuOTp.js → StreamingChunkSequence-p8qkpF6K.js} +1 -1
- package/.output/public/assets/_sessionId-Csx4T_y6.js +1 -0
- package/.output/public/assets/index-Bcq8bZoK.css +1 -0
- package/.output/public/assets/index-KMuh-31x.js +1 -0
- package/.output/public/assets/{main-D3pBFhb_.js → main-xlSdu7_I.js} +7 -7
- package/.output/server/_libs/lucide-react.mjs +204 -173
- package/.output/server/_libs/radix-ui__react-dialog.mjs +2 -0
- package/.output/server/{_sessionId-BFMVfrw3.mjs → _sessionId-CwVQgxoK.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-CwNPZgc_.mjs → CompareDrawer-Br9Ec0ah.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-HpLc7F_J.mjs → ProxyViewerContainer-Oe5Tr3C2.mjs} +982 -70
- package/.output/server/_ssr/{ReplayDialog-DBaFhSv6.mjs → ReplayDialog-C1_QsoA-.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-D0E0e2DT.mjs → RequestAnatomy-cn08QSoz.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-D2FFSRQ8.mjs → ResponseView-CIqUCCTG.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-Cdu36-rL.mjs → StreamingChunkSequence-0MhVWD2F.mjs} +3 -3
- package/.output/server/_ssr/{index-CtBfmJYM.mjs → index-jpnI5ghp.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-Dzx1sbAQ.mjs → router-SSOn7c09.mjs} +1971 -222
- package/.output/server/_tanstack-start-manifest_v-BCgy_9er.mjs +4 -0
- package/.output/server/index.mjs +62 -62
- package/package.json +2 -1
- package/src/assets/agent-inspector.ico +0 -0
- package/src/assets/favicon.svg +21 -31
- package/src/components/ProxyViewer.tsx +87 -1
- package/src/components/ProxyViewerContainer.tsx +26 -0
- package/src/components/alerts/AlertsDialog.tsx +610 -0
- package/src/components/proxy-viewer/LogEntry.tsx +324 -51
- package/src/components/ui/dialog.tsx +1 -0
- package/src/contracts/index.ts +15 -2
- package/src/contracts/log.ts +18 -0
- package/src/lib/alertContract.ts +79 -0
- package/src/lib/apiClient.ts +39 -0
- package/src/lib/export-logs.ts +47 -9
- package/src/lib/logImportContract.ts +12 -0
- package/src/lib/useAlerts.ts +82 -0
- package/src/lib/useGroupEvidence.ts +6 -2
- package/src/lib/useGroups.ts +6 -2
- package/src/proxy/alerts.ts +664 -0
- package/src/proxy/logBodyChunks.ts +91 -0
- package/src/proxy/logImporter.ts +491 -0
- package/src/proxy/logIndex.ts +63 -2
- package/src/proxy/sqliteLogIndex.ts +612 -0
- package/src/proxy/store.ts +32 -7
- package/src/routes/api/alerts.summary.ts +24 -0
- package/src/routes/api/alerts.ts +52 -0
- package/src/routes/api/logs.$id.body.ts +44 -0
- package/src/routes/api/logs.import.ts +50 -0
- package/src/services/alerts.ts +12 -0
- package/.output/public/assets/ProxyViewerContainer-YxtP1xBj.js +0 -117
- package/.output/public/assets/_sessionId-DGehOTux.js +0 -1
- package/.output/public/assets/index-B-MZ9lQM.css +0 -1
- package/.output/public/assets/index-DhsjXb_K.js +0 -1
- package/.output/server/_tanstack-start-manifest_v-VrssoWVE.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 {
|
|
5
|
-
|
|
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
|
|
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 ??
|
|
282
|
-
const requestExpansion = useJsonBulkExpansion(
|
|
283
|
-
|
|
284
|
-
|
|
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(
|
|
451
|
+
const rawRequestCopy = useCopyFeedback(rawRequestBodyForView);
|
|
308
452
|
const headersCopy = useCopyFeedback(headersText);
|
|
309
453
|
const rawHeadersCopy = useCopyFeedback(rawHeadersText);
|
|
310
|
-
const responseCopy = useCopyFeedback(
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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:
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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:
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
}, [
|
|
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
|
-
{
|
|
559
|
-
<
|
|
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>{
|
|
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>
|
|
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
|
-
{
|
|
970
|
+
{responseTextForView !== null && responsePreviewShouldParse ? (
|
|
714
971
|
<Suspense fallback={<TabFallback />}>
|
|
715
972
|
<LazyJsonViewerFromString
|
|
716
|
-
text={
|
|
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>
|
|
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
|
-
|
|
741
|
-
<
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
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>
|
package/src/contracts/index.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
export { JsonValueSchema, trustAsJsonValue } from "./json";
|
|
2
2
|
export type { JsonValue } from "./json";
|
|
3
3
|
|
|
4
|
-
export {
|
|
5
|
-
|
|
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,
|
package/src/contracts/log.ts
CHANGED
|
@@ -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(),
|