@tonyclaw/agent-inspector 2.0.4 → 2.0.5
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-BCH_fsLm.js → CompareDrawer-3nRwtk8J.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CbW5VRER.js +101 -0
- package/.output/public/assets/ReplayDialog-Cl62N9PI.js +1 -0
- package/.output/public/assets/{RequestAnatomy-DZ8grAih.js → RequestAnatomy-DgQWGvjs.js} +1 -1
- package/.output/public/assets/ResponseView-Cvc-ct4E.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-BCQaCAIe.js +1 -0
- package/.output/public/assets/_sessionId-CcD_aLGq.js +1 -0
- package/.output/public/assets/index-B_dffD3u.js +1 -0
- package/.output/public/assets/index-CX796gvi.css +1 -0
- package/.output/public/assets/{json-viewer-BrzjD7qI.js → json-viewer-IXejqXB0.js} +1 -1
- package/.output/public/assets/{main-mgxeUdZQ.js → main-2NlGzgOe.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +181 -114
- package/.output/server/{_sessionId-C4xsxIWm.mjs → _sessionId-DWCTasJU.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-DuWEpqQ7.mjs → CompareDrawer-DhrN1uC2.mjs} +6 -6
- package/.output/server/_ssr/{ProxyViewerContainer-Cckz5qKu.mjs → ProxyViewerContainer-DRl51s_n.mjs} +763 -119
- package/.output/server/_ssr/{ReplayDialog-BDRcr8E5.mjs → ReplayDialog-BQT_ygxC.mjs} +240 -14
- package/.output/server/_ssr/{RequestAnatomy-BoO2_Ij0.mjs → RequestAnatomy-DS2tZOgq.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-DZiPBxvO.mjs → ResponseView-e0kL2C3x.mjs} +8 -8
- package/.output/server/_ssr/{StreamingChunkSequence-D-be7KEL.mjs → StreamingChunkSequence-BJG-m7xs.mjs} +3 -3
- package/.output/server/_ssr/{index-5RImHKfu.mjs → index-Dea3OeRw.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-aJhb93ZK.mjs → json-viewer-DDU55MLK.mjs} +3 -3
- package/.output/server/_ssr/{router-Dgkv5nKP.mjs → router-Dl7oh0zx.mjs} +145 -71
- package/.output/server/_tanstack-start-manifest_v-m-FJNBVf.mjs +4 -0
- package/.output/server/index.mjs +69 -69
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +11 -19
- package/src/components/ProxyViewer.tsx +1 -1
- package/src/components/providers/ProviderCard.tsx +6 -20
- package/src/components/providers/SettingsDialog.tsx +95 -2
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +639 -38
- package/src/components/proxy-viewer/CompareDrawer.tsx +4 -2
- package/src/components/proxy-viewer/LogEntryHeader.tsx +12 -22
- package/src/components/proxy-viewer/ReplayDialog.tsx +190 -8
- package/src/components/proxy-viewer/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +37 -16
- package/src/components/proxy-viewer/TurnGroup.tsx +14 -2
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/replayComparison.ts +131 -0
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +64 -22
- package/src/components/proxy-viewer/viewerState.ts +14 -2
- package/src/knowledge/candidateStore.ts +32 -1
- package/src/routes/api/knowledge.candidates.$candidateId.ts +50 -0
- package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +12 -2
- package/.output/public/assets/ProxyViewerContainer-D85_UANk.js +0 -101
- package/.output/public/assets/ReplayDialog-DTeaHHit.js +0 -1
- package/.output/public/assets/ResponseView-Cldm6RCi.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-3x4p-yT7.js +0 -1
- package/.output/public/assets/_sessionId-YqWFBu6d.js +0 -1
- package/.output/public/assets/index-BIw2H6jO.js +0 -1
- package/.output/public/assets/index-CobXD0yH.css +0 -1
- package/.output/server/_tanstack-start-manifest_v-B8rrWXjr.mjs +0 -4
|
@@ -550,11 +550,13 @@ function SideSummary({ log, side }: { log: CapturedLog; side: "left" | "right" }
|
|
|
550
550
|
<div className="flex items-center gap-3 text-muted-foreground font-mono">
|
|
551
551
|
{log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
|
|
552
552
|
<span className="text-emerald-400">
|
|
553
|
-
Cache +{formatTokens(log.cacheCreationInputTokens)}
|
|
553
|
+
KV Cache +{formatTokens(log.cacheCreationInputTokens)}
|
|
554
554
|
</span>
|
|
555
555
|
)}
|
|
556
556
|
{log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
|
|
557
|
-
<span className="text-purple-400">
|
|
557
|
+
<span className="text-purple-400">
|
|
558
|
+
KV Cache ~{formatTokens(log.cacheReadInputTokens)}
|
|
559
|
+
</span>
|
|
558
560
|
)}
|
|
559
561
|
<span className="truncate" title={log.timestamp}>
|
|
560
562
|
{log.timestamp}
|
|
@@ -264,30 +264,20 @@ export const LogEntryHeader = memo(function ({
|
|
|
264
264
|
)}
|
|
265
265
|
{/* Cache tokens */}
|
|
266
266
|
{log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
|
|
267
|
-
<
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
</span>
|
|
274
|
-
</span>
|
|
275
|
-
</TooltipTrigger>
|
|
276
|
-
<TooltipContent>Tokens cached for reuse, reducing future API cost</TooltipContent>
|
|
277
|
-
</Tooltip>
|
|
267
|
+
<span className="flex items-center gap-1 text-xs shrink-0">
|
|
268
|
+
<CacheTrendIndicator trend={cacheTrend?.creation ?? null} />
|
|
269
|
+
<span className="font-mono tabular-nums text-emerald-400">
|
|
270
|
+
KV Cache +{formatTokens(log.cacheCreationInputTokens)}
|
|
271
|
+
</span>
|
|
272
|
+
</span>
|
|
278
273
|
)}
|
|
279
274
|
{log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
|
|
280
|
-
<
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
</span>
|
|
287
|
-
</span>
|
|
288
|
-
</TooltipTrigger>
|
|
289
|
-
<TooltipContent>Tokens served from cache, reducing API cost</TooltipContent>
|
|
290
|
-
</Tooltip>
|
|
275
|
+
<span className="flex items-center gap-1 text-xs shrink-0">
|
|
276
|
+
<CacheTrendIndicator trend={cacheTrend?.read ?? null} />
|
|
277
|
+
<span className="font-mono tabular-nums text-purple-400">
|
|
278
|
+
KV Cache ~{formatTokens(log.cacheReadInputTokens)}
|
|
279
|
+
</span>
|
|
280
|
+
</span>
|
|
291
281
|
)}
|
|
292
282
|
|
|
293
283
|
{/* Message count */}
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
import { RotateCcw } from "lucide-react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { useProviders } from "../../lib/useProviders";
|
|
5
6
|
import { Button } from "../ui/button";
|
|
6
7
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
7
8
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
|
|
8
9
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
|
9
10
|
import { ResponseView } from "./ResponseView";
|
|
10
11
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
12
|
+
import {
|
|
13
|
+
buildReplayComparisons,
|
|
14
|
+
buildReplayMetrics,
|
|
15
|
+
readRequestModel,
|
|
16
|
+
replaceRequestModel,
|
|
17
|
+
type ReplayMetricComparison,
|
|
18
|
+
type ReplayMetricValue,
|
|
19
|
+
} from "./replayComparison";
|
|
11
20
|
|
|
12
21
|
const ReplayResultSchema = z.object({
|
|
13
22
|
success: z.boolean(),
|
|
14
23
|
error: z.string().optional(),
|
|
15
|
-
responseStatus: z.number().optional(),
|
|
16
|
-
responseText: z.string().optional(),
|
|
17
|
-
inputTokens: z.number().optional(),
|
|
18
|
-
outputTokens: z.number().optional(),
|
|
19
|
-
elapsedMs: z.number().optional(),
|
|
24
|
+
responseStatus: z.number().nullable().optional(),
|
|
25
|
+
responseText: z.string().nullable().optional(),
|
|
26
|
+
inputTokens: z.number().nullable().optional(),
|
|
27
|
+
outputTokens: z.number().nullable().optional(),
|
|
28
|
+
elapsedMs: z.number().nullable().optional(),
|
|
20
29
|
streaming: z.boolean().optional(),
|
|
21
30
|
});
|
|
22
31
|
|
|
@@ -28,7 +37,91 @@ type ReplayDialogProps = {
|
|
|
28
37
|
onOpenChange: (open: boolean) => void;
|
|
29
38
|
};
|
|
30
39
|
|
|
40
|
+
type ReplayModelOption = {
|
|
41
|
+
key: string;
|
|
42
|
+
providerName: string;
|
|
43
|
+
model: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function formatElapsed(ms: number): string {
|
|
47
|
+
if (ms < 1000) return `${String(ms)}ms`;
|
|
48
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatMetricValue(
|
|
52
|
+
value: ReplayMetricValue,
|
|
53
|
+
metricId: ReplayMetricComparison["id"],
|
|
54
|
+
): string {
|
|
55
|
+
if (value === null) return "-";
|
|
56
|
+
if (typeof value === "boolean") return value ? "stream" : "non-stream";
|
|
57
|
+
switch (metricId) {
|
|
58
|
+
case "elapsed":
|
|
59
|
+
return formatElapsed(value);
|
|
60
|
+
case "input":
|
|
61
|
+
case "output":
|
|
62
|
+
case "bytes":
|
|
63
|
+
return value.toLocaleString();
|
|
64
|
+
case "status":
|
|
65
|
+
return String(value);
|
|
66
|
+
case "streaming":
|
|
67
|
+
return value ? "stream" : "non-stream";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatDelta(delta: number | null, metricId: ReplayMetricComparison["id"]): string {
|
|
72
|
+
if (delta === null) return "-";
|
|
73
|
+
const sign = delta > 0 ? "+" : "";
|
|
74
|
+
switch (metricId) {
|
|
75
|
+
case "elapsed":
|
|
76
|
+
return `${sign}${formatElapsed(delta)}`;
|
|
77
|
+
case "input":
|
|
78
|
+
case "output":
|
|
79
|
+
case "bytes":
|
|
80
|
+
case "status":
|
|
81
|
+
return `${sign}${delta.toLocaleString()}`;
|
|
82
|
+
case "streaming":
|
|
83
|
+
return "-";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function deltaToneClass(delta: number | null): string {
|
|
88
|
+
if (delta === null || delta === 0) return "text-muted-foreground";
|
|
89
|
+
if (delta > 0) return "text-amber-400";
|
|
90
|
+
return "text-emerald-400";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ReplayComparisonTable({
|
|
94
|
+
comparisons,
|
|
95
|
+
}: {
|
|
96
|
+
comparisons: ReplayMetricComparison[];
|
|
97
|
+
}): JSX.Element {
|
|
98
|
+
return (
|
|
99
|
+
<div className="overflow-hidden rounded-md border border-border">
|
|
100
|
+
<div className="grid grid-cols-[1fr_1fr_1fr_1fr] bg-muted/40 px-3 py-2 text-[11px] font-medium text-muted-foreground">
|
|
101
|
+
<span>Metric</span>
|
|
102
|
+
<span>Original</span>
|
|
103
|
+
<span>Replay</span>
|
|
104
|
+
<span>Delta</span>
|
|
105
|
+
</div>
|
|
106
|
+
{comparisons.map((comparison) => (
|
|
107
|
+
<div
|
|
108
|
+
key={comparison.id}
|
|
109
|
+
className="grid grid-cols-[1fr_1fr_1fr_1fr] border-t border-border px-3 py-2 text-xs"
|
|
110
|
+
>
|
|
111
|
+
<span className="text-muted-foreground">{comparison.label}</span>
|
|
112
|
+
<span className="font-mono">{formatMetricValue(comparison.original, comparison.id)}</span>
|
|
113
|
+
<span className="font-mono">{formatMetricValue(comparison.replay, comparison.id)}</span>
|
|
114
|
+
<span className={`font-mono ${deltaToneClass(comparison.delta)}`}>
|
|
115
|
+
{formatDelta(comparison.delta, comparison.id)}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
31
123
|
export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JSX.Element {
|
|
124
|
+
const { providers } = useProviders();
|
|
32
125
|
const [modifiedBody, setModifiedBody] = useState<string>(() => {
|
|
33
126
|
return log.rawRequestBody ?? "{}";
|
|
34
127
|
});
|
|
@@ -73,6 +166,55 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
73
166
|
onOpenChange(false);
|
|
74
167
|
}
|
|
75
168
|
|
|
169
|
+
const replayModelOptions = useMemo<ReplayModelOption[]>(() => {
|
|
170
|
+
const options: ReplayModelOption[] = [];
|
|
171
|
+
for (const provider of providers) {
|
|
172
|
+
for (const model of provider.models) {
|
|
173
|
+
if (model.trim().length === 0) continue;
|
|
174
|
+
options.push({
|
|
175
|
+
key: `${provider.id}:${model}`,
|
|
176
|
+
providerName: provider.name,
|
|
177
|
+
model,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return options;
|
|
182
|
+
}, [providers]);
|
|
183
|
+
const currentReplayModel = readRequestModel(modifiedBody) ?? log.model ?? "";
|
|
184
|
+
|
|
185
|
+
function handleReplayModelChange(model: string): void {
|
|
186
|
+
const result = replaceRequestModel(modifiedBody, model);
|
|
187
|
+
if (result.error !== null) {
|
|
188
|
+
setError(result.error);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
setModifiedBody(result.body);
|
|
192
|
+
setReplayResult(null);
|
|
193
|
+
setError(null);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const originalMetrics = buildReplayMetrics({
|
|
197
|
+
status: log.responseStatus,
|
|
198
|
+
elapsedMs: log.elapsedMs,
|
|
199
|
+
inputTokens: log.inputTokens,
|
|
200
|
+
outputTokens: log.outputTokens,
|
|
201
|
+
responseText: log.responseText,
|
|
202
|
+
streaming: log.streaming,
|
|
203
|
+
});
|
|
204
|
+
const replayMetrics =
|
|
205
|
+
replayResult === null
|
|
206
|
+
? null
|
|
207
|
+
: buildReplayMetrics({
|
|
208
|
+
status: replayResult.responseStatus,
|
|
209
|
+
elapsedMs: replayResult.elapsedMs,
|
|
210
|
+
inputTokens: replayResult.inputTokens,
|
|
211
|
+
outputTokens: replayResult.outputTokens,
|
|
212
|
+
responseText: replayResult.responseText,
|
|
213
|
+
streaming: replayResult.streaming,
|
|
214
|
+
});
|
|
215
|
+
const replayComparisons =
|
|
216
|
+
replayMetrics === null ? [] : buildReplayComparisons(originalMetrics, replayMetrics);
|
|
217
|
+
|
|
76
218
|
return (
|
|
77
219
|
<Dialog open={open} onOpenChange={handleClose}>
|
|
78
220
|
<DialogContent className="max-w-4xl max-h-[85vh] overflow-auto">
|
|
@@ -88,9 +230,34 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
88
230
|
<TabsTrigger value="modified">Modified Request</TabsTrigger>
|
|
89
231
|
<TabsTrigger value="original">Original Response</TabsTrigger>
|
|
90
232
|
{replayResult && <TabsTrigger value="replay">Replay Response</TabsTrigger>}
|
|
233
|
+
{replayResult && <TabsTrigger value="compare">Compare</TabsTrigger>}
|
|
91
234
|
</TabsList>
|
|
92
235
|
|
|
93
236
|
<TabsContent value="modified" className="space-y-4">
|
|
237
|
+
{replayModelOptions.length > 0 && (
|
|
238
|
+
<div className="grid gap-1.5">
|
|
239
|
+
<label htmlFor={`replay-model-${String(log.id)}`} className="text-sm font-medium">
|
|
240
|
+
Replay target
|
|
241
|
+
</label>
|
|
242
|
+
<select
|
|
243
|
+
id={`replay-model-${String(log.id)}`}
|
|
244
|
+
value={currentReplayModel}
|
|
245
|
+
onChange={(event) => handleReplayModelChange(event.currentTarget.value)}
|
|
246
|
+
className="h-8 rounded-md border border-input bg-background px-2 text-sm"
|
|
247
|
+
>
|
|
248
|
+
{currentReplayModel === "" && <option value="">Select model</option>}
|
|
249
|
+
{currentReplayModel !== "" &&
|
|
250
|
+
!replayModelOptions.some((option) => option.model === currentReplayModel) && (
|
|
251
|
+
<option value={currentReplayModel}>{currentReplayModel}</option>
|
|
252
|
+
)}
|
|
253
|
+
{replayModelOptions.map((option) => (
|
|
254
|
+
<option key={option.key} value={option.model}>
|
|
255
|
+
{option.providerName} / {option.model}
|
|
256
|
+
</option>
|
|
257
|
+
))}
|
|
258
|
+
</select>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
94
261
|
<div>
|
|
95
262
|
<label className="text-sm font-medium mb-2 block">Request Body (JSON)</label>
|
|
96
263
|
<TooltipProvider>
|
|
@@ -127,6 +294,10 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
127
294
|
</Button>
|
|
128
295
|
</div>
|
|
129
296
|
|
|
297
|
+
{replayResult !== null && (
|
|
298
|
+
<ReplayComparisonTable comparisons={replayComparisons.slice(0, 4)} />
|
|
299
|
+
)}
|
|
300
|
+
|
|
130
301
|
{replayResult && replayResult.success && (
|
|
131
302
|
<Tabs defaultValue="parsed">
|
|
132
303
|
<TabsList>
|
|
@@ -184,7 +355,7 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
184
355
|
|
|
185
356
|
{replayResult && replayResult.success && (
|
|
186
357
|
<TabsContent value="replay">
|
|
187
|
-
{replayResult.responseText !== null ? (
|
|
358
|
+
{(replayResult.responseText ?? null) !== null ? (
|
|
188
359
|
<Tabs defaultValue="parsed">
|
|
189
360
|
<TabsList>
|
|
190
361
|
<TabsTrigger value="parsed">Response</TabsTrigger>
|
|
@@ -202,7 +373,7 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
202
373
|
</TabsContent>
|
|
203
374
|
<TabsContent value="raw">
|
|
204
375
|
<pre className="font-mono text-xs whitespace-pre-wrap bg-muted p-3 rounded-md max-h-96 overflow-auto">
|
|
205
|
-
{replayResult.responseText}
|
|
376
|
+
{replayResult.responseText ?? ""}
|
|
206
377
|
</pre>
|
|
207
378
|
</TabsContent>
|
|
208
379
|
</Tabs>
|
|
@@ -211,6 +382,17 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
211
382
|
)}
|
|
212
383
|
</TabsContent>
|
|
213
384
|
)}
|
|
385
|
+
|
|
386
|
+
{replayResult && (
|
|
387
|
+
<TabsContent value="compare" className="space-y-3">
|
|
388
|
+
<ReplayComparisonTable comparisons={replayComparisons} />
|
|
389
|
+
{replayResult.success ? null : (
|
|
390
|
+
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
391
|
+
{replayResult.error ?? "Replay failed"}
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
</TabsContent>
|
|
395
|
+
)}
|
|
214
396
|
</Tabs>
|
|
215
397
|
</DialogContent>
|
|
216
398
|
</Dialog>
|
|
@@ -148,14 +148,14 @@ export const ResponseView = memo(function ResponseView({
|
|
|
148
148
|
cacheCreationInputTokens !== undefined &&
|
|
149
149
|
cacheCreationInputTokens > 0 && (
|
|
150
150
|
<span className="font-mono tabular-nums text-emerald-400">
|
|
151
|
-
Cache +{formatTokens(cacheCreationInputTokens)}
|
|
151
|
+
KV Cache +{formatTokens(cacheCreationInputTokens)}
|
|
152
152
|
</span>
|
|
153
153
|
)}
|
|
154
154
|
{cacheReadInputTokens !== null &&
|
|
155
155
|
cacheReadInputTokens !== undefined &&
|
|
156
156
|
cacheReadInputTokens > 0 && (
|
|
157
157
|
<span className="font-mono tabular-nums text-purple-400">
|
|
158
|
-
Cache ~{formatTokens(cacheReadInputTokens)}
|
|
158
|
+
KV Cache ~{formatTokens(cacheReadInputTokens)}
|
|
159
159
|
</span>
|
|
160
160
|
)}
|
|
161
161
|
</span>
|
|
@@ -1,31 +1,52 @@
|
|
|
1
1
|
import { type JSX } from "react";
|
|
2
|
-
import { ChevronRight, Wrench } from "lucide-react";
|
|
2
|
+
import { Check, ChevronRight, Copy, Wrench } from "lucide-react";
|
|
3
3
|
import type { ToolTraceEvent } from "./viewerState";
|
|
4
|
+
import { useCopyFeedback } from "./useCopyFeedback";
|
|
4
5
|
|
|
5
6
|
type ToolTraceEventsProps = {
|
|
6
7
|
events: ToolTraceEvent[];
|
|
7
8
|
};
|
|
8
9
|
|
|
10
|
+
function ToolTraceEventRow({ event }: { event: ToolTraceEvent }): JSX.Element {
|
|
11
|
+
const argumentCopy = useCopyFeedback(event.argumentsText);
|
|
12
|
+
const canCopyArguments = event.argumentsText !== null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
key={event.id}
|
|
17
|
+
className="group/tool-trace flex min-w-0 items-center gap-2 rounded-md border border-border/70 bg-muted/20 px-2.5 py-1.5 text-xs"
|
|
18
|
+
>
|
|
19
|
+
<Wrench className="size-3.5 shrink-0 text-sky-400/70" />
|
|
20
|
+
<span className="font-mono font-semibold text-foreground/80">{event.name}</span>
|
|
21
|
+
{event.argumentsPreview !== null && (
|
|
22
|
+
<>
|
|
23
|
+
<ChevronRight className="size-3 shrink-0 text-muted-foreground/60" />
|
|
24
|
+
<span className="min-w-0 truncate font-mono text-muted-foreground">
|
|
25
|
+
{event.argumentsPreview}
|
|
26
|
+
</span>
|
|
27
|
+
</>
|
|
28
|
+
)}
|
|
29
|
+
{canCopyArguments && (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
className="ml-auto inline-flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity hover:bg-background/80 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/tool-trace:opacity-100 group-focus-within/tool-trace:opacity-100"
|
|
33
|
+
onClick={argumentCopy.copy}
|
|
34
|
+
aria-label={argumentCopy.copied ? "Copied tool arguments" : "Copy tool arguments"}
|
|
35
|
+
title={argumentCopy.copied ? "Copied tool arguments" : "Copy tool arguments"}
|
|
36
|
+
>
|
|
37
|
+
{argumentCopy.copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
|
38
|
+
</button>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
9
44
|
export function ToolTraceEvents({ events }: ToolTraceEventsProps): JSX.Element | null {
|
|
10
45
|
if (events.length === 0) return null;
|
|
11
46
|
return (
|
|
12
47
|
<div className="mx-3 mb-2 grid gap-1.5">
|
|
13
48
|
{events.map((event) => (
|
|
14
|
-
<
|
|
15
|
-
key={event.id}
|
|
16
|
-
className="flex min-w-0 items-center gap-2 rounded-md border border-border/70 bg-muted/20 px-2.5 py-1.5 text-xs"
|
|
17
|
-
>
|
|
18
|
-
<Wrench className="size-3.5 shrink-0 text-sky-400/70" />
|
|
19
|
-
<span className="font-mono font-semibold text-foreground/80">{event.name}</span>
|
|
20
|
-
{event.argumentsPreview !== null && (
|
|
21
|
-
<>
|
|
22
|
-
<ChevronRight className="size-3 shrink-0 text-muted-foreground/60" />
|
|
23
|
-
<span className="min-w-0 truncate font-mono text-muted-foreground">
|
|
24
|
-
{event.argumentsPreview}
|
|
25
|
-
</span>
|
|
26
|
-
</>
|
|
27
|
-
)}
|
|
28
|
-
</div>
|
|
49
|
+
<ToolTraceEventRow key={event.id} event={event} />
|
|
29
50
|
))}
|
|
30
51
|
</div>
|
|
31
52
|
);
|
|
@@ -137,15 +137,27 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
137
137
|
};
|
|
138
138
|
}, []);
|
|
139
139
|
|
|
140
|
+
const firstLogId = entries[0]?.log.id ?? turnIndex;
|
|
141
|
+
const turnLabel = `Turn ${String(turnIndex + 1)}`;
|
|
142
|
+
|
|
140
143
|
return (
|
|
141
144
|
<div
|
|
142
145
|
ref={containerRef}
|
|
143
|
-
|
|
146
|
+
tabIndex={collapsed ? undefined : 0}
|
|
147
|
+
role={collapsed ? undefined : "group"}
|
|
148
|
+
aria-label={collapsed ? undefined : turnLabel}
|
|
149
|
+
data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
|
|
150
|
+
className={cn(
|
|
151
|
+
"border rounded-lg",
|
|
152
|
+
isPending ? "border-amber-500/10" : "border-transparent",
|
|
153
|
+
!collapsed &&
|
|
154
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
|
|
155
|
+
)}
|
|
144
156
|
>
|
|
145
157
|
{collapsed ? (
|
|
146
158
|
/* ---- Collapsed: dual-crab (+ summary card for multi-log turns) ---- */
|
|
147
159
|
<div
|
|
148
|
-
data-nav-id={`turn-collapsed-${
|
|
160
|
+
data-nav-id={`turn-collapsed-${String(firstLogId)}`}
|
|
149
161
|
data-nav-action="expand"
|
|
150
162
|
role="button"
|
|
151
163
|
tabIndex={0}
|
|
@@ -38,14 +38,14 @@ export const StructuredResponseViewAnthropic = memo(function StructuredResponseV
|
|
|
38
38
|
response.usage.cache_creation_input_tokens !== null &&
|
|
39
39
|
response.usage.cache_creation_input_tokens > 0 && (
|
|
40
40
|
<span className="font-mono tabular-nums text-emerald-400">
|
|
41
|
-
Cache +{formatTokens(response.usage.cache_creation_input_tokens)}
|
|
41
|
+
KV Cache +{formatTokens(response.usage.cache_creation_input_tokens)}
|
|
42
42
|
</span>
|
|
43
43
|
)}
|
|
44
44
|
{response.usage.cache_read_input_tokens !== undefined &&
|
|
45
45
|
response.usage.cache_read_input_tokens !== null &&
|
|
46
46
|
response.usage.cache_read_input_tokens > 0 && (
|
|
47
47
|
<span className="font-mono tabular-nums text-purple-400">
|
|
48
|
-
Cache ~{formatTokens(response.usage.cache_read_input_tokens)}
|
|
48
|
+
KV Cache ~{formatTokens(response.usage.cache_read_input_tokens)}
|
|
49
49
|
</span>
|
|
50
50
|
)}
|
|
51
51
|
</span>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export type ReplayMetricValue = number | boolean | null;
|
|
2
|
+
|
|
3
|
+
export type ReplayMetrics = {
|
|
4
|
+
status: number | null;
|
|
5
|
+
elapsedMs: number | null;
|
|
6
|
+
inputTokens: number | null;
|
|
7
|
+
outputTokens: number | null;
|
|
8
|
+
responseBytes: number | null;
|
|
9
|
+
streaming: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ReplayMetricComparison = {
|
|
13
|
+
id: "status" | "elapsed" | "input" | "output" | "bytes" | "streaming";
|
|
14
|
+
label: string;
|
|
15
|
+
original: ReplayMetricValue;
|
|
16
|
+
replay: ReplayMetricValue;
|
|
17
|
+
delta: number | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type JsonObject = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function byteLength(value: string | null | undefined): number | null {
|
|
27
|
+
if (value === null || value === undefined) return null;
|
|
28
|
+
return new TextEncoder().encode(value).length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readRequestModel(body: string): string | null {
|
|
32
|
+
try {
|
|
33
|
+
const parsed: unknown = JSON.parse(body);
|
|
34
|
+
if (!isJsonObject(parsed)) return null;
|
|
35
|
+
const model = parsed["model"];
|
|
36
|
+
return typeof model === "string" && model.length > 0 ? model : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function replaceRequestModel(
|
|
43
|
+
body: string,
|
|
44
|
+
model: string,
|
|
45
|
+
): { body: string; error: string | null } {
|
|
46
|
+
try {
|
|
47
|
+
const parsed: unknown = JSON.parse(body);
|
|
48
|
+
if (!isJsonObject(parsed)) {
|
|
49
|
+
return { body, error: "Request body must be a JSON object." };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
body: JSON.stringify({ ...parsed, model }, null, 2),
|
|
53
|
+
error: null,
|
|
54
|
+
};
|
|
55
|
+
} catch {
|
|
56
|
+
return { body, error: "Request body must be valid JSON before changing the replay model." };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildReplayMetrics(input: {
|
|
61
|
+
status: number | null | undefined;
|
|
62
|
+
elapsedMs: number | null | undefined;
|
|
63
|
+
inputTokens: number | null | undefined;
|
|
64
|
+
outputTokens: number | null | undefined;
|
|
65
|
+
responseText: string | null | undefined;
|
|
66
|
+
streaming: boolean | null | undefined;
|
|
67
|
+
}): ReplayMetrics {
|
|
68
|
+
return {
|
|
69
|
+
status: input.status ?? null,
|
|
70
|
+
elapsedMs: input.elapsedMs ?? null,
|
|
71
|
+
inputTokens: input.inputTokens ?? null,
|
|
72
|
+
outputTokens: input.outputTokens ?? null,
|
|
73
|
+
responseBytes: byteLength(input.responseText),
|
|
74
|
+
streaming: input.streaming === true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function numericDelta(original: number | null, replay: number | null): number | null {
|
|
79
|
+
if (original === null || replay === null) return null;
|
|
80
|
+
return replay - original;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildReplayComparisons(
|
|
84
|
+
original: ReplayMetrics,
|
|
85
|
+
replay: ReplayMetrics,
|
|
86
|
+
): ReplayMetricComparison[] {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
id: "status",
|
|
90
|
+
label: "Status",
|
|
91
|
+
original: original.status,
|
|
92
|
+
replay: replay.status,
|
|
93
|
+
delta: numericDelta(original.status, replay.status),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "elapsed",
|
|
97
|
+
label: "Elapsed",
|
|
98
|
+
original: original.elapsedMs,
|
|
99
|
+
replay: replay.elapsedMs,
|
|
100
|
+
delta: numericDelta(original.elapsedMs, replay.elapsedMs),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "input",
|
|
104
|
+
label: "Input",
|
|
105
|
+
original: original.inputTokens,
|
|
106
|
+
replay: replay.inputTokens,
|
|
107
|
+
delta: numericDelta(original.inputTokens, replay.inputTokens),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "output",
|
|
111
|
+
label: "Output",
|
|
112
|
+
original: original.outputTokens,
|
|
113
|
+
replay: replay.outputTokens,
|
|
114
|
+
delta: numericDelta(original.outputTokens, replay.outputTokens),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "bytes",
|
|
118
|
+
label: "Bytes",
|
|
119
|
+
original: original.responseBytes,
|
|
120
|
+
replay: replay.responseBytes,
|
|
121
|
+
delta: numericDelta(original.responseBytes, replay.responseBytes),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "streaming",
|
|
125
|
+
label: "Stream",
|
|
126
|
+
original: original.streaming,
|
|
127
|
+
replay: replay.streaming,
|
|
128
|
+
delta: null,
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|