@tonyclaw/agent-inspector 2.1.14 → 2.1.15
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-BcEd6V-V.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +106 -0
- package/.output/public/assets/ReplayDialog-BNpC0548.js +1 -0
- package/.output/public/assets/{RequestAnatomy-BNahe83D.js → RequestAnatomy-Ds1uRLVB.js} +1 -1
- package/.output/public/assets/{ResponseView-DSOnGqi6.js → ResponseView-7KPVqKl5.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-BEKTDklB.js → StreamingChunkSequence-BHQT261s.js} +1 -1
- package/.output/public/assets/_sessionId-DWePGjnS.js +1 -0
- package/.output/public/assets/index-CI1-G8ua.js +1 -0
- package/.output/public/assets/index-DdhFqPsI.css +1 -0
- package/.output/public/assets/{index-DWOkqdCa.js → index-DjKt8XKe.js} +1 -1
- package/.output/public/assets/{json-viewer-C2JpgcW0.js → json-viewer-CkCu-rka.js} +1 -1
- package/.output/public/assets/{main-CSONBwwn.js → main-DpD1N0S8.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +194 -200
- package/.output/server/{_sessionId-CPAa37n5.mjs → _sessionId-DF9Sy8cP.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-ceW5VxMo.mjs → CompareDrawer-BoxztaO7.mjs} +36 -15
- package/.output/server/_ssr/{ProxyViewerContainer-CDfEE_w-.mjs → ProxyViewerContainer-CRBkqFlJ.mjs} +362 -124
- package/.output/server/_ssr/{ReplayDialog-V0s_eEbR.mjs → ReplayDialog-Cc1dyDuK.mjs} +14 -7
- package/.output/server/_ssr/{RequestAnatomy-f1ccwR9d.mjs → RequestAnatomy-CMGSsz5Z.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-BIRrqG4H.mjs → ResponseView-Cp10DM1D.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-V3JFjCgX.mjs → StreamingChunkSequence-B1VGxy3A.mjs} +2 -2
- package/.output/server/_ssr/{index-DsykulzS.mjs → index-47XVPghS.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Dcnm0Ivf.mjs → json-viewer-zDE2rrmJ.mjs} +3 -3
- package/.output/server/_ssr/{router-4bdm6Mt2.mjs → router-DVeuZFqI.mjs} +10 -5
- package/.output/server/{_tanstack-start-manifest_v-Xp4CO64V.mjs → _tanstack-start-manifest_v-Bp8JxtPW.mjs} +1 -1
- package/.output/server/index.mjs +68 -68
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +28 -2
- package/src/components/clients/ClientLogo.tsx +132 -0
- package/src/components/groups/GroupsDialog.tsx +18 -13
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +56 -23
- package/src/components/proxy-viewer/CompareDrawer.tsx +38 -8
- package/src/components/proxy-viewer/ConversationHeader.tsx +33 -41
- package/src/components/proxy-viewer/LogEntry.tsx +10 -1
- package/src/components/proxy-viewer/LogEntryHeader.tsx +14 -14
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
- package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
- package/src/components/proxy-viewer/ThreadConnector.tsx +18 -7
- package/src/components/proxy-viewer/TurnGroup.tsx +14 -25
- package/src/components/proxy-viewer/viewerState.ts +5 -2
- package/src/lib/sessionInfoContract.ts +1 -0
- package/src/lib/stopReason.ts +48 -16
- package/src/proxy/sessionInfo.ts +5 -1
- 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
|
@@ -1173,19 +1173,24 @@ function LatestLogsPreview({ logs }: { logs: SessionLogSummary[] }): JSX.Element
|
|
|
1173
1173
|
Latest logs
|
|
1174
1174
|
</div>
|
|
1175
1175
|
<div className="divide-y divide-border">
|
|
1176
|
-
{visibleLogs.map((log) =>
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1176
|
+
{visibleLogs.map((log) => {
|
|
1177
|
+
const displayNumber = log.sessionLogNumber ?? log.id;
|
|
1178
|
+
return (
|
|
1179
|
+
<div
|
|
1180
|
+
key={log.id}
|
|
1181
|
+
className="grid gap-2 px-3 py-2 text-xs sm:grid-cols-[72px_1fr_88px_88px]"
|
|
1182
|
+
>
|
|
1183
|
+
<span className="font-mono text-muted-foreground" title={`Log ID ${String(log.id)}`}>
|
|
1184
|
+
#{displayNumber}
|
|
1185
|
+
</span>
|
|
1186
|
+
<span className="min-w-0 truncate">
|
|
1187
|
+
{displayText(log.model)} · {log.apiFormat} · {log.path}
|
|
1188
|
+
</span>
|
|
1189
|
+
<span className="font-mono text-muted-foreground">{formatMs(log.firstChunkMs)}</span>
|
|
1190
|
+
<span className="font-mono text-muted-foreground">{formatMs(log.latencyMs)}</span>
|
|
1191
|
+
</div>
|
|
1192
|
+
);
|
|
1193
|
+
})}
|
|
1189
1194
|
</div>
|
|
1190
1195
|
</div>
|
|
1191
1196
|
);
|
|
@@ -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);
|
|
@@ -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(
|
|
@@ -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) => {
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
Loader2,
|
|
8
8
|
MessageSquare,
|
|
9
9
|
Trash2,
|
|
10
|
-
User,
|
|
11
10
|
Zap,
|
|
12
11
|
} from "lucide-react";
|
|
13
12
|
import type { JSX } from "react";
|
|
@@ -17,8 +16,9 @@ import { formatTimestampRange } from "../../lib/timeDisplay";
|
|
|
17
16
|
import { cn, formatTokens } from "../../lib/utils";
|
|
18
17
|
import type { CapturedLog } from "../../contracts";
|
|
19
18
|
import { ConfirmDialog } from "../ui/confirm-dialog";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
19
|
+
import type { Provider } from "../providers/ProviderLogo";
|
|
20
|
+
import { ClientLogo, clientAppLabel, detectClientApp } from "../clients/ClientLogo";
|
|
21
|
+
import { ProviderLogoStack, ROW_CHEVRON_SLOT_CLASS } from "./ProviderLogoStack";
|
|
22
22
|
import { resolveLogFormat } from "./log-formats";
|
|
23
23
|
|
|
24
24
|
export type ConversationHeaderProps = {
|
|
@@ -89,6 +89,17 @@ export function ConversationHeader({
|
|
|
89
89
|
},
|
|
90
90
|
[conversationId],
|
|
91
91
|
);
|
|
92
|
+
const providerLogoItems =
|
|
93
|
+
providers?.map(({ provider, models }) => ({
|
|
94
|
+
provider,
|
|
95
|
+
tooltip: models.length === 1 ? (models[0] ?? provider) : models.join(", "),
|
|
96
|
+
})) ?? [];
|
|
97
|
+
const clientApp = detectClientApp({ userAgent, conversationId, clientProjectFolder });
|
|
98
|
+
const detectedClientLabel = clientAppLabel(clientApp);
|
|
99
|
+
const hasUserAgent = userAgent !== null && userAgent !== undefined && userAgent !== "";
|
|
100
|
+
const clientDisplayLabel = detectedClientLabel ?? userAgent ?? null;
|
|
101
|
+
const clientTooltip =
|
|
102
|
+
detectedClientLabel !== null && hasUserAgent ? `${detectedClientLabel} - ${userAgent}` : null;
|
|
92
103
|
|
|
93
104
|
return (
|
|
94
105
|
<div
|
|
@@ -112,14 +123,19 @@ export function ConversationHeader({
|
|
|
112
123
|
}
|
|
113
124
|
}}
|
|
114
125
|
>
|
|
126
|
+
{/* Provider logos: first visual element whenever model/provider logos are available. */}
|
|
127
|
+
<ProviderLogoStack items={providerLogoItems} ariaLabel="Models used in this session" />
|
|
128
|
+
|
|
115
129
|
{/* Expand chevron - shows spinner when collapsed and group has pending logs */}
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
<span className={ROW_CHEVRON_SLOT_CLASS}>
|
|
131
|
+
{expanded ? (
|
|
132
|
+
<ChevronDown className="size-4 shrink-0" />
|
|
133
|
+
) : isLoading ? (
|
|
134
|
+
<Loader2 className="size-4 shrink-0 animate-spin" />
|
|
135
|
+
) : (
|
|
136
|
+
<ChevronRight className="size-4 shrink-0" />
|
|
137
|
+
)}
|
|
138
|
+
</span>
|
|
123
139
|
|
|
124
140
|
{/* Session identifier.
|
|
125
141
|
- Real session id (e.g. `sess_abc123...`) → render the id with the
|
|
@@ -175,41 +191,17 @@ export function ConversationHeader({
|
|
|
175
191
|
</>
|
|
176
192
|
)}
|
|
177
193
|
|
|
178
|
-
{/* User-Agent */}
|
|
179
|
-
{
|
|
194
|
+
{/* Client app / User-Agent */}
|
|
195
|
+
{clientDisplayLabel !== null && clientDisplayLabel !== "" && (
|
|
180
196
|
<span
|
|
181
197
|
className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
|
|
182
|
-
title={
|
|
198
|
+
title={clientTooltip ?? clientDisplayLabel}
|
|
183
199
|
>
|
|
184
|
-
<
|
|
185
|
-
<span className="font-mono tabular-nums truncate max-w-[120px]">
|
|
186
|
-
|
|
187
|
-
)}
|
|
188
|
-
|
|
189
|
-
{/* Provider logos — one per unique provider used in this session. When
|
|
190
|
-
a session switched models mid-stream (e.g. claude-3-5 → deepseek-v4)
|
|
191
|
-
multiple logos appear side-by-side; tooltip lists every distinct
|
|
192
|
-
model name. */}
|
|
193
|
-
{providers !== undefined && providers.length > 0 && (
|
|
194
|
-
<TooltipProvider delayDuration={150}>
|
|
195
|
-
<span
|
|
196
|
-
className="flex items-center gap-0.5 shrink-0"
|
|
197
|
-
aria-label="Models used in this session"
|
|
198
|
-
>
|
|
199
|
-
{providers.map(({ provider, models }) => (
|
|
200
|
-
<Tooltip key={provider}>
|
|
201
|
-
<TooltipTrigger asChild>
|
|
202
|
-
<span className="shrink-0">
|
|
203
|
-
<ProviderLogo provider={provider} className="size-4" />
|
|
204
|
-
</span>
|
|
205
|
-
</TooltipTrigger>
|
|
206
|
-
<TooltipContent>
|
|
207
|
-
{models.length === 1 ? models[0] : models.join(", ")}
|
|
208
|
-
</TooltipContent>
|
|
209
|
-
</Tooltip>
|
|
210
|
-
))}
|
|
200
|
+
<ClientLogo client={clientApp} className="size-5" />
|
|
201
|
+
<span className="font-mono tabular-nums truncate max-w-[120px]">
|
|
202
|
+
{clientDisplayLabel}
|
|
211
203
|
</span>
|
|
212
|
-
</
|
|
204
|
+
</span>
|
|
213
205
|
)}
|
|
214
206
|
|
|
215
207
|
{/* Time range */}
|
|
@@ -277,6 +277,8 @@ function HeaderRows({
|
|
|
277
277
|
|
|
278
278
|
export type LogEntryProps = {
|
|
279
279
|
log: CapturedLog;
|
|
280
|
+
/** Human-facing ordinal inside the current session. Internal lookups still use `log.id`. */
|
|
281
|
+
displayNumber: number;
|
|
280
282
|
viewMode?: "simple" | "full";
|
|
281
283
|
/** Live "strip Claude Code billing header" flag, sourced once at the viewer
|
|
282
284
|
* container. Hoisted out of `LogEntry` so a single SWR subscription serves
|
|
@@ -361,6 +363,7 @@ const HeadersDiffContent = memo(function ({
|
|
|
361
363
|
|
|
362
364
|
export const LogEntry = memo(function ({
|
|
363
365
|
log,
|
|
366
|
+
displayNumber,
|
|
364
367
|
viewMode = "simple",
|
|
365
368
|
strip,
|
|
366
369
|
slowResponseThresholdSeconds,
|
|
@@ -734,6 +737,7 @@ export const LogEntry = memo(function ({
|
|
|
734
737
|
/>
|
|
735
738
|
<LogEntryHeader
|
|
736
739
|
log={displayLog}
|
|
740
|
+
displayNumber={displayNumber}
|
|
737
741
|
messageCount={requestAnalysis.messageCount}
|
|
738
742
|
toolCount={requestAnalysis.toolCount}
|
|
739
743
|
expanded={expanded}
|
|
@@ -1051,7 +1055,12 @@ export const LogEntry = memo(function ({
|
|
|
1051
1055
|
)}
|
|
1052
1056
|
</div>
|
|
1053
1057
|
<Suspense fallback={null}>
|
|
1054
|
-
<LazyReplayDialog
|
|
1058
|
+
<LazyReplayDialog
|
|
1059
|
+
log={displayLog}
|
|
1060
|
+
displayNumber={displayNumber}
|
|
1061
|
+
open={replayOpen}
|
|
1062
|
+
onOpenChange={setReplayOpen}
|
|
1063
|
+
/>
|
|
1055
1064
|
</Suspense>
|
|
1056
1065
|
</TooltipProvider>
|
|
1057
1066
|
);
|
|
@@ -32,6 +32,7 @@ import { Badge } from "../ui/badge";
|
|
|
32
32
|
import { Button } from "../ui/button";
|
|
33
33
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
34
34
|
import type { CacheTrend } from "./cacheTrend";
|
|
35
|
+
import { ROW_CHEVRON_SLOT_CLASS } from "./ProviderLogoStack";
|
|
35
36
|
|
|
36
37
|
const STATUS_BADGE_CLASSES: Record<StatusCategory, string> = {
|
|
37
38
|
success: "bg-emerald-400/[0.08] text-emerald-200 border-emerald-300/25",
|
|
@@ -155,6 +156,8 @@ export type HeaderTabActions = Record<string, HeaderTabAction | undefined>;
|
|
|
155
156
|
|
|
156
157
|
export type LogEntryHeaderProps = {
|
|
157
158
|
log: CapturedLog;
|
|
159
|
+
/** Human-facing ordinal inside the current session. Internal actions still use `log.id`. */
|
|
160
|
+
displayNumber: number;
|
|
158
161
|
/** Number of messages in the request (supports both Anthropic and OpenAI formats). */
|
|
159
162
|
messageCount?: number | null;
|
|
160
163
|
/** Number of tools defined in the request (supports both Anthropic and OpenAI formats). */
|
|
@@ -184,6 +187,7 @@ export type LogEntryHeaderProps = {
|
|
|
184
187
|
|
|
185
188
|
export const LogEntryHeader = memo(function ({
|
|
186
189
|
log,
|
|
190
|
+
displayNumber,
|
|
187
191
|
messageCount = null,
|
|
188
192
|
toolCount = null,
|
|
189
193
|
expanded,
|
|
@@ -249,6 +253,14 @@ export const LogEntryHeader = memo(function ({
|
|
|
249
253
|
)}
|
|
250
254
|
aria-hidden="true"
|
|
251
255
|
/>
|
|
256
|
+
<span className={ROW_CHEVRON_SLOT_CLASS}>
|
|
257
|
+
{expanded ? (
|
|
258
|
+
<ChevronDown className="size-4 shrink-0" />
|
|
259
|
+
) : (
|
|
260
|
+
<ChevronRight className="size-4 shrink-0" />
|
|
261
|
+
)}
|
|
262
|
+
</span>
|
|
263
|
+
|
|
252
264
|
{/* Request ID */}
|
|
253
265
|
<span
|
|
254
266
|
className={cn(
|
|
@@ -257,10 +269,9 @@ export const LogEntryHeader = memo(function ({
|
|
|
257
269
|
)}
|
|
258
270
|
>
|
|
259
271
|
<span className={cn("size-1.5 rounded-full", statusCue.dot)} aria-hidden="true" />
|
|
260
|
-
<span className="text-
|
|
261
|
-
|
|
272
|
+
<span className="text-xs font-semibold tabular-nums" title={`Log ID ${String(log.id)}`}>
|
|
273
|
+
#{displayNumber}
|
|
262
274
|
</span>
|
|
263
|
-
<span className="text-xs font-semibold tabular-nums">#{log.id}</span>
|
|
264
275
|
</span>
|
|
265
276
|
|
|
266
277
|
{/* Request start time */}
|
|
@@ -271,10 +282,6 @@ export const LogEntryHeader = memo(function ({
|
|
|
271
282
|
</span>
|
|
272
283
|
</span>
|
|
273
284
|
|
|
274
|
-
{/* Model logo moved to the session/conversation header — per-log rows
|
|
275
|
-
no longer render the logo (sessions with many logs would show the
|
|
276
|
-
same provider logo dozens of times). */}
|
|
277
|
-
|
|
278
285
|
{/* Response Status — only shown for non-200 or pending. Each category
|
|
279
286
|
carries a distinct icon in addition to color so the meaning is
|
|
280
287
|
legible without color perception. */}
|
|
@@ -608,13 +615,6 @@ export const LogEntryHeader = memo(function ({
|
|
|
608
615
|
)}
|
|
609
616
|
</span>
|
|
610
617
|
)}
|
|
611
|
-
|
|
612
|
-
{/* Expand chevron */}
|
|
613
|
-
{expanded ? (
|
|
614
|
-
<ChevronDown className="size-4 text-muted-foreground shrink-0" />
|
|
615
|
-
) : (
|
|
616
|
-
<ChevronRight className="size-4 text-muted-foreground shrink-0" />
|
|
617
|
-
)}
|
|
618
618
|
</div>
|
|
619
619
|
</TooltipProvider>
|
|
620
620
|
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
import { ProviderLogo, type Provider } from "../providers/ProviderLogo";
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
|
5
|
+
|
|
6
|
+
const MAX_VISIBLE_LOGOS = 3;
|
|
7
|
+
|
|
8
|
+
export const ROW_CHEVRON_SLOT_CLASS =
|
|
9
|
+
"flex h-8 w-5 shrink-0 items-center justify-center text-muted-foreground";
|
|
10
|
+
|
|
11
|
+
export type ProviderLogoStackItem = {
|
|
12
|
+
provider: Provider;
|
|
13
|
+
tooltip: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function ProviderLogoStack({
|
|
17
|
+
items,
|
|
18
|
+
ariaLabel,
|
|
19
|
+
className,
|
|
20
|
+
}: {
|
|
21
|
+
items: readonly ProviderLogoStackItem[];
|
|
22
|
+
ariaLabel: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
}): JSX.Element {
|
|
25
|
+
if (items.length === 0) {
|
|
26
|
+
return (
|
|
27
|
+
<span className={cn("flex h-8 w-12 shrink-0 items-center", className)} aria-hidden="true" />
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const visibleItems = items.slice(0, MAX_VISIBLE_LOGOS);
|
|
32
|
+
const overflowCount = items.length - visibleItems.length;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<TooltipProvider delayDuration={150}>
|
|
36
|
+
<span className={cn("flex h-8 w-12 shrink-0 items-center", className)} aria-label={ariaLabel}>
|
|
37
|
+
{visibleItems.map((item, index) => (
|
|
38
|
+
<Tooltip key={`${item.provider}-${String(index)}`}>
|
|
39
|
+
<TooltipTrigger asChild>
|
|
40
|
+
<span
|
|
41
|
+
className={cn(
|
|
42
|
+
"inline-flex size-6 shrink-0 items-center justify-center rounded-md border border-white/10 bg-black/20 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]",
|
|
43
|
+
index > 0 && "-ml-2",
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
<ProviderLogo provider={item.provider} className="size-4" />
|
|
47
|
+
</span>
|
|
48
|
+
</TooltipTrigger>
|
|
49
|
+
<TooltipContent>{item.tooltip}</TooltipContent>
|
|
50
|
+
</Tooltip>
|
|
51
|
+
))}
|
|
52
|
+
{overflowCount > 0 && (
|
|
53
|
+
<span className="-ml-2 inline-flex h-6 min-w-6 shrink-0 items-center justify-center rounded-md border border-white/10 bg-black/40 px-1 font-mono text-[10px] text-muted-foreground">
|
|
54
|
+
+{overflowCount}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</span>
|
|
58
|
+
</TooltipProvider>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -35,6 +35,7 @@ type ReplayResult = z.infer<typeof ReplayResultSchema>;
|
|
|
35
35
|
|
|
36
36
|
type ReplayDialogProps = {
|
|
37
37
|
log: CapturedLog;
|
|
38
|
+
displayNumber: number;
|
|
38
39
|
open: boolean;
|
|
39
40
|
onOpenChange: (open: boolean) => void;
|
|
40
41
|
};
|
|
@@ -159,7 +160,12 @@ function ReplayComparisonTable({
|
|
|
159
160
|
);
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
export function ReplayDialog({
|
|
163
|
+
export function ReplayDialog({
|
|
164
|
+
log,
|
|
165
|
+
displayNumber,
|
|
166
|
+
open,
|
|
167
|
+
onOpenChange,
|
|
168
|
+
}: ReplayDialogProps): JSX.Element {
|
|
163
169
|
const { providers } = useProviders();
|
|
164
170
|
const originalBody = log.rawRequestBody ?? "{}";
|
|
165
171
|
const [modifiedBody, setModifiedBody] = useState<string>(() => {
|
|
@@ -310,7 +316,7 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
310
316
|
<DialogHeader>
|
|
311
317
|
<DialogTitle className="flex items-center gap-2">
|
|
312
318
|
<RotateCcw className="size-4" />
|
|
313
|
-
|
|
319
|
+
<span title={`Log ID ${String(log.id)}`}>Replay Request #{displayNumber}</span>
|
|
314
320
|
</DialogTitle>
|
|
315
321
|
</DialogHeader>
|
|
316
322
|
|