@tonyclaw/agent-inspector 2.0.42 → 2.1.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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-Cfqxlo-U.js → CompareDrawer-VDpcSNGM.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-fsXSjqtm.js +117 -0
- package/.output/public/assets/{ReplayDialog-daRidZo_.js → ReplayDialog-CoDgTzHR.js} +1 -1
- package/.output/public/assets/RequestAnatomy-t3plgkPE.js +1 -0
- package/.output/public/assets/ResponseView-BYuKiNeG.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-nK-0LpbM.js → StreamingChunkSequence-DOdU38Is.js} +1 -1
- package/.output/public/assets/_sessionId-ew5QlyZ2.js +1 -0
- package/.output/public/assets/index-DFICWD6o.js +1 -0
- package/.output/public/assets/index-zLLCkAnF.css +1 -0
- package/.output/public/assets/{main-DOy_Q96H.js → main-DmzLt6ti.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +219 -178
- package/.output/server/{_sessionId-DtYRZHlM.mjs → _sessionId-CCN1UoEf.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-fy1ARwtx.mjs → CompareDrawer-DkSY3vxP.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-9GJtCWCq.mjs → ProxyViewerContainer-OTh1V_Kh.mjs} +1034 -448
- package/.output/server/_ssr/{ReplayDialog-BrS7syOE.mjs → ReplayDialog-DhI6yxrR.mjs} +5 -5
- package/.output/server/_ssr/{RequestAnatomy-BtWt1cWY.mjs → RequestAnatomy-DN1SJVVL.mjs} +4 -4
- package/.output/server/_ssr/{ResponseView-CkKGYE6m.mjs → ResponseView-DW-kpJL5.mjs} +4 -4
- package/.output/server/_ssr/{StreamingChunkSequence-C7rB3osr.mjs → StreamingChunkSequence-BAE57Fae.mjs} +4 -4
- package/.output/server/_ssr/{index-D24WforP.mjs → index-C9ryJEna.mjs} +3 -3
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-CVqVjBzJ.mjs → router-DIIJGyML.mjs} +458 -10
- package/.output/server/{_tanstack-start-manifest_v-DSikvwru.mjs → _tanstack-start-manifest_v-C_v9-E5d.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +74 -68
- package/src/components/ProxyViewer.tsx +942 -297
- package/src/components/ProxyViewerContainer.tsx +199 -5
- package/src/components/groups/GroupsDialog.tsx +11 -10
- package/src/components/proxy-viewer/LogEntry.tsx +107 -56
- package/src/components/proxy-viewer/LogEntryHeader.tsx +12 -6
- package/src/components/ui/crab-logo.tsx +0 -50
- package/src/components/ui/dialog.tsx +1 -1
- package/src/proxy/logIndex.ts +188 -1
- package/src/proxy/store.ts +405 -3
- package/src/routes/api/logs.ts +36 -0
- package/.output/public/assets/ProxyViewerContainer-CE9pAX4c.js +0 -117
- package/.output/public/assets/RequestAnatomy-CJ7EPQGQ.js +0 -1
- package/.output/public/assets/ResponseView-xBmr54hB.js +0 -1
- package/.output/public/assets/_sessionId-CbWEG5ej.js +0 -1
- package/.output/public/assets/index-BfGJEb-2.css +0 -1
- package/.output/public/assets/index-D1MkoT4l.js +0 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef, useMemo, type JSX } from "react";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { CapturedLogSchema, type CapturedLog } from "../contracts";
|
|
4
|
+
import { fetchJson } from "../lib/apiClient";
|
|
5
|
+
import { InspectorGroupsListResponseSchema, type InspectorGroup } from "../lib/groupContract";
|
|
4
6
|
import { useStripConfig } from "../lib/useStripConfig";
|
|
5
7
|
import { OnboardingBanner } from "./OnboardingBanner";
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
ProxyViewer,
|
|
10
|
+
type LogPaginationControls,
|
|
11
|
+
type SessionMembershipEvidence,
|
|
12
|
+
} from "./ProxyViewer";
|
|
7
13
|
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
8
14
|
|
|
9
15
|
type SSEUpdate =
|
|
@@ -27,6 +33,47 @@ const SSEUpdateSchema = z.union([
|
|
|
27
33
|
}),
|
|
28
34
|
]);
|
|
29
35
|
|
|
36
|
+
const LogCursorPageSchema = z.object({
|
|
37
|
+
logs: z.array(CapturedLogSchema),
|
|
38
|
+
total: z.number().int().nonnegative(),
|
|
39
|
+
limit: z.number().int().positive(),
|
|
40
|
+
hasOlder: z.boolean(),
|
|
41
|
+
hasNewer: z.boolean(),
|
|
42
|
+
oldestLogId: z.number().int().positive().nullable(),
|
|
43
|
+
newestLogId: z.number().int().positive().nullable(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
type LogCursorPage = z.infer<typeof LogCursorPageSchema>;
|
|
47
|
+
|
|
48
|
+
type SessionPageRequest =
|
|
49
|
+
| { kind: "newest" }
|
|
50
|
+
| { kind: "oldest" }
|
|
51
|
+
| { kind: "older"; beforeLogId: number }
|
|
52
|
+
| { kind: "newer"; afterLogId: number };
|
|
53
|
+
|
|
54
|
+
function buildSessionMembershipEvidence(
|
|
55
|
+
groups: readonly InspectorGroup[],
|
|
56
|
+
sessionId: string,
|
|
57
|
+
): SessionMembershipEvidence[] {
|
|
58
|
+
const memberships: SessionMembershipEvidence[] = [];
|
|
59
|
+
for (const group of groups) {
|
|
60
|
+
for (const member of group.members) {
|
|
61
|
+
if (member.sessionId !== sessionId) continue;
|
|
62
|
+
memberships.push({
|
|
63
|
+
groupId: group.id,
|
|
64
|
+
groupTitle: group.title,
|
|
65
|
+
groupKind: group.kind,
|
|
66
|
+
groupStatus: group.status,
|
|
67
|
+
groupTask: group.task,
|
|
68
|
+
groupProject: group.project,
|
|
69
|
+
groupEvidence: group.evidence,
|
|
70
|
+
member,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return memberships;
|
|
75
|
+
}
|
|
76
|
+
|
|
30
77
|
function extractSessions(logs: CapturedLog[]): string[] {
|
|
31
78
|
const set = new Set<string>();
|
|
32
79
|
for (const l of logs) {
|
|
@@ -65,11 +112,41 @@ const DEBOUNCE_MS = 50;
|
|
|
65
112
|
const HASH_SCROLL_ATTEMPTS = 12;
|
|
66
113
|
const HASH_HIGHLIGHT_MS = 1800;
|
|
67
114
|
const MAX_CLIENT_LOGS = 500;
|
|
115
|
+
const SESSION_PAGE_LIMIT = 100;
|
|
68
116
|
|
|
69
117
|
function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
118
|
+
const params = new URLSearchParams();
|
|
119
|
+
if (sessionId !== undefined) {
|
|
120
|
+
params.set("sessionId", sessionId);
|
|
121
|
+
params.set("compact", "1");
|
|
122
|
+
}
|
|
123
|
+
const query = params.toString();
|
|
124
|
+
return query.length > 0 ? `/api/logs/stream?${query}` : "/api/logs/stream";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildSessionLogsPageUrl(sessionId: string, request: SessionPageRequest): string {
|
|
128
|
+
const params = new URLSearchParams({
|
|
129
|
+
cursor: "1",
|
|
130
|
+
compact: "1",
|
|
131
|
+
limit: String(SESSION_PAGE_LIMIT),
|
|
132
|
+
sessionId,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
switch (request.kind) {
|
|
136
|
+
case "oldest":
|
|
137
|
+
params.set("anchor", "oldest");
|
|
138
|
+
break;
|
|
139
|
+
case "older":
|
|
140
|
+
params.set("beforeLogId", String(request.beforeLogId));
|
|
141
|
+
break;
|
|
142
|
+
case "newer":
|
|
143
|
+
params.set("afterLogId", String(request.afterLogId));
|
|
144
|
+
break;
|
|
145
|
+
case "newest":
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return `/api/logs?${params.toString()}`;
|
|
73
150
|
}
|
|
74
151
|
|
|
75
152
|
function buildLogIndex(logs: readonly CapturedLog[]): Map<number, number> {
|
|
@@ -109,9 +186,14 @@ export function ProxyViewerContainer({
|
|
|
109
186
|
const [selectedModel, setSelectedModel] = useState("__all__");
|
|
110
187
|
const [viewMode, setViewMode] = useState<"simple" | "full">("simple");
|
|
111
188
|
const [error, setError] = useState<string | null>(null);
|
|
189
|
+
const [streamInitialized, setStreamInitialized] = useState(initialSessionId === undefined);
|
|
190
|
+
const [logPage, setLogPage] = useState<LogCursorPage | null>(null);
|
|
191
|
+
const [sessionPageLoading, setSessionPageLoading] = useState(initialSessionId !== undefined);
|
|
192
|
+
const [sessionMemberships, setSessionMemberships] = useState<SessionMembershipEvidence[]>([]);
|
|
112
193
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
113
194
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
114
195
|
const handledHashRef = useRef<string | null>(null);
|
|
196
|
+
const sessionPageRequestIdRef = useRef(0);
|
|
115
197
|
|
|
116
198
|
// O(1) log lookup by id
|
|
117
199
|
const logIndexRef = useRef<Map<number, number>>(new Map());
|
|
@@ -169,6 +251,7 @@ export function ProxyViewerContainer({
|
|
|
169
251
|
eventSourceRef.current.close();
|
|
170
252
|
}
|
|
171
253
|
|
|
254
|
+
setStreamInitialized(false);
|
|
172
255
|
const es = new EventSource(buildLogsStreamUrl(initialSessionId));
|
|
173
256
|
eventSourceRef.current = es;
|
|
174
257
|
|
|
@@ -193,6 +276,7 @@ export function ProxyViewerContainer({
|
|
|
193
276
|
const nextLogs = trimClientLogs(update.logs);
|
|
194
277
|
logIndexRef.current = buildLogIndex(nextLogs);
|
|
195
278
|
setAllLogs(nextLogs);
|
|
279
|
+
setStreamInitialized(true);
|
|
196
280
|
setError(null);
|
|
197
281
|
} else if (update.type === "update") {
|
|
198
282
|
scheduleUpdate(update.log);
|
|
@@ -204,6 +288,7 @@ export function ProxyViewerContainer({
|
|
|
204
288
|
|
|
205
289
|
es.onerror = () => {
|
|
206
290
|
setError("SSE connection lost, reconnecting...");
|
|
291
|
+
setStreamInitialized(true);
|
|
207
292
|
es.close();
|
|
208
293
|
if (reconnectTimeoutRef.current !== null) {
|
|
209
294
|
clearTimeout(reconnectTimeoutRef.current);
|
|
@@ -212,7 +297,71 @@ export function ProxyViewerContainer({
|
|
|
212
297
|
};
|
|
213
298
|
}, [initialSessionId, scheduleUpdate]);
|
|
214
299
|
|
|
300
|
+
const loadSessionPage = useCallback(
|
|
301
|
+
(request: SessionPageRequest) => {
|
|
302
|
+
if (initialSessionId === undefined) return;
|
|
303
|
+
|
|
304
|
+
const requestId = sessionPageRequestIdRef.current + 1;
|
|
305
|
+
sessionPageRequestIdRef.current = requestId;
|
|
306
|
+
setSessionPageLoading(true);
|
|
307
|
+
setError(null);
|
|
308
|
+
|
|
309
|
+
void fetchJson(buildSessionLogsPageUrl(initialSessionId, request), LogCursorPageSchema)
|
|
310
|
+
.then((page) => {
|
|
311
|
+
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
312
|
+
const nextLogs = page.logs;
|
|
313
|
+
logIndexRef.current = buildLogIndex(nextLogs);
|
|
314
|
+
setAllLogs(nextLogs);
|
|
315
|
+
setLogPage(page);
|
|
316
|
+
setSelectedSession(initialSessionId);
|
|
317
|
+
setSessionPageLoading(false);
|
|
318
|
+
setError(null);
|
|
319
|
+
})
|
|
320
|
+
.catch((err: unknown) => {
|
|
321
|
+
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
322
|
+
setAllLogs([]);
|
|
323
|
+
setLogPage(null);
|
|
324
|
+
setSessionPageLoading(false);
|
|
325
|
+
setError(err instanceof Error ? err.message : "Failed to load session logs");
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
[initialSessionId],
|
|
329
|
+
);
|
|
330
|
+
|
|
215
331
|
useEffect(() => {
|
|
332
|
+
if (initialSessionId === undefined) {
|
|
333
|
+
setSessionMemberships([]);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let cancelled = false;
|
|
338
|
+
void fetch("/api/groups")
|
|
339
|
+
.then(async (response) => {
|
|
340
|
+
if (!response.ok) return null;
|
|
341
|
+
const raw: unknown = await response.json();
|
|
342
|
+
const parsed = InspectorGroupsListResponseSchema.safeParse(raw);
|
|
343
|
+
return parsed.success ? parsed.data.groups : null;
|
|
344
|
+
})
|
|
345
|
+
.then((groups) => {
|
|
346
|
+
if (cancelled || groups === null) return;
|
|
347
|
+
setSessionMemberships(buildSessionMembershipEvidence(groups, initialSessionId));
|
|
348
|
+
})
|
|
349
|
+
.catch(() => {
|
|
350
|
+
if (!cancelled) setSessionMemberships([]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return () => {
|
|
354
|
+
cancelled = true;
|
|
355
|
+
};
|
|
356
|
+
}, [initialSessionId]);
|
|
357
|
+
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
if (initialSessionId === undefined) return;
|
|
360
|
+
loadSessionPage({ kind: "newest" });
|
|
361
|
+
}, [initialSessionId, loadSessionPage]);
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (initialSessionId !== undefined) return undefined;
|
|
216
365
|
connectSSE();
|
|
217
366
|
return () => {
|
|
218
367
|
if (eventSourceRef.current) {
|
|
@@ -228,7 +377,7 @@ export function ProxyViewerContainer({
|
|
|
228
377
|
flushTimerRef.current = null;
|
|
229
378
|
}
|
|
230
379
|
};
|
|
231
|
-
}, [connectSSE]);
|
|
380
|
+
}, [connectSSE, initialSessionId]);
|
|
232
381
|
|
|
233
382
|
useEffect(() => {
|
|
234
383
|
const hash = window.location.hash;
|
|
@@ -342,6 +491,48 @@ export function ProxyViewerContainer({
|
|
|
342
491
|
// Read the strip config once at the container so the virtualized list does
|
|
343
492
|
// not need N independent SWR subscriptions per row.
|
|
344
493
|
const { strip, captureMode, slowResponseThresholdSeconds, timeDisplayFormat } = useStripConfig();
|
|
494
|
+
const loadOldestPage = useCallback(() => {
|
|
495
|
+
loadSessionPage({ kind: "oldest" });
|
|
496
|
+
}, [loadSessionPage]);
|
|
497
|
+
const loadNewestPage = useCallback(() => {
|
|
498
|
+
loadSessionPage({ kind: "newest" });
|
|
499
|
+
}, [loadSessionPage]);
|
|
500
|
+
const loadOlderPage = useCallback(() => {
|
|
501
|
+
const firstLog = allLogs[0];
|
|
502
|
+
const beforeLogId = logPage?.oldestLogId ?? firstLog?.id ?? null;
|
|
503
|
+
if (beforeLogId === null) return;
|
|
504
|
+
loadSessionPage({ kind: "older", beforeLogId });
|
|
505
|
+
}, [allLogs, loadSessionPage, logPage]);
|
|
506
|
+
const loadNewerPage = useCallback(() => {
|
|
507
|
+
const lastLog = allLogs[allLogs.length - 1];
|
|
508
|
+
const afterLogId = logPage?.newestLogId ?? lastLog?.id ?? null;
|
|
509
|
+
if (afterLogId === null) return;
|
|
510
|
+
loadSessionPage({ kind: "newer", afterLogId });
|
|
511
|
+
}, [allLogs, loadSessionPage, logPage]);
|
|
512
|
+
const pagination = useMemo<LogPaginationControls | undefined>(() => {
|
|
513
|
+
if (initialSessionId === undefined) return undefined;
|
|
514
|
+
return {
|
|
515
|
+
isLoading: sessionPageLoading,
|
|
516
|
+
total: logPage?.total ?? null,
|
|
517
|
+
pageSize: logPage?.limit ?? SESSION_PAGE_LIMIT,
|
|
518
|
+
hasOlder: logPage?.hasOlder ?? false,
|
|
519
|
+
hasNewer: logPage?.hasNewer ?? false,
|
|
520
|
+
oldestLogId: logPage?.oldestLogId ?? null,
|
|
521
|
+
newestLogId: logPage?.newestLogId ?? null,
|
|
522
|
+
onOldest: loadOldestPage,
|
|
523
|
+
onOlder: loadOlderPage,
|
|
524
|
+
onNewer: loadNewerPage,
|
|
525
|
+
onNewest: loadNewestPage,
|
|
526
|
+
};
|
|
527
|
+
}, [
|
|
528
|
+
initialSessionId,
|
|
529
|
+
loadNewestPage,
|
|
530
|
+
loadNewerPage,
|
|
531
|
+
loadOlderPage,
|
|
532
|
+
loadOldestPage,
|
|
533
|
+
logPage,
|
|
534
|
+
sessionPageLoading,
|
|
535
|
+
]);
|
|
345
536
|
|
|
346
537
|
return (
|
|
347
538
|
<>
|
|
@@ -361,6 +552,9 @@ export function ProxyViewerContainer({
|
|
|
361
552
|
onModelChange={setSelectedModel}
|
|
362
553
|
onClearAll={handleClearAll}
|
|
363
554
|
onClearGroup={handleClearGroup}
|
|
555
|
+
isLoading={initialSessionId === undefined ? !streamInitialized : sessionPageLoading}
|
|
556
|
+
pagination={pagination}
|
|
557
|
+
sessionMemberships={sessionMemberships}
|
|
364
558
|
viewMode={viewMode}
|
|
365
559
|
onViewModeChange={setViewMode}
|
|
366
560
|
captureMode={captureMode}
|
|
@@ -920,21 +920,22 @@ function MemberRow({ row }: { row: GroupMemberRow }): JSX.Element {
|
|
|
920
920
|
const errors = session?.errorCount ?? null;
|
|
921
921
|
const tokens = session?.tokenUsage.total ?? null;
|
|
922
922
|
const firstChunkMs = log?.firstChunkMs ?? null;
|
|
923
|
+
const sessionLabel = firstNonEmpty(member.label, member.sessionId);
|
|
923
924
|
|
|
924
925
|
return (
|
|
925
926
|
<tr className="border-b border-border last:border-0">
|
|
926
927
|
<td className="max-w-56 px-3 py-2">
|
|
927
|
-
<
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
</
|
|
928
|
+
<a
|
|
929
|
+
href={sessionPath}
|
|
930
|
+
target="_blank"
|
|
931
|
+
rel="noreferrer"
|
|
932
|
+
className="inline-flex max-w-full min-w-0 items-center gap-2 rounded-sm font-mono text-xs text-primary outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
933
|
+
aria-label={`Open session ${sessionLabel}`}
|
|
934
|
+
title={`Open session ${sessionLabel}`}
|
|
935
|
+
>
|
|
936
|
+
<span className="min-w-0 truncate">{sessionLabel}</span>
|
|
936
937
|
<ExternalLink className="size-3 shrink-0 text-muted-foreground" />
|
|
937
|
-
</
|
|
938
|
+
</a>
|
|
938
939
|
<div className="mt-0.5 truncate text-[11px] text-muted-foreground">
|
|
939
940
|
{displayText(member.agent)}
|
|
940
941
|
</div>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AlertTriangle, GitCompareArrows } from "lucide-react";
|
|
2
2
|
import { Suspense, type JSX } from "react";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState, memo } from "react";
|
|
4
|
-
import type
|
|
4
|
+
import { CapturedLogSchema, type CapturedLog } from "../../contracts";
|
|
5
|
+
import { fetchJson } from "../../lib/apiClient";
|
|
5
6
|
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
6
7
|
import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
|
|
7
8
|
import { Button } from "../ui/button";
|
|
@@ -50,6 +51,12 @@ function resolveFocusedTab(tab: LogFocusTab, anatomySegments: AnatomySegment[] |
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
type BodyHydrationState = "idle" | "loading" | "failed";
|
|
55
|
+
|
|
56
|
+
function shouldHydrateLogBody(log: CapturedLog): boolean {
|
|
57
|
+
return log.bodyContentMode === "compact" || log.bodyContentMode === "truncated";
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
export type LogEntryProps = {
|
|
54
61
|
log: CapturedLog;
|
|
55
62
|
viewMode?: "simple" | "full";
|
|
@@ -144,53 +151,56 @@ export const LogEntry = memo(function ({
|
|
|
144
151
|
const [requestDiff, setRequestDiff] = useState<boolean>(false);
|
|
145
152
|
const [activeTab, setActiveTab] = useState("request");
|
|
146
153
|
const [expandToPath, setExpandToPath] = useState<string | null>(null);
|
|
154
|
+
const [hydratedLog, setHydratedLog] = useState<CapturedLog | null>(null);
|
|
155
|
+
const [bodyHydrationState, setBodyHydrationState] = useState<BodyHydrationState>("idle");
|
|
147
156
|
const requestJsonRef = useRef<HTMLDivElement | null>(null);
|
|
148
|
-
const
|
|
157
|
+
const displayLog = hydratedLog ?? log;
|
|
158
|
+
const resolvedFormat = resolveLogFormat(displayLog);
|
|
149
159
|
const adapter = getLogFormatAdapter(resolvedFormat);
|
|
150
160
|
const requestAnalysis = useMemo(
|
|
151
|
-
() => adapter.analyzeRequest(
|
|
152
|
-
[adapter,
|
|
161
|
+
() => adapter.analyzeRequest(displayLog.rawRequestBody),
|
|
162
|
+
[adapter, displayLog.rawRequestBody],
|
|
153
163
|
);
|
|
154
164
|
const responseAnalysis = useMemo(
|
|
155
|
-
() => adapter.analyzeResponse(
|
|
156
|
-
[adapter,
|
|
165
|
+
() => adapter.analyzeResponse(displayLog.responseText),
|
|
166
|
+
[adapter, displayLog.responseText],
|
|
157
167
|
);
|
|
158
168
|
const strippedRequestBody = useMemo(() => {
|
|
159
|
-
if (!strip || resolvedFormat !== "anthropic" ||
|
|
169
|
+
if (!strip || resolvedFormat !== "anthropic" || displayLog.rawRequestBody === null) {
|
|
160
170
|
return null;
|
|
161
171
|
}
|
|
162
|
-
return stripClaudeCodeBillingHeader(
|
|
163
|
-
}, [
|
|
164
|
-
const displayedRequestBody = strippedRequestBody ??
|
|
172
|
+
return stripClaudeCodeBillingHeader(displayLog.rawRequestBody).body;
|
|
173
|
+
}, [displayLog.rawRequestBody, resolvedFormat, strip]);
|
|
174
|
+
const displayedRequestBody = strippedRequestBody ?? displayLog.rawRequestBody;
|
|
165
175
|
const requestExpansion = useJsonBulkExpansion(displayedRequestBody);
|
|
166
|
-
const rawRequestExpansion = useJsonBulkExpansion(
|
|
167
|
-
const responseExpansion = useJsonBulkExpansion(
|
|
176
|
+
const rawRequestExpansion = useJsonBulkExpansion(displayLog.rawRequestBody);
|
|
177
|
+
const responseExpansion = useJsonBulkExpansion(displayLog.responseText);
|
|
168
178
|
|
|
169
179
|
// Headers are rendered as a flat list, so we copy them as pretty-printed JSON.
|
|
170
180
|
// Only build the string when there's at least one entry — empty headers would
|
|
171
181
|
// otherwise copy "{}", which is misleading.
|
|
172
182
|
const headersText = useMemo(
|
|
173
183
|
() =>
|
|
174
|
-
|
|
175
|
-
? JSON.stringify(
|
|
184
|
+
displayLog.headers && Object.keys(displayLog.headers).length > 0
|
|
185
|
+
? JSON.stringify(displayLog.headers, null, 2)
|
|
176
186
|
: null,
|
|
177
|
-
[
|
|
187
|
+
[displayLog.headers],
|
|
178
188
|
);
|
|
179
189
|
const rawHeadersText = useMemo(
|
|
180
190
|
() =>
|
|
181
|
-
|
|
182
|
-
? JSON.stringify(
|
|
191
|
+
displayLog.rawHeaders && Object.keys(displayLog.rawHeaders).length > 0
|
|
192
|
+
? JSON.stringify(displayLog.rawHeaders, null, 2)
|
|
183
193
|
: null,
|
|
184
|
-
[
|
|
194
|
+
[displayLog.rawHeaders],
|
|
185
195
|
);
|
|
186
196
|
|
|
187
197
|
// One copy-feedback hook per JSON-bearing tab, so each tab can surface its
|
|
188
198
|
// own Copy button in the header with independent "Copied!" feedback.
|
|
189
199
|
const requestCopy = useCopyFeedback(displayedRequestBody);
|
|
190
|
-
const rawRequestCopy = useCopyFeedback(
|
|
200
|
+
const rawRequestCopy = useCopyFeedback(displayLog.rawRequestBody);
|
|
191
201
|
const headersCopy = useCopyFeedback(headersText);
|
|
192
202
|
const rawHeadersCopy = useCopyFeedback(rawHeadersText);
|
|
193
|
-
const responseCopy = useCopyFeedback(
|
|
203
|
+
const responseCopy = useCopyFeedback(displayLog.responseText);
|
|
194
204
|
|
|
195
205
|
// Per-tab action bundles consumed by the header. The header renders the
|
|
196
206
|
// entry whose key matches `activeTab`. Tabs without an entry (Context,
|
|
@@ -214,7 +224,7 @@ export const LogEntry = memo(function ({
|
|
|
214
224
|
resolvedFormat,
|
|
215
225
|
viewMode,
|
|
216
226
|
strip,
|
|
217
|
-
|
|
227
|
+
displayLog.rawRequestBody !== null,
|
|
218
228
|
)
|
|
219
229
|
? { active: requestDiff, onToggle: () => setRequestDiff(!requestDiff) }
|
|
220
230
|
: undefined,
|
|
@@ -222,12 +232,12 @@ export const LogEntry = memo(function ({
|
|
|
222
232
|
onCompareWithPrevious === undefined
|
|
223
233
|
? undefined
|
|
224
234
|
: () => {
|
|
225
|
-
onCompareWithPrevious(
|
|
235
|
+
onCompareWithPrevious(displayLog);
|
|
226
236
|
},
|
|
227
237
|
},
|
|
228
238
|
"raw-request": {
|
|
229
239
|
copyLabel: "Copy raw request",
|
|
230
|
-
copyText:
|
|
240
|
+
copyText: displayLog.rawRequestBody,
|
|
231
241
|
copyCopied: rawRequestCopy.copied,
|
|
232
242
|
onCopy: rawRequestCopy.copy,
|
|
233
243
|
expansion: {
|
|
@@ -253,7 +263,7 @@ export const LogEntry = memo(function ({
|
|
|
253
263
|
},
|
|
254
264
|
raw: {
|
|
255
265
|
copyLabel: "Copy response",
|
|
256
|
-
copyText:
|
|
266
|
+
copyText: displayLog.responseText,
|
|
257
267
|
copyCopied: responseCopy.copied,
|
|
258
268
|
onCopy: responseCopy.copy,
|
|
259
269
|
expansion: {
|
|
@@ -268,14 +278,15 @@ export const LogEntry = memo(function ({
|
|
|
268
278
|
requestCopy,
|
|
269
279
|
requestExpansion,
|
|
270
280
|
requestDiff,
|
|
271
|
-
|
|
281
|
+
displayLog,
|
|
282
|
+
displayLog.rawRequestBody,
|
|
272
283
|
rawRequestCopy,
|
|
273
284
|
rawRequestExpansion,
|
|
274
285
|
headersText,
|
|
275
286
|
headersCopy,
|
|
276
287
|
rawHeadersText,
|
|
277
288
|
rawHeadersCopy,
|
|
278
|
-
|
|
289
|
+
displayLog.responseText,
|
|
279
290
|
responseCopy,
|
|
280
291
|
responseExpansion,
|
|
281
292
|
resolvedFormat,
|
|
@@ -295,7 +306,7 @@ export const LogEntry = memo(function ({
|
|
|
295
306
|
() => parseRequestTools(requestExpansion.parsedData),
|
|
296
307
|
[requestExpansion.parsedData],
|
|
297
308
|
);
|
|
298
|
-
const warnings =
|
|
309
|
+
const warnings = displayLog.warnings ?? [];
|
|
299
310
|
const anatomyPaths = useMemo(() => {
|
|
300
311
|
if (anatomySegments === null) return undefined;
|
|
301
312
|
return new Set(anatomySegments.map((s) => s.path));
|
|
@@ -316,6 +327,34 @@ export const LogEntry = memo(function ({
|
|
|
316
327
|
setExpanded(nextExpanded);
|
|
317
328
|
}, [anatomySegments, expanded]);
|
|
318
329
|
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
setHydratedLog(null);
|
|
332
|
+
setBodyHydrationState("idle");
|
|
333
|
+
}, [log.id]);
|
|
334
|
+
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (!expanded) return;
|
|
337
|
+
if (!shouldHydrateLogBody(log)) return;
|
|
338
|
+
if (hydratedLog !== null || bodyHydrationState !== "idle") return;
|
|
339
|
+
|
|
340
|
+
let cancelled = false;
|
|
341
|
+
setBodyHydrationState("loading");
|
|
342
|
+
void fetchJson(`/api/logs/${String(log.id)}`, CapturedLogSchema)
|
|
343
|
+
.then((fullLog) => {
|
|
344
|
+
if (cancelled) return;
|
|
345
|
+
setHydratedLog(fullLog);
|
|
346
|
+
setBodyHydrationState("idle");
|
|
347
|
+
})
|
|
348
|
+
.catch(() => {
|
|
349
|
+
if (cancelled) return;
|
|
350
|
+
setBodyHydrationState("failed");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return () => {
|
|
354
|
+
cancelled = true;
|
|
355
|
+
};
|
|
356
|
+
}, [bodyHydrationState, expanded, hydratedLog, log.bodyContentMode, log.id]);
|
|
357
|
+
|
|
319
358
|
useEffect(() => {
|
|
320
359
|
const handleLogFocusRequest = (event: Event): void => {
|
|
321
360
|
const request = readLogFocusRequest(event);
|
|
@@ -335,7 +374,7 @@ export const LogEntry = memo(function ({
|
|
|
335
374
|
<TooltipProvider>
|
|
336
375
|
<div className="border border-border rounded-lg mb-1 overflow-hidden">
|
|
337
376
|
<LogEntryHeader
|
|
338
|
-
log={
|
|
377
|
+
log={displayLog}
|
|
339
378
|
messageCount={requestAnalysis.messageCount}
|
|
340
379
|
toolCount={requestAnalysis.toolCount}
|
|
341
380
|
expanded={expanded}
|
|
@@ -373,7 +412,7 @@ export const LogEntry = memo(function ({
|
|
|
373
412
|
<TabsContent value="raw-request">
|
|
374
413
|
{activeTab === "raw-request" && (
|
|
375
414
|
<div className="px-4 pt-1 pb-3">
|
|
376
|
-
{
|
|
415
|
+
{displayLog.rawRequestBody === null ? (
|
|
377
416
|
<p className="text-xs text-muted-foreground italic">No request body</p>
|
|
378
417
|
) : rawRequestExpansion.parsedData !== null ? (
|
|
379
418
|
<Suspense fallback={<TabFallback />}>
|
|
@@ -385,7 +424,7 @@ export const LogEntry = memo(function ({
|
|
|
385
424
|
</Suspense>
|
|
386
425
|
) : (
|
|
387
426
|
<pre className="font-mono text-xs whitespace-pre-wrap break-words">
|
|
388
|
-
{
|
|
427
|
+
{displayLog.rawRequestBody}
|
|
389
428
|
</pre>
|
|
390
429
|
)}
|
|
391
430
|
</div>
|
|
@@ -396,6 +435,17 @@ export const LogEntry = memo(function ({
|
|
|
396
435
|
<TabsContent value="request">
|
|
397
436
|
{activeTab === "request" && (
|
|
398
437
|
<div className="px-4 pt-1 pb-3">
|
|
438
|
+
{bodyHydrationState === "loading" && (
|
|
439
|
+
<div className="mb-3 rounded border border-border bg-muted/30 p-3 text-xs text-muted-foreground">
|
|
440
|
+
Loading full request and response bodies...
|
|
441
|
+
</div>
|
|
442
|
+
)}
|
|
443
|
+
{bodyHydrationState === "failed" && (
|
|
444
|
+
<div className="mb-3 rounded border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-200">
|
|
445
|
+
Full request and response bodies could not be loaded. Compact metadata is
|
|
446
|
+
still available.
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
399
449
|
{warnings.length > 0 && (
|
|
400
450
|
<div className="mb-3 rounded border border-amber-500/30 bg-amber-500/10 p-3 text-xs">
|
|
401
451
|
<div className="flex items-center gap-2 font-semibold text-amber-300">
|
|
@@ -418,7 +468,7 @@ export const LogEntry = memo(function ({
|
|
|
418
468
|
the diff or JSON view, depending on toggle state. */}
|
|
419
469
|
{requestDiff ? (
|
|
420
470
|
<RequestDiffContent
|
|
421
|
-
rawBody={
|
|
471
|
+
rawBody={displayLog.rawRequestBody}
|
|
422
472
|
displayedBody={displayedRequestBody}
|
|
423
473
|
emptyLabel="No transformation applied; raw and sent request bodies are identical."
|
|
424
474
|
/>
|
|
@@ -453,8 +503,8 @@ export const LogEntry = memo(function ({
|
|
|
453
503
|
<Suspense fallback={<TabFallback />}>
|
|
454
504
|
<LazyRequestAnatomy
|
|
455
505
|
parsed={requestExpansion.parsedData}
|
|
456
|
-
inputTokens={
|
|
457
|
-
model={
|
|
506
|
+
inputTokens={displayLog.inputTokens ?? null}
|
|
507
|
+
model={displayLog.model}
|
|
458
508
|
segments={anatomySegments}
|
|
459
509
|
onSegmentActivate={jumpToAnatomySegment}
|
|
460
510
|
/>
|
|
@@ -471,7 +521,8 @@ export const LogEntry = memo(function ({
|
|
|
471
521
|
<div className="flex justify-end gap-2 mb-2">
|
|
472
522
|
{shouldShowHeadersDiffButton(
|
|
473
523
|
viewMode,
|
|
474
|
-
|
|
524
|
+
displayLog.rawHeaders !== undefined &&
|
|
525
|
+
Object.keys(displayLog.rawHeaders).length > 0,
|
|
475
526
|
) && (
|
|
476
527
|
<DiffToggleButton
|
|
477
528
|
active={headersDiff}
|
|
@@ -484,13 +535,13 @@ export const LogEntry = memo(function ({
|
|
|
484
535
|
</div>
|
|
485
536
|
{headersDiff ? (
|
|
486
537
|
<HeadersDiffContent
|
|
487
|
-
rawHeaders={
|
|
488
|
-
headers={
|
|
538
|
+
rawHeaders={displayLog.rawHeaders}
|
|
539
|
+
headers={displayLog.headers}
|
|
489
540
|
emptyLabel="No transformation applied; raw and processed headers are identical."
|
|
490
541
|
/>
|
|
491
|
-
) :
|
|
542
|
+
) : displayLog.headers && Object.keys(displayLog.headers).length > 0 ? (
|
|
492
543
|
<div className="space-y-1 font-mono text-xs">
|
|
493
|
-
{Object.entries(
|
|
544
|
+
{Object.entries(displayLog.headers)
|
|
494
545
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
495
546
|
.map(([key, value]) => (
|
|
496
547
|
<div key={key} className="flex gap-2">
|
|
@@ -516,9 +567,9 @@ export const LogEntry = memo(function ({
|
|
|
516
567
|
{activeTab === "raw-headers" && (
|
|
517
568
|
<div className="px-4 pt-1 pb-3">
|
|
518
569
|
{/* Copy lives in the log header. */}
|
|
519
|
-
{
|
|
570
|
+
{displayLog.rawHeaders && Object.keys(displayLog.rawHeaders).length > 0 ? (
|
|
520
571
|
<div className="space-y-1 font-mono text-xs">
|
|
521
|
-
{Object.entries(
|
|
572
|
+
{Object.entries(displayLog.rawHeaders)
|
|
522
573
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
523
574
|
.map(([key, value]) => (
|
|
524
575
|
<div key={key} className="flex gap-2">
|
|
@@ -544,16 +595,16 @@ export const LogEntry = memo(function ({
|
|
|
544
595
|
<TabsContent value="raw">
|
|
545
596
|
{activeTab === "raw" && (
|
|
546
597
|
<div className="px-4 pt-1 pb-3 space-y-3">
|
|
547
|
-
{
|
|
598
|
+
{displayLog.error !== undefined && displayLog.error !== null && (
|
|
548
599
|
<div className="rounded border border-destructive/50 bg-destructive/10 p-3 text-xs">
|
|
549
600
|
<div className="font-semibold text-destructive mb-1">SSE Error</div>
|
|
550
|
-
<div className="text-muted-foreground font-mono">{
|
|
601
|
+
<div className="text-muted-foreground font-mono">{displayLog.error}</div>
|
|
551
602
|
</div>
|
|
552
603
|
)}
|
|
553
|
-
{
|
|
604
|
+
{displayLog.responseText !== null ? (
|
|
554
605
|
<Suspense fallback={<TabFallback />}>
|
|
555
606
|
<LazyJsonViewerFromString
|
|
556
|
-
text={
|
|
607
|
+
text={displayLog.responseText}
|
|
557
608
|
defaultExpandDepth={0}
|
|
558
609
|
bulkDepth={responseExpansion.bulkDepth}
|
|
559
610
|
bulkRevision={responseExpansion.bulkRevision}
|
|
@@ -562,11 +613,11 @@ export const LogEntry = memo(function ({
|
|
|
562
613
|
) : (
|
|
563
614
|
<p className="text-xs text-muted-foreground italic">No response</p>
|
|
564
615
|
)}
|
|
565
|
-
{
|
|
616
|
+
{displayLog.streaming === true && (
|
|
566
617
|
<Suspense fallback={<TabFallback />}>
|
|
567
618
|
<LazyStreamingChunkSequence
|
|
568
|
-
logId={
|
|
569
|
-
truncated={
|
|
619
|
+
logId={displayLog.id}
|
|
620
|
+
truncated={displayLog.streamingChunksPath !== null}
|
|
570
621
|
/>
|
|
571
622
|
</Suspense>
|
|
572
623
|
)}
|
|
@@ -579,15 +630,15 @@ export const LogEntry = memo(function ({
|
|
|
579
630
|
<div className="px-4 pt-1 pb-3">
|
|
580
631
|
<Suspense fallback={<TabFallback />}>
|
|
581
632
|
<LazyResponseView
|
|
582
|
-
responseText={
|
|
583
|
-
responseStatus={
|
|
584
|
-
streaming={
|
|
585
|
-
inputTokens={
|
|
586
|
-
outputTokens={
|
|
587
|
-
cacheCreationInputTokens={
|
|
588
|
-
cacheReadInputTokens={
|
|
633
|
+
responseText={displayLog.responseText}
|
|
634
|
+
responseStatus={displayLog.responseStatus}
|
|
635
|
+
streaming={displayLog.streaming}
|
|
636
|
+
inputTokens={displayLog.inputTokens}
|
|
637
|
+
outputTokens={displayLog.outputTokens}
|
|
638
|
+
cacheCreationInputTokens={displayLog.cacheCreationInputTokens}
|
|
639
|
+
cacheReadInputTokens={displayLog.cacheReadInputTokens}
|
|
589
640
|
apiFormat={resolvedFormat}
|
|
590
|
-
error={
|
|
641
|
+
error={displayLog.error}
|
|
591
642
|
/>
|
|
592
643
|
</Suspense>
|
|
593
644
|
</div>
|
|
@@ -598,7 +649,7 @@ export const LogEntry = memo(function ({
|
|
|
598
649
|
)}
|
|
599
650
|
</div>
|
|
600
651
|
<Suspense fallback={null}>
|
|
601
|
-
<LazyReplayDialog log={
|
|
652
|
+
<LazyReplayDialog log={displayLog} open={replayOpen} onOpenChange={setReplayOpen} />
|
|
602
653
|
</Suspense>
|
|
603
654
|
</TooltipProvider>
|
|
604
655
|
);
|