@tonyclaw/agent-inspector 2.1.14 → 2.1.16
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-Ccrrcx1j.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +106 -0
- package/.output/public/assets/ReplayDialog-C7axhr-l.js +1 -0
- package/.output/public/assets/{RequestAnatomy-BNahe83D.js → RequestAnatomy-WOptg8j_.js} +1 -1
- package/.output/public/assets/{ResponseView-DSOnGqi6.js → ResponseView-CSAcxh_M.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-BEKTDklB.js → StreamingChunkSequence-DJS5KhPx.js} +1 -1
- package/.output/public/assets/_sessionId-DUSalzKH.js +1 -0
- package/.output/public/assets/index-BPpA21dY.css +1 -0
- package/.output/public/assets/index-Bt0Az2I2.js +1 -0
- package/.output/public/assets/{index-DWOkqdCa.js → index-DMPNh46t.js} +1 -1
- package/.output/public/assets/{json-viewer-C2JpgcW0.js → json-viewer-inX5QSa3.js} +1 -1
- package/.output/public/assets/{main-CSONBwwn.js → main-aCAKYGSD.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +224 -194
- package/.output/server/{_sessionId-CPAa37n5.mjs → _sessionId-C0fhvQnn.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-ceW5VxMo.mjs → CompareDrawer-CO-Ti5sg.mjs} +36 -15
- package/.output/server/_ssr/{ProxyViewerContainer-CDfEE_w-.mjs → ProxyViewerContainer-N4J7uBCX.mjs} +1153 -494
- package/.output/server/_ssr/{ReplayDialog-V0s_eEbR.mjs → ReplayDialog-J1VxC0In.mjs} +14 -7
- package/.output/server/_ssr/{RequestAnatomy-f1ccwR9d.mjs → RequestAnatomy-DBH-F3rq.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-BIRrqG4H.mjs → ResponseView-DLFwV7Hd.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-V3JFjCgX.mjs → StreamingChunkSequence-BywmuND0.mjs} +3 -3
- package/.output/server/_ssr/{index-DsykulzS.mjs → index-CStymIc0.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Dcnm0Ivf.mjs → json-viewer-DlQu0N1x.mjs} +3 -3
- package/.output/server/_ssr/{router-4bdm6Mt2.mjs → router-CQ4Zxhmr.mjs} +118 -47
- package/.output/server/{_tanstack-start-manifest_v-Xp4CO64V.mjs → _tanstack-start-manifest_v-BHeJBGfo.mjs} +1 -1
- package/.output/server/index.mjs +75 -75
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +174 -107
- package/src/components/ProxyViewerContainer.tsx +77 -67
- package/src/components/alerts/AlertsDialog.tsx +2 -1
- package/src/components/clients/ClientLogo.tsx +132 -0
- package/src/components/groups/GroupsDialog.tsx +37 -14
- package/src/components/providers/ProviderCard.tsx +1 -1
- package/src/components/providers/ProviderForm.tsx +18 -18
- package/src/components/providers/ProviderLogo.tsx +6 -1
- package/src/components/providers/ProvidersPanel.tsx +6 -6
- package/src/components/providers/SettingsDialog.tsx +13 -6
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +62 -29
- package/src/components/proxy-viewer/CompareDrawer.tsx +38 -8
- package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
- package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
- package/src/components/proxy-viewer/ConversationHeader.tsx +65 -61
- package/src/components/proxy-viewer/LogEntry.tsx +31 -18
- package/src/components/proxy-viewer/LogEntryHeader.tsx +42 -49
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
- package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
- package/src/components/proxy-viewer/ThreadConnector.tsx +22 -11
- package/src/components/proxy-viewer/TurnGroup.tsx +30 -36
- package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
- package/src/components/proxy-viewer/viewerState.ts +5 -6
- package/src/components/ui/icon-trigger.ts +2 -0
- package/src/components/ui/transient-toast.tsx +104 -0
- package/src/lib/sessionInfoContract.ts +5 -0
- package/src/lib/stopReason.ts +48 -16
- package/src/mcp/server.ts +71 -27
- package/src/mcp/toolHandlers.ts +23 -4
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
- package/src/proxy/sessionInfo.ts +36 -3
- package/src/proxy/sessionSupervisor.ts +20 -1
- package/src/proxy/store.ts +1 -1
- package/src/routes/api/providers.$providerId.test.log.ts +8 -4
- package/styles/globals.css +180 -180
- package/.output/public/assets/CompareDrawer-DjgjIFx7.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +0 -106
- package/.output/public/assets/ReplayDialog-CU0Tbb2c.js +0 -1
- package/.output/public/assets/_sessionId-Cif8JZdn.js +0 -1
- package/.output/public/assets/index-D_WfwzUi.js +0 -1
- package/.output/public/assets/index-DtLuQrs0.css +0 -1
|
@@ -521,7 +521,7 @@ export function ProvidersPanel({
|
|
|
521
521
|
if (showForm || editingProvider) {
|
|
522
522
|
return (
|
|
523
523
|
<div className="space-y-4">
|
|
524
|
-
<div className="
|
|
524
|
+
<div className="border border-border bg-background/95 shadow-sm sticky top-0 z-10 flex items-center justify-between rounded-[8px] px-3 py-2">
|
|
525
525
|
<h3 className="text-lg font-medium">
|
|
526
526
|
{editingProvider ? "Edit Provider" : "Add New Provider"}
|
|
527
527
|
</h3>
|
|
@@ -540,7 +540,7 @@ export function ProvidersPanel({
|
|
|
540
540
|
|
|
541
541
|
return (
|
|
542
542
|
<div className="space-y-4">
|
|
543
|
-
<div className="
|
|
543
|
+
<div className="border border-border bg-background/95 shadow-sm sticky top-0 z-10 flex items-center justify-between rounded-[8px] px-3 py-2">
|
|
544
544
|
<h3 className="text-lg font-medium">Providers</h3>
|
|
545
545
|
<div className="flex items-center gap-2">
|
|
546
546
|
<TooltipProvider>
|
|
@@ -608,7 +608,7 @@ export function ProvidersPanel({
|
|
|
608
608
|
</div>
|
|
609
609
|
|
|
610
610
|
{configPath !== null && (
|
|
611
|
-
<div className="
|
|
611
|
+
<div className="border border-border bg-card shadow-sm flex items-center gap-2 rounded-[8px] px-3 py-2 text-xs text-muted-foreground">
|
|
612
612
|
<span className="shrink-0">Config:</span>
|
|
613
613
|
<span className="font-mono truncate" title={configPath}>
|
|
614
614
|
{configPath}
|
|
@@ -644,14 +644,14 @@ export function ProvidersPanel({
|
|
|
644
644
|
)}
|
|
645
645
|
|
|
646
646
|
{error !== null && (
|
|
647
|
-
<div className="
|
|
647
|
+
<div className="border border-border bg-card shadow-sm flex items-center gap-2 rounded-[8px] px-3 py-2 text-sm text-destructive">
|
|
648
648
|
<AlertCircle className="size-4 shrink-0" />
|
|
649
649
|
{error}
|
|
650
650
|
</div>
|
|
651
651
|
)}
|
|
652
652
|
|
|
653
653
|
{providers.length === 0 ? (
|
|
654
|
-
<div className="
|
|
654
|
+
<div className="border border-border bg-card shadow-sm space-y-3 rounded-[8px] py-12 text-center">
|
|
655
655
|
<p className="text-sm text-muted-foreground">No providers configured yet.</p>
|
|
656
656
|
<Button onClick={() => setShowForm(true)} variant="outline" size="sm">
|
|
657
657
|
<Plus className="size-4" />
|
|
@@ -660,7 +660,7 @@ export function ProvidersPanel({
|
|
|
660
660
|
</div>
|
|
661
661
|
) : (
|
|
662
662
|
<div className="space-y-3">
|
|
663
|
-
<div className="flex gap-1
|
|
663
|
+
<div className="border border-border bg-muted/40 flex gap-1 rounded-[8px] p-1">
|
|
664
664
|
{(["all", "personal", "company"] as const).map((tab) => (
|
|
665
665
|
<TooltipProvider key={tab}>
|
|
666
666
|
<Tooltip>
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
|
|
5
5
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
|
6
6
|
import { Button } from "../ui/button";
|
|
7
|
+
import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
|
|
7
8
|
import { ProvidersPanel } from "./ProvidersPanel";
|
|
8
9
|
import { useProviders } from "../../lib/useProviders";
|
|
9
10
|
import { useStripConfig } from "../../lib/useStripConfig";
|
|
@@ -61,7 +62,13 @@ export function SettingsDialog(): JSX.Element {
|
|
|
61
62
|
return (
|
|
62
63
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
63
64
|
<DialogTrigger asChild>
|
|
64
|
-
<Button
|
|
65
|
+
<Button
|
|
66
|
+
variant="ghost"
|
|
67
|
+
size="icon"
|
|
68
|
+
className={INSPECTOR_ICON_TRIGGER_CLASS}
|
|
69
|
+
aria-label="Settings"
|
|
70
|
+
title="Settings"
|
|
71
|
+
>
|
|
65
72
|
<Settings className="size-4" />
|
|
66
73
|
<span className="sr-only">Settings</span>
|
|
67
74
|
</Button>
|
|
@@ -226,25 +233,25 @@ function StorageSettingsTab(): JSX.Element {
|
|
|
226
233
|
copiedId={copiedId}
|
|
227
234
|
onCopy={handleCopy}
|
|
228
235
|
/>
|
|
229
|
-
<div className="
|
|
236
|
+
<div className="border border-border bg-muted/40 rounded-md px-3 py-2">
|
|
230
237
|
<div className="font-medium">In-memory logs</div>
|
|
231
238
|
<div className="mt-1 font-mono text-muted-foreground">
|
|
232
239
|
{stats.memoryCount.toLocaleString()} loaded
|
|
233
240
|
</div>
|
|
234
241
|
</div>
|
|
235
|
-
<div className="
|
|
242
|
+
<div className="border border-border bg-muted/40 rounded-md px-3 py-2">
|
|
236
243
|
<div className="font-medium">Log files</div>
|
|
237
244
|
<div className="mt-1 font-mono text-muted-foreground">
|
|
238
245
|
{stats.logFileCount.toLocaleString()} files / {formatBytes(stats.logBytes)}
|
|
239
246
|
</div>
|
|
240
247
|
</div>
|
|
241
|
-
<div className="
|
|
248
|
+
<div className="border border-border bg-muted/40 rounded-md px-3 py-2">
|
|
242
249
|
<div className="font-medium">Streaming chunks</div>
|
|
243
250
|
<div className="mt-1 font-mono text-muted-foreground">
|
|
244
251
|
{stats.chunkFileCount.toLocaleString()} files / {formatBytes(stats.chunkBytes)}
|
|
245
252
|
</div>
|
|
246
253
|
</div>
|
|
247
|
-
<div className="
|
|
254
|
+
<div className="border border-border bg-muted/40 rounded-md px-3 py-2">
|
|
248
255
|
<div className="font-medium">Session archives</div>
|
|
249
256
|
<div className="mt-1 font-mono text-muted-foreground">
|
|
250
257
|
{stats.sessionArchiveFileCount.toLocaleString()} files /{" "}
|
|
@@ -274,7 +281,7 @@ function CopyableSetupValue({
|
|
|
274
281
|
}): JSX.Element {
|
|
275
282
|
const copied = copiedId === id;
|
|
276
283
|
return (
|
|
277
|
-
<div className="
|
|
284
|
+
<div className="border border-border bg-muted/40 min-w-0 rounded-md px-3 py-2">
|
|
278
285
|
<div className="mb-1 text-xs font-medium text-muted-foreground">{label}</div>
|
|
279
286
|
<div className="flex min-w-0 items-center gap-2">
|
|
280
287
|
<code className="min-w-0 flex-1 truncate font-mono text-xs text-foreground">{value}</code>
|
|
@@ -146,11 +146,29 @@ function firstToolLog(logs: CapturedLog[]): CapturedLog | null {
|
|
|
146
146
|
return null;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function buildDisplayNumberByLogId(logs: readonly CapturedLog[]): Map<number, number> {
|
|
150
|
+
const result = new Map<number, number>();
|
|
151
|
+
for (let index = 0; index < logs.length; index += 1) {
|
|
152
|
+
const log = logs[index];
|
|
153
|
+
if (log === undefined) continue;
|
|
154
|
+
result.set(log.id, index + 1);
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function displayLogNumber(
|
|
160
|
+
logId: number,
|
|
161
|
+
displayNumberByLogId: ReadonlyMap<number, number>,
|
|
162
|
+
): string {
|
|
163
|
+
return String(displayNumberByLogId.get(logId) ?? logId);
|
|
164
|
+
}
|
|
165
|
+
|
|
149
166
|
function buildTraceInsights(input: {
|
|
150
167
|
logs: CapturedLog[];
|
|
151
168
|
scopeId: string;
|
|
152
169
|
summary: TraceSummary;
|
|
153
170
|
candidates: KnowledgeCandidate[];
|
|
171
|
+
displayNumberByLogId: ReadonlyMap<number, number>;
|
|
154
172
|
}): TraceInsight[] {
|
|
155
173
|
const insights: TraceInsight[] = [
|
|
156
174
|
{
|
|
@@ -170,7 +188,7 @@ function buildTraceInsights(input: {
|
|
|
170
188
|
title: `${String(input.summary.toolCallCount)} tool call${
|
|
171
189
|
input.summary.toolCallCount === 1 ? "" : "s"
|
|
172
190
|
}`,
|
|
173
|
-
detail: `First tool evidence at #${
|
|
191
|
+
detail: `First tool evidence at #${displayLogNumber(toolLog.id, input.displayNumberByLogId)}`,
|
|
174
192
|
logId: toolLog.id,
|
|
175
193
|
});
|
|
176
194
|
}
|
|
@@ -180,7 +198,7 @@ function buildTraceInsights(input: {
|
|
|
180
198
|
insights.push({
|
|
181
199
|
kind: "failure",
|
|
182
200
|
title: `Failure #${String(failure.responseStatus)}`,
|
|
183
|
-
detail: `First failed request is #${
|
|
201
|
+
detail: `First failed request is #${displayLogNumber(failure.id, input.displayNumberByLogId)}`,
|
|
184
202
|
logId: failure.id,
|
|
185
203
|
});
|
|
186
204
|
}
|
|
@@ -190,7 +208,7 @@ function buildTraceInsights(input: {
|
|
|
190
208
|
insights.push({
|
|
191
209
|
kind: "slow",
|
|
192
210
|
title: `Slowest ${formatElapsed(input.summary.maxElapsedMs)}`,
|
|
193
|
-
detail: `Max latency observed at #${
|
|
211
|
+
detail: `Max latency observed at #${displayLogNumber(slowest.id, input.displayNumberByLogId)}`,
|
|
194
212
|
logId: slowest.id,
|
|
195
213
|
});
|
|
196
214
|
}
|
|
@@ -291,12 +309,16 @@ function previewText(value: string, maxLength: number): string {
|
|
|
291
309
|
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
292
310
|
}
|
|
293
311
|
|
|
294
|
-
function logRangeLabel(
|
|
312
|
+
function logRangeLabel(
|
|
313
|
+
logIds: number[],
|
|
314
|
+
displayNumberByLogId: ReadonlyMap<number, number>,
|
|
315
|
+
): string {
|
|
295
316
|
const first = logIds[0];
|
|
296
317
|
const last = logIds[logIds.length - 1];
|
|
297
318
|
if (first === undefined) return "No evidence logs";
|
|
298
|
-
|
|
299
|
-
|
|
319
|
+
const firstLabel = displayLogNumber(first, displayNumberByLogId);
|
|
320
|
+
if (last === undefined || first === last) return `#${firstLabel}`;
|
|
321
|
+
return `#${firstLabel}-#${displayLogNumber(last, displayNumberByLogId)}`;
|
|
300
322
|
}
|
|
301
323
|
|
|
302
324
|
function tagsToText(tags: string[]): string {
|
|
@@ -316,12 +338,14 @@ function CandidateItem({
|
|
|
316
338
|
isUpdating,
|
|
317
339
|
onPromoteCandidate,
|
|
318
340
|
onUpdateCandidate,
|
|
341
|
+
displayNumberByLogId,
|
|
319
342
|
}: {
|
|
320
343
|
candidate: KnowledgeCandidate;
|
|
321
344
|
isPromoting: boolean;
|
|
322
345
|
isUpdating: boolean;
|
|
323
346
|
onPromoteCandidate: (candidateId: string) => void;
|
|
324
347
|
onUpdateCandidate: (candidateId: string, update: CandidateDraftUpdate) => Promise<boolean>;
|
|
348
|
+
displayNumberByLogId: ReadonlyMap<number, number>;
|
|
325
349
|
}): JSX.Element {
|
|
326
350
|
const [editing, setEditing] = useState(false);
|
|
327
351
|
const [draftType, setDraftType] = useState<KnowledgeCandidate["type"]>(candidate.type);
|
|
@@ -368,7 +392,7 @@ function CandidateItem({
|
|
|
368
392
|
|
|
369
393
|
if (editing) {
|
|
370
394
|
return (
|
|
371
|
-
<div className="
|
|
395
|
+
<div className="border border-border bg-card shadow-sm rounded-[8px] px-2.5 py-2">
|
|
372
396
|
<div className="grid gap-2">
|
|
373
397
|
<div className="flex flex-wrap items-center gap-2">
|
|
374
398
|
<select
|
|
@@ -380,7 +404,7 @@ function CandidateItem({
|
|
|
380
404
|
);
|
|
381
405
|
if (parsed.success) setDraftType(parsed.data);
|
|
382
406
|
}}
|
|
383
|
-
className="
|
|
407
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-7 rounded-md px-2 text-xs"
|
|
384
408
|
aria-label="Candidate type"
|
|
385
409
|
>
|
|
386
410
|
<option value="episode">episode</option>
|
|
@@ -392,7 +416,7 @@ function CandidateItem({
|
|
|
392
416
|
value={draftTitle}
|
|
393
417
|
disabled={isUpdating}
|
|
394
418
|
onChange={(event) => setDraftTitle(event.currentTarget.value)}
|
|
395
|
-
className="h-7 min-w-[220px] flex-1 rounded-md
|
|
419
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-7 min-w-[220px] flex-1 rounded-md px-2 text-xs"
|
|
396
420
|
aria-label="Candidate title"
|
|
397
421
|
/>
|
|
398
422
|
</div>
|
|
@@ -400,14 +424,14 @@ function CandidateItem({
|
|
|
400
424
|
value={draftContent}
|
|
401
425
|
disabled={isUpdating}
|
|
402
426
|
onChange={(event) => setDraftContent(event.currentTarget.value)}
|
|
403
|
-
className="min-h-28 rounded-md
|
|
427
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground min-h-28 rounded-md px-2 py-1.5 font-mono text-[11px] leading-relaxed"
|
|
404
428
|
aria-label="Candidate content"
|
|
405
429
|
/>
|
|
406
430
|
<input
|
|
407
431
|
value={draftTags}
|
|
408
432
|
disabled={isUpdating}
|
|
409
433
|
onChange={(event) => setDraftTags(event.currentTarget.value)}
|
|
410
|
-
className="
|
|
434
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-7 rounded-md px-2 text-xs"
|
|
411
435
|
aria-label="Candidate tags"
|
|
412
436
|
/>
|
|
413
437
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -484,7 +508,7 @@ function CandidateItem({
|
|
|
484
508
|
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-muted-foreground">
|
|
485
509
|
<span className="inline-flex items-center gap-1">
|
|
486
510
|
<FileSearch className="size-3" />
|
|
487
|
-
{logRangeLabel(candidate.logIds)}
|
|
511
|
+
{logRangeLabel(candidate.logIds, displayNumberByLogId)}
|
|
488
512
|
</span>
|
|
489
513
|
<span className="inline-flex items-center gap-1">
|
|
490
514
|
<ShieldCheck className="size-3" />
|
|
@@ -504,20 +528,24 @@ function CandidateItem({
|
|
|
504
528
|
)}
|
|
505
529
|
</div>
|
|
506
530
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
507
|
-
{candidate.logIds.map((logId) =>
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
531
|
+
{candidate.logIds.map((logId) => {
|
|
532
|
+
const displayNumber = displayLogNumber(logId, displayNumberByLogId);
|
|
533
|
+
return (
|
|
534
|
+
<a
|
|
535
|
+
key={logId}
|
|
536
|
+
href={`#${getLogAnchor(logId)}`}
|
|
537
|
+
onClick={(event) => {
|
|
538
|
+
event.preventDefault();
|
|
539
|
+
jumpToLog(logId);
|
|
540
|
+
}}
|
|
541
|
+
className="rounded border border-blue-400/25 px-1.5 py-0.5 font-mono text-[10px] text-blue-400 underline-offset-2 transition-colors hover:bg-blue-400/10 hover:text-blue-300 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
542
|
+
aria-label={`Jump to evidence log ${displayNumber}`}
|
|
543
|
+
title={`Log ID ${String(logId)}`}
|
|
544
|
+
>
|
|
545
|
+
#{displayNumber}
|
|
546
|
+
</a>
|
|
547
|
+
);
|
|
548
|
+
})}
|
|
521
549
|
<span className="flex-1" />
|
|
522
550
|
{canEdit && (
|
|
523
551
|
<Button
|
|
@@ -560,12 +588,14 @@ function CandidateList({
|
|
|
560
588
|
updatingCandidateIds,
|
|
561
589
|
onPromoteCandidate,
|
|
562
590
|
onUpdateCandidate,
|
|
591
|
+
displayNumberByLogId,
|
|
563
592
|
}: {
|
|
564
593
|
candidates: KnowledgeCandidate[];
|
|
565
594
|
promotingCandidateIds: ReadonlySet<string>;
|
|
566
595
|
updatingCandidateIds: ReadonlySet<string>;
|
|
567
596
|
onPromoteCandidate: (candidateId: string) => void;
|
|
568
597
|
onUpdateCandidate: (candidateId: string, update: CandidateDraftUpdate) => Promise<boolean>;
|
|
598
|
+
displayNumberByLogId: ReadonlyMap<number, number>;
|
|
569
599
|
}): JSX.Element | null {
|
|
570
600
|
if (candidates.length === 0) return null;
|
|
571
601
|
return (
|
|
@@ -578,6 +608,7 @@ function CandidateList({
|
|
|
578
608
|
isUpdating={updatingCandidateIds.has(candidate.id)}
|
|
579
609
|
onPromoteCandidate={onPromoteCandidate}
|
|
580
610
|
onUpdateCandidate={onUpdateCandidate}
|
|
611
|
+
displayNumberByLogId={displayNumberByLogId}
|
|
581
612
|
/>
|
|
582
613
|
))}
|
|
583
614
|
</div>
|
|
@@ -608,9 +639,10 @@ export function AgentTraceSummary({
|
|
|
608
639
|
() => buildTraceSummary(logs, slowResponseThresholdSeconds, candidates.length),
|
|
609
640
|
[candidates.length, logs, slowResponseThresholdSeconds],
|
|
610
641
|
);
|
|
642
|
+
const displayNumberByLogId = useMemo(() => buildDisplayNumberByLogId(logs), [logs]);
|
|
611
643
|
const traceInsights = useMemo(
|
|
612
|
-
() => buildTraceInsights({ logs, scopeId, summary, candidates }),
|
|
613
|
-
[candidates, logs, scopeId, summary],
|
|
644
|
+
() => buildTraceInsights({ logs, scopeId, summary, candidates, displayNumberByLogId }),
|
|
645
|
+
[candidates, displayNumberByLogId, logs, scopeId, summary],
|
|
614
646
|
);
|
|
615
647
|
const showElapsedSummary = showRollupMetrics || summary.maxElapsedMs !== null;
|
|
616
648
|
const timeRange = useMemo(
|
|
@@ -762,7 +794,7 @@ export function AgentTraceSummary({
|
|
|
762
794
|
if (logs.length === 0) return null;
|
|
763
795
|
|
|
764
796
|
return (
|
|
765
|
-
<section className="
|
|
797
|
+
<section className="border border-border bg-card shadow-sm mb-2 rounded-[8px] px-3 py-2">
|
|
766
798
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-xs">
|
|
767
799
|
{showRollupMetrics && (
|
|
768
800
|
<span className="inline-flex items-center gap-1.5 font-semibold text-foreground">
|
|
@@ -870,6 +902,7 @@ export function AgentTraceSummary({
|
|
|
870
902
|
updatingCandidateIds={updatingCandidateIds}
|
|
871
903
|
onPromoteCandidate={promoteCandidate}
|
|
872
904
|
onUpdateCandidate={updateCandidate}
|
|
905
|
+
displayNumberByLogId={displayNumberByLogId}
|
|
873
906
|
/>
|
|
874
907
|
)}
|
|
875
908
|
</section>
|
|
@@ -32,6 +32,10 @@ export type CompareDrawerProps = {
|
|
|
32
32
|
left: CapturedLog;
|
|
33
33
|
/** Log selected second (shown on the right). */
|
|
34
34
|
right: CapturedLog;
|
|
35
|
+
/** Human-facing ordinal inside the left log's session. */
|
|
36
|
+
leftDisplayNumber: number;
|
|
37
|
+
/** Human-facing ordinal inside the right log's session. */
|
|
38
|
+
rightDisplayNumber: number;
|
|
35
39
|
onClose: () => void;
|
|
36
40
|
};
|
|
37
41
|
|
|
@@ -541,7 +545,15 @@ function ModeToggle({
|
|
|
541
545
|
);
|
|
542
546
|
}
|
|
543
547
|
|
|
544
|
-
function SideSummary({
|
|
548
|
+
function SideSummary({
|
|
549
|
+
log,
|
|
550
|
+
side,
|
|
551
|
+
displayNumber,
|
|
552
|
+
}: {
|
|
553
|
+
log: CapturedLog;
|
|
554
|
+
side: "left" | "right";
|
|
555
|
+
displayNumber: number;
|
|
556
|
+
}): JSX.Element {
|
|
545
557
|
const conversationId = getConversationId(log);
|
|
546
558
|
return (
|
|
547
559
|
<div className="flex-1 min-w-0 space-y-1 text-xs">
|
|
@@ -557,7 +569,9 @@ function SideSummary({ log, side }: { log: CapturedLog; side: "left" | "right" }
|
|
|
557
569
|
>
|
|
558
570
|
{side === "left" ? "← Left" : "Right →"}
|
|
559
571
|
</Badge>
|
|
560
|
-
<span className="font-mono text-blue-400/80"
|
|
572
|
+
<span className="font-mono text-blue-400/80" title={`Log ID ${String(log.id)}`}>
|
|
573
|
+
#{displayNumber}
|
|
574
|
+
</span>
|
|
561
575
|
{log.model !== null && (
|
|
562
576
|
<span className="font-mono text-muted-foreground truncate">{log.model}</span>
|
|
563
577
|
)}
|
|
@@ -584,7 +598,13 @@ function SideSummary({ log, side }: { log: CapturedLog; side: "left" | "right" }
|
|
|
584
598
|
);
|
|
585
599
|
}
|
|
586
600
|
|
|
587
|
-
export function CompareDrawer({
|
|
601
|
+
export function CompareDrawer({
|
|
602
|
+
left,
|
|
603
|
+
right,
|
|
604
|
+
leftDisplayNumber,
|
|
605
|
+
rightDisplayNumber,
|
|
606
|
+
onClose,
|
|
607
|
+
}: CompareDrawerProps): JSX.Element {
|
|
588
608
|
// Memoize the diff so re-renders (e.g. parent re-renders) don't recompute.
|
|
589
609
|
const ops = useMemo<DiffOp[]>(() => {
|
|
590
610
|
const leftRequest = getLogFormatAdapter(resolveLogFormat(left)).analyzeRequest(
|
|
@@ -741,8 +761,8 @@ export function CompareDrawer({ left, right, onClose }: CompareDrawerProps): JSX
|
|
|
741
761
|
{/* Header */}
|
|
742
762
|
<div className="flex items-start gap-4 px-4 py-3 border-b border-border">
|
|
743
763
|
<div className="flex-1 flex gap-4 min-w-0">
|
|
744
|
-
<SideSummary log={left} side="left" />
|
|
745
|
-
<SideSummary log={right} side="right" />
|
|
764
|
+
<SideSummary log={left} side="left" displayNumber={leftDisplayNumber} />
|
|
765
|
+
<SideSummary log={right} side="right" displayNumber={rightDisplayNumber} />
|
|
746
766
|
</div>
|
|
747
767
|
<div className="flex items-center gap-2 shrink-0">
|
|
748
768
|
<ModeToggle mode={mode} onChange={setMode} />
|
|
@@ -799,7 +819,13 @@ export function CompareDrawer({ left, right, onClose }: CompareDrawerProps): JSX
|
|
|
799
819
|
})}
|
|
800
820
|
</div>
|
|
801
821
|
) : (
|
|
802
|
-
<SplitBody
|
|
822
|
+
<SplitBody
|
|
823
|
+
grouped={grouped}
|
|
824
|
+
left={left}
|
|
825
|
+
right={right}
|
|
826
|
+
leftDisplayNumber={leftDisplayNumber}
|
|
827
|
+
rightDisplayNumber={rightDisplayNumber}
|
|
828
|
+
/>
|
|
803
829
|
)}
|
|
804
830
|
</div>
|
|
805
831
|
</>
|
|
@@ -814,18 +840,22 @@ function SplitBody({
|
|
|
814
840
|
grouped,
|
|
815
841
|
left,
|
|
816
842
|
right,
|
|
843
|
+
leftDisplayNumber,
|
|
844
|
+
rightDisplayNumber,
|
|
817
845
|
}: {
|
|
818
846
|
grouped: GroupedOp[];
|
|
819
847
|
left: CapturedLog;
|
|
820
848
|
right: CapturedLog;
|
|
849
|
+
leftDisplayNumber: number;
|
|
850
|
+
rightDisplayNumber: number;
|
|
821
851
|
}): JSX.Element {
|
|
822
852
|
return (
|
|
823
853
|
<div className="grid grid-cols-[200px_1fr_1fr] gap-x-2 gap-y-0.5 px-3 py-2 text-xs">
|
|
824
854
|
{/* Column headers */}
|
|
825
855
|
<div className="grid grid-cols-[200px_1fr_1fr] gap-x-2 col-span-3 pb-2 mb-2 border-b border-border text-[10px] uppercase tracking-wider text-muted-foreground">
|
|
826
856
|
<span>Path</span>
|
|
827
|
-
<span>Left (Log #{
|
|
828
|
-
<span>Right (Log #{
|
|
857
|
+
<span title={`Log ID ${String(left.id)}`}>Left (Log #{leftDisplayNumber})</span>
|
|
858
|
+
<span title={`Log ID ${String(right.id)}`}>Right (Log #{rightDisplayNumber})</span>
|
|
829
859
|
</div>
|
|
830
860
|
|
|
831
861
|
{grouped.map((g, i) => {
|
|
@@ -8,7 +8,7 @@ import { TurnGroupList } from "./TurnGroupList";
|
|
|
8
8
|
import { AgentTraceSummary } from "./AgentTraceSummary";
|
|
9
9
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
10
10
|
import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
|
|
11
|
-
import { buildTurnGroups
|
|
11
|
+
import { buildTurnGroups } from "./viewerState";
|
|
12
12
|
|
|
13
13
|
export type ConversationGroupProps = {
|
|
14
14
|
group: ConversationGroupData;
|
|
@@ -25,14 +25,18 @@ export type ConversationGroupProps = {
|
|
|
25
25
|
/** Callback to open CompareDrawer with a log and its immediate predecessor. */
|
|
26
26
|
onCompareWithPrevious: (log: CapturedLog) => void;
|
|
27
27
|
comparisonPredecessors: Map<number, CapturedLog>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/** When true, the route-level session context already shows rollup metrics. */
|
|
31
|
-
hasPinnedSessionContext?: boolean;
|
|
28
|
+
/** Initial expansion state. Single-session pages use this to show logs under the session header. */
|
|
29
|
+
defaultExpanded?: boolean;
|
|
32
30
|
/** Controls whether timestamps render as compact local time or full ISO strings. */
|
|
33
31
|
timeDisplayFormat: TimeDisplayFormat;
|
|
34
32
|
/** Clear all logs that belong to this group. */
|
|
35
33
|
onClearGroup: (ids: number[]) => void;
|
|
34
|
+
/** Show IDE identity in the session header. */
|
|
35
|
+
showClientIdentity?: boolean;
|
|
36
|
+
/** Show process/project metadata in the session header. */
|
|
37
|
+
showProcessMetadata?: boolean;
|
|
38
|
+
/** Show client PID/project metadata on individual log rows. */
|
|
39
|
+
showLogClientMetadata?: boolean;
|
|
36
40
|
/** Optional controlled expansion state used by virtualized lists. */
|
|
37
41
|
expanded?: boolean;
|
|
38
42
|
/** Optional controlled expansion setter used by virtualized lists. */
|
|
@@ -104,13 +108,15 @@ export const ConversationGroup = memo(function ({
|
|
|
104
108
|
onCompareWithPrevious,
|
|
105
109
|
comparisonPredecessors,
|
|
106
110
|
onClearGroup,
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
showClientIdentity = true,
|
|
112
|
+
showProcessMetadata = true,
|
|
113
|
+
showLogClientMetadata = true,
|
|
114
|
+
defaultExpanded = false,
|
|
109
115
|
timeDisplayFormat,
|
|
110
116
|
expanded: controlledExpanded,
|
|
111
117
|
onExpandedChange,
|
|
112
118
|
}: ConversationGroupProps): JSX.Element {
|
|
113
|
-
const [internalExpanded, setInternalExpanded] = useState(
|
|
119
|
+
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
114
120
|
const expanded = controlledExpanded ?? internalExpanded;
|
|
115
121
|
const setExpanded = useCallback(
|
|
116
122
|
(nextExpanded: boolean) => {
|
|
@@ -122,12 +128,17 @@ export const ConversationGroup = memo(function ({
|
|
|
122
128
|
},
|
|
123
129
|
[onExpandedChange],
|
|
124
130
|
);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!defaultExpanded || controlledExpanded !== undefined) return;
|
|
134
|
+
setInternalExpanded(true);
|
|
135
|
+
}, [controlledExpanded, defaultExpanded]);
|
|
136
|
+
|
|
125
137
|
const stats = useMemo(() => computeStats(group.logs), [group.logs]);
|
|
126
138
|
const providers = useMemo(() => collectProviders(group.logs), [group.logs]);
|
|
127
139
|
const startTime = group.logs[0]?.timestamp ?? new Date().toISOString();
|
|
128
140
|
const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
|
|
129
141
|
const isLoading = group.logs.some((log) => log.responseStatus === null);
|
|
130
|
-
const showTraceRollupMetrics = standalone && !hasPinnedSessionContext;
|
|
131
142
|
const clientPid = useMemo(() => getFirstClientPid(group.logs), [group.logs]);
|
|
132
143
|
const clientProjectFolder = useMemo(() => getFirstClientProjectFolder(group.logs), [group.logs]);
|
|
133
144
|
|
|
@@ -136,7 +147,6 @@ export const ConversationGroup = memo(function ({
|
|
|
136
147
|
|
|
137
148
|
useEffect(() => {
|
|
138
149
|
const handleLogFocusRequest = (event: Event): void => {
|
|
139
|
-
if (standalone) return;
|
|
140
150
|
const request = readLogFocusRequest(event);
|
|
141
151
|
if (request === null) return;
|
|
142
152
|
if (!group.logs.some((log) => log.id === request.logId)) return;
|
|
@@ -147,37 +157,37 @@ export const ConversationGroup = memo(function ({
|
|
|
147
157
|
return () => {
|
|
148
158
|
window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
149
159
|
};
|
|
150
|
-
}, [group.logs, setExpanded
|
|
160
|
+
}, [group.logs, setExpanded]);
|
|
151
161
|
|
|
152
162
|
return (
|
|
153
163
|
<div className="mb-2">
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
164
|
+
<ConversationHeader
|
|
165
|
+
conversationId={group.conversationId}
|
|
166
|
+
startTime={startTime}
|
|
167
|
+
endTime={endTime}
|
|
168
|
+
totalCalls={group.logs.length}
|
|
169
|
+
totalInputTokens={stats.totalInputTokens}
|
|
170
|
+
totalOutputTokens={stats.totalOutputTokens}
|
|
171
|
+
expanded={expanded}
|
|
172
|
+
onToggle={() => setExpanded(!expanded)}
|
|
173
|
+
providers={providers}
|
|
174
|
+
isLoading={isLoading}
|
|
175
|
+
userAgent={group.logs[0]?.userAgent ?? null}
|
|
176
|
+
clientPid={clientPid}
|
|
177
|
+
clientProjectFolder={clientProjectFolder}
|
|
178
|
+
showClientIdentity={showClientIdentity}
|
|
179
|
+
showProcessMetadata={showProcessMetadata}
|
|
180
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
181
|
+
onClear={() => onClearGroup(group.logs.map((l) => l.id))}
|
|
182
|
+
/>
|
|
173
183
|
|
|
174
|
-
{
|
|
184
|
+
{expanded && (
|
|
175
185
|
<div>
|
|
176
186
|
<AgentTraceSummary
|
|
177
187
|
logs={group.logs}
|
|
178
188
|
scopeId={group.conversationId}
|
|
179
189
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
180
|
-
showRollupMetrics={
|
|
190
|
+
showRollupMetrics={false}
|
|
181
191
|
timeDisplayFormat={timeDisplayFormat}
|
|
182
192
|
/>
|
|
183
193
|
<TurnGroupList
|
|
@@ -189,6 +199,7 @@ export const ConversationGroup = memo(function ({
|
|
|
189
199
|
cacheTrends={cacheTrends}
|
|
190
200
|
onCompareWithPrevious={onCompareWithPrevious}
|
|
191
201
|
comparisonPredecessors={comparisonPredecessors}
|
|
202
|
+
showLogClientMetadata={showLogClientMetadata}
|
|
192
203
|
/>
|
|
193
204
|
</div>
|
|
194
205
|
)}
|