@tonyclaw/agent-inspector 2.1.16 → 2.1.18
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/cli.js +57 -13
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-Ccrrcx1j.js → CompareDrawer-Fqqw5Lci.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CP_RzwW9.js +106 -0
- package/.output/public/assets/{ReplayDialog-C7axhr-l.js → ReplayDialog-hYhz_i6W.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-WOptg8j_.js → RequestAnatomy-CTuWbtwC.js} +1 -1
- package/.output/public/assets/{ResponseView-CSAcxh_M.js → ResponseView-CsZ7S5Gv.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-DJS5KhPx.js → StreamingChunkSequence-PIbJMotU.js} +1 -1
- package/.output/public/assets/_sessionId-CUGA9qU3.js +1 -0
- package/.output/public/assets/{index-DMPNh46t.js → index-F7mynwuH.js} +1 -1
- package/.output/public/assets/index-G4dTUobw.js +1 -0
- package/.output/public/assets/index-gvYkgMPF.css +1 -0
- package/.output/public/assets/{json-viewer-inX5QSa3.js → json-viewer-DCXtNmmD.js} +1 -1
- package/.output/public/assets/{main-aCAKYGSD.js → main-BJZf7_Ib.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +220 -202
- package/.output/server/{_sessionId-C0fhvQnn.mjs → _sessionId-DjSHDNh8.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CO-Ti5sg.mjs → CompareDrawer-Dal52IFo.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-N4J7uBCX.mjs → ProxyViewerContainer-CVqFZGff.mjs} +1833 -1366
- package/.output/server/_ssr/{ReplayDialog-J1VxC0In.mjs → ReplayDialog-BOEgdMes.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-DBH-F3rq.mjs → RequestAnatomy-gch7_vCV.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-DLFwV7Hd.mjs → ResponseView-lEWPy-H4.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-BywmuND0.mjs → StreamingChunkSequence-DnudbJ1O.mjs} +2 -2
- package/.output/server/_ssr/{index-CStymIc0.mjs → index-Cza8zDrW.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-DlQu0N1x.mjs → json-viewer-C2lIVEX3.mjs} +3 -3
- package/.output/server/_ssr/{router-CQ4Zxhmr.mjs → router-bF9uzd1k.mjs} +939 -113
- package/.output/server/{_tanstack-start-manifest_v-BHeJBGfo.mjs → _tanstack-start-manifest_v-ErvW-mtr.mjs} +1 -1
- package/.output/server/index.mjs +74 -74
- package/README.md +57 -2
- package/package.json +1 -1
- package/src/cli/startupOutput.ts +16 -0
- package/src/cli.ts +61 -15
- package/src/components/ProxyViewer.tsx +395 -64
- package/src/components/providers/ProviderCard.tsx +132 -31
- package/src/components/providers/ProviderForm.tsx +43 -11
- package/src/components/providers/ProvidersPanel.tsx +1 -1
- package/src/components/proxy-viewer/ConversationHeader.tsx +133 -112
- package/src/components/proxy-viewer/LogEntry.tsx +109 -26
- package/src/components/proxy-viewer/LogEntryHeader.tsx +339 -329
- package/src/components/proxy-viewer/ThreadConnector.tsx +49 -46
- package/src/components/proxy-viewer/TurnGroup.tsx +22 -62
- package/src/components/proxy-viewer/bodyHydration.ts +56 -0
- package/src/lib/providerTestContract.ts +56 -10
- package/src/lib/providerTestPrompt.ts +23 -1
- package/src/lib/upstreamUrl.ts +105 -1
- package/src/mcp/server.ts +275 -0
- package/src/mcp/toolHandlers.ts +381 -4
- package/src/proxy/logSearch.ts +3 -0
- package/src/proxy/store.ts +40 -15
- package/src/routes/api/logs.ts +8 -2
- package/src/routes/api/providers.$providerId.test.log.ts +85 -10
- package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +0 -106
- package/.output/public/assets/_sessionId-DUSalzKH.js +0 -1
- package/.output/public/assets/index-BPpA21dY.css +0 -1
- package/.output/public/assets/index-Bt0Az2I2.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useState } from "react";
|
|
1
|
+
import { type ReactNode, useCallback, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
ChevronDown,
|
|
4
4
|
ChevronRight,
|
|
@@ -21,6 +21,34 @@ import { ClientLogo, clientAppLabel, detectClientApp } from "../clients/ClientLo
|
|
|
21
21
|
import { ProviderLogoStack } from "./ProviderLogoStack";
|
|
22
22
|
import { resolveLogFormat } from "./log-formats";
|
|
23
23
|
|
|
24
|
+
const HEADER_ACTION_BUTTON_CLASS =
|
|
25
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground";
|
|
26
|
+
|
|
27
|
+
function HeaderMetric({
|
|
28
|
+
icon,
|
|
29
|
+
children,
|
|
30
|
+
title,
|
|
31
|
+
className,
|
|
32
|
+
}: {
|
|
33
|
+
icon: ReactNode;
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
title?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
}): JSX.Element {
|
|
38
|
+
return (
|
|
39
|
+
<span
|
|
40
|
+
className={cn(
|
|
41
|
+
"border border-border bg-muted/30 inline-flex h-7 min-w-0 items-center gap-1.5 rounded-md px-2 text-xs text-muted-foreground",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
title={title}
|
|
45
|
+
>
|
|
46
|
+
{icon}
|
|
47
|
+
<span className="min-w-0 font-mono tabular-nums">{children}</span>
|
|
48
|
+
</span>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
24
52
|
export type ConversationHeaderProps = {
|
|
25
53
|
conversationId: string;
|
|
26
54
|
startTime: string;
|
|
@@ -112,6 +140,9 @@ export function ConversationHeader({
|
|
|
112
140
|
? conversationId.slice(0, 12) + "..." + conversationId.slice(-12)
|
|
113
141
|
: conversationId;
|
|
114
142
|
|
|
143
|
+
const timeLabel = formatTimestampRange(startTime, endTime, timeDisplayFormat);
|
|
144
|
+
const callsLabel = `${totalCalls} call${totalCalls !== 1 ? "s" : ""}`;
|
|
145
|
+
|
|
115
146
|
return (
|
|
116
147
|
<div
|
|
117
148
|
role="button"
|
|
@@ -119,11 +150,11 @@ export function ConversationHeader({
|
|
|
119
150
|
data-nav-id={`conv-${conversationId}`}
|
|
120
151
|
data-nav-action={expanded ? "collapse" : "expand"}
|
|
121
152
|
className={cn(
|
|
122
|
-
"border border-border bg-card shadow-sm flex cursor-pointer items-
|
|
153
|
+
"border border-border bg-card shadow-sm flex cursor-pointer items-start gap-3 rounded-[8px] px-3 py-2.5 transition-colors",
|
|
123
154
|
"hover:border-cyan-200/20",
|
|
124
155
|
"select-none",
|
|
125
156
|
"sticky top-0 z-10 mb-2",
|
|
126
|
-
"focus-visible:ring-
|
|
157
|
+
"focus-visible:ring-1 focus-visible:ring-cyan-300/35 focus-visible:outline-none",
|
|
127
158
|
)}
|
|
128
159
|
onClick={onToggle}
|
|
129
160
|
onKeyDown={(e) => {
|
|
@@ -135,143 +166,133 @@ export function ConversationHeader({
|
|
|
135
166
|
}}
|
|
136
167
|
>
|
|
137
168
|
{/* Provider logos: first visual element whenever model/provider logos are available. */}
|
|
138
|
-
<ProviderLogoStack
|
|
169
|
+
<ProviderLogoStack
|
|
170
|
+
items={providerLogoItems}
|
|
171
|
+
ariaLabel="Models used in this session"
|
|
172
|
+
className="mt-0.5"
|
|
173
|
+
/>
|
|
139
174
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
<span className="min-w-0 flex-1">
|
|
176
|
+
<span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
177
|
+
{isFallbackConversation && showProcessMetadata ? (
|
|
178
|
+
clientPid !== null && clientPid !== undefined ? (
|
|
179
|
+
<span
|
|
180
|
+
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-violet-200"
|
|
181
|
+
title={`PID ${clientPid}`}
|
|
182
|
+
>
|
|
183
|
+
PID {clientPid}
|
|
184
|
+
</span>
|
|
185
|
+
) : (
|
|
186
|
+
<span
|
|
187
|
+
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-violet-200"
|
|
188
|
+
title={conversationId}
|
|
189
|
+
>
|
|
190
|
+
{conversationId}
|
|
191
|
+
</span>
|
|
192
|
+
)
|
|
193
|
+
) : (
|
|
151
194
|
<span
|
|
152
|
-
className="
|
|
153
|
-
title={
|
|
195
|
+
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-violet-200"
|
|
196
|
+
title={conversationId}
|
|
154
197
|
>
|
|
155
|
-
|
|
156
|
-
</span>
|
|
157
|
-
) : (
|
|
158
|
-
<span className="shrink-0 font-mono text-xs font-semibold text-violet-200">
|
|
159
|
-
{conversationId}
|
|
198
|
+
{isFallbackConversation ? "sessionless" : compactSessionLabel}
|
|
160
199
|
</span>
|
|
161
200
|
)}
|
|
162
|
-
|
|
201
|
+
|
|
202
|
+
{!isFallbackConversation &&
|
|
203
|
+
showProcessMetadata &&
|
|
204
|
+
clientPid !== null &&
|
|
205
|
+
clientPid !== undefined && (
|
|
206
|
+
<span
|
|
207
|
+
className="border border-border bg-muted/40 shrink-0 rounded px-1.5 py-0.5 font-mono text-xs tabular-nums text-muted-foreground"
|
|
208
|
+
title={`Client process ID ${clientPid}`}
|
|
209
|
+
>
|
|
210
|
+
PID {clientPid}
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{isFallbackConversation &&
|
|
215
|
+
showProcessMetadata &&
|
|
216
|
+
clientProjectFolder !== null &&
|
|
163
217
|
clientProjectFolder !== undefined &&
|
|
164
218
|
clientProjectFolder !== "" && (
|
|
165
219
|
<span
|
|
166
|
-
className="
|
|
220
|
+
className="min-w-0 max-w-[240px] truncate font-mono text-xs text-muted-foreground"
|
|
167
221
|
title={clientProjectFolder}
|
|
168
222
|
>
|
|
169
223
|
{clientProjectFolder}
|
|
170
224
|
</span>
|
|
171
225
|
)}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<>
|
|
175
|
-
<span
|
|
176
|
-
className="shrink-0 font-mono text-xs font-semibold text-violet-200"
|
|
177
|
-
title={conversationId}
|
|
178
|
-
>
|
|
179
|
-
{isFallbackConversation ? "sessionless" : compactSessionLabel}
|
|
180
|
-
</span>
|
|
181
|
-
{showProcessMetadata && clientPid !== null && clientPid !== undefined && (
|
|
226
|
+
|
|
227
|
+
{showClientIdentity && clientDisplayLabel !== null && clientDisplayLabel !== "" && (
|
|
182
228
|
<span
|
|
183
|
-
className="border border-border bg-muted/
|
|
184
|
-
title={
|
|
229
|
+
className="border border-border bg-muted/30 inline-flex h-6 max-w-[180px] shrink-0 items-center gap-1 rounded-md px-1.5 text-xs text-muted-foreground"
|
|
230
|
+
title={clientTooltip ?? clientDisplayLabel}
|
|
185
231
|
>
|
|
186
|
-
|
|
232
|
+
<ClientLogo client={clientApp} className="size-4 shrink-0" />
|
|
233
|
+
<span className="truncate font-mono tabular-nums">{clientDisplayLabel}</span>
|
|
187
234
|
</span>
|
|
188
235
|
)}
|
|
189
|
-
</>
|
|
190
|
-
)}
|
|
191
|
-
|
|
192
|
-
{/* Client app / User-Agent */}
|
|
193
|
-
{showClientIdentity && clientDisplayLabel !== null && clientDisplayLabel !== "" && (
|
|
194
|
-
<span
|
|
195
|
-
className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
|
|
196
|
-
title={clientTooltip ?? clientDisplayLabel}
|
|
197
|
-
>
|
|
198
|
-
<ClientLogo client={clientApp} className="size-5" />
|
|
199
|
-
<span className="font-mono tabular-nums truncate max-w-[120px]">
|
|
200
|
-
{clientDisplayLabel}
|
|
201
|
-
</span>
|
|
202
|
-
</span>
|
|
203
|
-
)}
|
|
204
|
-
|
|
205
|
-
{/* Time range */}
|
|
206
|
-
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
207
|
-
<Clock className="size-3" />
|
|
208
|
-
<span className="font-mono tabular-nums">
|
|
209
|
-
{formatTimestampRange(startTime, endTime, timeDisplayFormat)}
|
|
210
|
-
</span>
|
|
211
|
-
</span>
|
|
212
|
-
|
|
213
|
-
{/* Total calls */}
|
|
214
|
-
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
215
|
-
<MessageSquare className="size-3" />
|
|
216
|
-
<span className="font-mono tabular-nums">
|
|
217
|
-
{totalCalls} call{totalCalls !== 1 ? "s" : ""}
|
|
218
236
|
</span>
|
|
219
|
-
</span>
|
|
220
237
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
<span className="mt-1.5 flex min-w-0 flex-wrap items-center gap-1.5">
|
|
239
|
+
<HeaderMetric icon={<Clock className="size-3 shrink-0" />} title={timeLabel}>
|
|
240
|
+
{timeLabel}
|
|
241
|
+
</HeaderMetric>
|
|
242
|
+
<HeaderMetric icon={<MessageSquare className="size-3 shrink-0" />}>
|
|
243
|
+
{callsLabel}
|
|
244
|
+
</HeaderMetric>
|
|
245
|
+
<HeaderMetric icon={<Zap className="size-3 shrink-0" />} className="max-w-full">
|
|
246
|
+
<span className="text-blue-400">{formatTokens(totalInputTokens)}</span>
|
|
247
|
+
{" / "}
|
|
248
|
+
<span className="text-amber-400">{formatTokens(totalOutputTokens)}</span>
|
|
249
|
+
</HeaderMetric>
|
|
228
250
|
</span>
|
|
229
251
|
</span>
|
|
230
252
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
{/* Open this session in a new tab - deep link to /session/$id */}
|
|
235
|
-
<button
|
|
236
|
-
type="button"
|
|
237
|
-
onClick={handleOpenInNewTab}
|
|
238
|
-
onKeyDown={(e) => {
|
|
239
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
240
|
-
handleOpenInNewTab(e);
|
|
241
|
-
}
|
|
242
|
-
}}
|
|
243
|
-
aria-label={`Open session ${conversationId} in a new tab`}
|
|
244
|
-
title="Open this session in a new tab"
|
|
245
|
-
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground"
|
|
246
|
-
>
|
|
247
|
-
<ExternalLink className="size-3.5" />
|
|
248
|
-
</button>
|
|
249
|
-
|
|
250
|
-
{/* Per-group Clear - does not toggle the group's expand state */}
|
|
251
|
-
{onClear !== undefined && (
|
|
253
|
+
<span className="flex shrink-0 items-start gap-1.5">
|
|
254
|
+
{/* Open this session in a new tab - deep link to /session/$id */}
|
|
252
255
|
<button
|
|
253
256
|
type="button"
|
|
254
|
-
onClick={
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
257
|
+
onClick={handleOpenInNewTab}
|
|
258
|
+
onKeyDown={(e) => {
|
|
259
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
260
|
+
handleOpenInNewTab(e);
|
|
261
|
+
}
|
|
262
|
+
}}
|
|
263
|
+
aria-label={`Open session ${conversationId} in a new tab`}
|
|
264
|
+
title="Open this session in a new tab"
|
|
265
|
+
className={HEADER_ACTION_BUTTON_CLASS}
|
|
258
266
|
>
|
|
259
|
-
<
|
|
267
|
+
<ExternalLink className="size-3.5" />
|
|
260
268
|
</button>
|
|
261
|
-
)}
|
|
262
269
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
{/* Per-group Clear - does not toggle the group's expand state */}
|
|
271
|
+
{onClear !== undefined && (
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
onClick={handleClearClick}
|
|
275
|
+
aria-label={`Clear group (${totalCalls} request${totalCalls !== 1 ? "s" : ""})`}
|
|
276
|
+
title="Clear this group"
|
|
277
|
+
className={HEADER_ACTION_BUTTON_CLASS}
|
|
278
|
+
>
|
|
279
|
+
<Trash2 className="size-3.5" />
|
|
280
|
+
</button>
|
|
274
281
|
)}
|
|
282
|
+
|
|
283
|
+
{/* Expand chevron - shows spinner when collapsed and group has pending logs */}
|
|
284
|
+
<span
|
|
285
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-md text-slate-200"
|
|
286
|
+
aria-hidden="true"
|
|
287
|
+
>
|
|
288
|
+
{expanded ? (
|
|
289
|
+
<ChevronDown className="size-4 shrink-0" />
|
|
290
|
+
) : isLoading ? (
|
|
291
|
+
<Loader2 className="size-4 shrink-0 animate-spin" />
|
|
292
|
+
) : (
|
|
293
|
+
<ChevronRight className="size-4 shrink-0" />
|
|
294
|
+
)}
|
|
295
|
+
</span>
|
|
275
296
|
</span>
|
|
276
297
|
|
|
277
298
|
<ConfirmDialog
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AlertTriangle, GitCompareArrows } from "lucide-react";
|
|
1
|
+
import { AlertTriangle, GitCompareArrows, Loader2, RefreshCw } 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
4
|
import {
|
|
@@ -27,6 +27,12 @@ import {
|
|
|
27
27
|
} from "./lazy";
|
|
28
28
|
import type { AnatomySegment } from "./anatomy/types";
|
|
29
29
|
import { useAnatomyJump } from "./anatomy/useAnatomyJump";
|
|
30
|
+
import {
|
|
31
|
+
describeFullBodyHydrationError,
|
|
32
|
+
formatBodyHydrationFootprint,
|
|
33
|
+
formatBytes,
|
|
34
|
+
type FullBodyHydrationError,
|
|
35
|
+
} from "./bodyHydration";
|
|
30
36
|
import { computeHeadersDiff, computeRequestDiff, DiffView } from "./diff";
|
|
31
37
|
import { LogEntryHeader } from "./LogEntryHeader";
|
|
32
38
|
import { RequestTools } from "./RequestToolsPanel";
|
|
@@ -57,7 +63,7 @@ function resolveFocusedTab(tab: LogFocusTab, anatomySegments: AnatomySegment[] |
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
type
|
|
66
|
+
type BodyHydrationStatus = "idle" | "loading" | "failed";
|
|
61
67
|
type BodyChunkState = "idle" | "loading" | "success" | "failed";
|
|
62
68
|
|
|
63
69
|
type BodyPreviewState = {
|
|
@@ -108,13 +114,6 @@ function shouldHydrateLogBody(log: CapturedLog): boolean {
|
|
|
108
114
|
return isCompactBody(log) && !shouldUseChunkedBodyLoading(log);
|
|
109
115
|
}
|
|
110
116
|
|
|
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
117
|
function bodyPreviewErrorMessage(error: unknown): string {
|
|
119
118
|
if (error instanceof ApiTimeoutError) {
|
|
120
119
|
return "Body preview timed out. The log is still available on disk; try loading again.";
|
|
@@ -123,6 +122,57 @@ function bodyPreviewErrorMessage(error: unknown): string {
|
|
|
123
122
|
return "Body preview could not be loaded.";
|
|
124
123
|
}
|
|
125
124
|
|
|
125
|
+
function FullBodyHydrationNotice({
|
|
126
|
+
status,
|
|
127
|
+
error,
|
|
128
|
+
footprint,
|
|
129
|
+
onRetry,
|
|
130
|
+
}: {
|
|
131
|
+
status: BodyHydrationStatus;
|
|
132
|
+
error: FullBodyHydrationError | null;
|
|
133
|
+
footprint: string | null;
|
|
134
|
+
onRetry: () => void;
|
|
135
|
+
}): JSX.Element | null {
|
|
136
|
+
switch (status) {
|
|
137
|
+
case "idle":
|
|
138
|
+
return null;
|
|
139
|
+
case "loading":
|
|
140
|
+
return (
|
|
141
|
+
<NoticeBlock tone="loading" title="Loading full bodies">
|
|
142
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
143
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
144
|
+
<Loader2 className="size-3.5 shrink-0 animate-spin text-cyan-200" />
|
|
145
|
+
<span>Loading full request and response bodies from the log store.</span>
|
|
146
|
+
</span>
|
|
147
|
+
{footprint !== null && (
|
|
148
|
+
<span className="rounded border border-cyan-300/15 bg-cyan-300/[0.06] px-2 py-0.5 font-mono text-[11px] text-cyan-100">
|
|
149
|
+
{footprint}
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</NoticeBlock>
|
|
154
|
+
);
|
|
155
|
+
case "failed":
|
|
156
|
+
return (
|
|
157
|
+
<NoticeBlock tone="warning" title={error?.title ?? "Full bodies could not be loaded"}>
|
|
158
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
159
|
+
<span>{error?.detail ?? "Compact metadata is still available."}</span>
|
|
160
|
+
<Button
|
|
161
|
+
type="button"
|
|
162
|
+
variant="outline"
|
|
163
|
+
size="sm"
|
|
164
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-7 gap-1.5 text-xs text-amber-100"
|
|
165
|
+
onClick={onRetry}
|
|
166
|
+
>
|
|
167
|
+
<RefreshCw className="size-3" />
|
|
168
|
+
Retry
|
|
169
|
+
</Button>
|
|
170
|
+
</div>
|
|
171
|
+
</NoticeBlock>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
126
176
|
function bodyPreviewTextForView(enabled: boolean, state: BodyPreviewState): string | null {
|
|
127
177
|
if (!enabled) return null;
|
|
128
178
|
if (state.text !== "") return state.text;
|
|
@@ -381,7 +431,9 @@ export const LogEntry = memo(function ({
|
|
|
381
431
|
const [activeTab, setActiveTab] = useState("request");
|
|
382
432
|
const [expandToPath, setExpandToPath] = useState<string | null>(null);
|
|
383
433
|
const [hydratedLog, setHydratedLog] = useState<CapturedLog | null>(null);
|
|
384
|
-
const [
|
|
434
|
+
const [bodyHydrationStatus, setBodyHydrationStatus] = useState<BodyHydrationStatus>("idle");
|
|
435
|
+
const [bodyHydrationError, setBodyHydrationError] = useState<FullBodyHydrationError | null>(null);
|
|
436
|
+
const [bodyHydrationRetryNonce, setBodyHydrationRetryNonce] = useState(0);
|
|
385
437
|
const [requestPreview, setRequestPreview] = useState<BodyPreviewState>(() =>
|
|
386
438
|
createEmptyBodyPreviewState(log.rawRequestBodyBytes ?? null),
|
|
387
439
|
);
|
|
@@ -393,6 +445,10 @@ export const LogEntry = memo(function ({
|
|
|
393
445
|
const bodyHydrationInFlightRef = useRef(false);
|
|
394
446
|
const displayLog = hydratedLog ?? log;
|
|
395
447
|
const useChunkedBody = hydratedLog === null && shouldUseChunkedBodyLoading(log);
|
|
448
|
+
const fullBodyHydrationFootprint = useMemo(
|
|
449
|
+
() => formatBodyHydrationFootprint(log.rawRequestBodyBytes, log.responseTextBytes),
|
|
450
|
+
[log.rawRequestBodyBytes, log.responseTextBytes],
|
|
451
|
+
);
|
|
396
452
|
const requestPreviewBody = bodyPreviewTextForView(useChunkedBody, requestPreview);
|
|
397
453
|
const responsePreviewBody = bodyPreviewTextForView(useChunkedBody, responsePreview);
|
|
398
454
|
const rawRequestBodyForView = displayLog.rawRequestBody ?? requestPreviewBody;
|
|
@@ -631,6 +687,14 @@ export const LogEntry = memo(function ({
|
|
|
631
687
|
[log.id],
|
|
632
688
|
);
|
|
633
689
|
|
|
690
|
+
const retryFullBodyHydration = useCallback(() => {
|
|
691
|
+
if (bodyHydrationInFlightRef.current) return;
|
|
692
|
+
setHydratedLog(null);
|
|
693
|
+
setBodyHydrationStatus("idle");
|
|
694
|
+
setBodyHydrationError(null);
|
|
695
|
+
setBodyHydrationRetryNonce((current) => current + 1);
|
|
696
|
+
}, []);
|
|
697
|
+
|
|
634
698
|
const handleToggleExpanded = useCallback(() => {
|
|
635
699
|
const nextExpanded = !expanded;
|
|
636
700
|
if (nextExpanded) {
|
|
@@ -643,7 +707,9 @@ export const LogEntry = memo(function ({
|
|
|
643
707
|
logIdRef.current = log.id;
|
|
644
708
|
bodyHydrationInFlightRef.current = false;
|
|
645
709
|
setHydratedLog(null);
|
|
646
|
-
|
|
710
|
+
setBodyHydrationStatus("idle");
|
|
711
|
+
setBodyHydrationError(null);
|
|
712
|
+
setBodyHydrationRetryNonce(0);
|
|
647
713
|
setRequestPreview(createEmptyBodyPreviewState(log.rawRequestBodyBytes ?? null));
|
|
648
714
|
setResponsePreview(createEmptyBodyPreviewState(log.responseTextBytes ?? null));
|
|
649
715
|
}, [log.id, log.rawRequestBodyBytes, log.responseTextBytes]);
|
|
@@ -655,7 +721,8 @@ export const LogEntry = memo(function ({
|
|
|
655
721
|
|
|
656
722
|
let cancelled = false;
|
|
657
723
|
bodyHydrationInFlightRef.current = true;
|
|
658
|
-
|
|
724
|
+
setBodyHydrationStatus("loading");
|
|
725
|
+
setBodyHydrationError(null);
|
|
659
726
|
void fetchJsonWithTimeout(
|
|
660
727
|
`/api/logs/${String(log.id)}`,
|
|
661
728
|
CapturedLogSchema,
|
|
@@ -665,12 +732,14 @@ export const LogEntry = memo(function ({
|
|
|
665
732
|
if (cancelled) return;
|
|
666
733
|
bodyHydrationInFlightRef.current = false;
|
|
667
734
|
setHydratedLog(fullLog);
|
|
668
|
-
|
|
735
|
+
setBodyHydrationStatus("idle");
|
|
736
|
+
setBodyHydrationError(null);
|
|
669
737
|
})
|
|
670
|
-
.catch(() => {
|
|
738
|
+
.catch((error: unknown) => {
|
|
671
739
|
if (cancelled) return;
|
|
672
740
|
bodyHydrationInFlightRef.current = false;
|
|
673
|
-
|
|
741
|
+
setBodyHydrationStatus("failed");
|
|
742
|
+
setBodyHydrationError(describeFullBodyHydrationError(error));
|
|
674
743
|
});
|
|
675
744
|
|
|
676
745
|
return () => {
|
|
@@ -684,6 +753,7 @@ export const LogEntry = memo(function ({
|
|
|
684
753
|
log.id,
|
|
685
754
|
log.rawRequestBodyBytes,
|
|
686
755
|
log.responseTextBytes,
|
|
756
|
+
bodyHydrationRetryNonce,
|
|
687
757
|
]);
|
|
688
758
|
|
|
689
759
|
useEffect(() => {
|
|
@@ -812,6 +882,12 @@ export const LogEntry = memo(function ({
|
|
|
812
882
|
<TabsContent value="raw-request">
|
|
813
883
|
{activeTab === "raw-request" && (
|
|
814
884
|
<ExpandedPanel>
|
|
885
|
+
<FullBodyHydrationNotice
|
|
886
|
+
status={bodyHydrationStatus}
|
|
887
|
+
error={bodyHydrationError}
|
|
888
|
+
footprint={fullBodyHydrationFootprint}
|
|
889
|
+
onRetry={retryFullBodyHydration}
|
|
890
|
+
/>
|
|
815
891
|
{useChunkedBody && (
|
|
816
892
|
<BodyPreviewNotice
|
|
817
893
|
label="Request"
|
|
@@ -843,17 +919,12 @@ export const LogEntry = memo(function ({
|
|
|
843
919
|
<TabsContent value="request">
|
|
844
920
|
{activeTab === "request" && (
|
|
845
921
|
<ExpandedPanel>
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
<NoticeBlock tone="warning">
|
|
853
|
-
Full request and response bodies could not be loaded. Compact metadata is
|
|
854
|
-
still available.
|
|
855
|
-
</NoticeBlock>
|
|
856
|
-
)}
|
|
922
|
+
<FullBodyHydrationNotice
|
|
923
|
+
status={bodyHydrationStatus}
|
|
924
|
+
error={bodyHydrationError}
|
|
925
|
+
footprint={fullBodyHydrationFootprint}
|
|
926
|
+
onRetry={retryFullBodyHydration}
|
|
927
|
+
/>
|
|
857
928
|
{useChunkedBody && (
|
|
858
929
|
<BodyPreviewNotice
|
|
859
930
|
label="Request"
|
|
@@ -982,6 +1053,12 @@ export const LogEntry = memo(function ({
|
|
|
982
1053
|
<TabsContent value="raw">
|
|
983
1054
|
{activeTab === "raw" && (
|
|
984
1055
|
<ExpandedPanel className="space-y-3">
|
|
1056
|
+
<FullBodyHydrationNotice
|
|
1057
|
+
status={bodyHydrationStatus}
|
|
1058
|
+
error={bodyHydrationError}
|
|
1059
|
+
footprint={fullBodyHydrationFootprint}
|
|
1060
|
+
onRetry={retryFullBodyHydration}
|
|
1061
|
+
/>
|
|
985
1062
|
{useChunkedBody && (
|
|
986
1063
|
<BodyPreviewNotice
|
|
987
1064
|
label="Response"
|
|
@@ -1026,6 +1103,12 @@ export const LogEntry = memo(function ({
|
|
|
1026
1103
|
<TabsContent value="parsed">
|
|
1027
1104
|
{activeTab === "parsed" && (
|
|
1028
1105
|
<ExpandedPanel>
|
|
1106
|
+
<FullBodyHydrationNotice
|
|
1107
|
+
status={bodyHydrationStatus}
|
|
1108
|
+
error={bodyHydrationError}
|
|
1109
|
+
footprint={fullBodyHydrationFootprint}
|
|
1110
|
+
onRetry={retryFullBodyHydration}
|
|
1111
|
+
/>
|
|
1029
1112
|
{useChunkedBody && (
|
|
1030
1113
|
<BodyPreviewNotice
|
|
1031
1114
|
label="Response"
|