@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.
Files changed (50) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-BcEd6V-V.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +106 -0
  4. package/.output/public/assets/ReplayDialog-BNpC0548.js +1 -0
  5. package/.output/public/assets/{RequestAnatomy-BNahe83D.js → RequestAnatomy-Ds1uRLVB.js} +1 -1
  6. package/.output/public/assets/{ResponseView-DSOnGqi6.js → ResponseView-7KPVqKl5.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-BEKTDklB.js → StreamingChunkSequence-BHQT261s.js} +1 -1
  8. package/.output/public/assets/_sessionId-DWePGjnS.js +1 -0
  9. package/.output/public/assets/index-CI1-G8ua.js +1 -0
  10. package/.output/public/assets/index-DdhFqPsI.css +1 -0
  11. package/.output/public/assets/{index-DWOkqdCa.js → index-DjKt8XKe.js} +1 -1
  12. package/.output/public/assets/{json-viewer-C2JpgcW0.js → json-viewer-CkCu-rka.js} +1 -1
  13. package/.output/public/assets/{main-CSONBwwn.js → main-DpD1N0S8.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +194 -200
  15. package/.output/server/{_sessionId-CPAa37n5.mjs → _sessionId-DF9Sy8cP.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-ceW5VxMo.mjs → CompareDrawer-BoxztaO7.mjs} +36 -15
  17. package/.output/server/_ssr/{ProxyViewerContainer-CDfEE_w-.mjs → ProxyViewerContainer-CRBkqFlJ.mjs} +362 -124
  18. package/.output/server/_ssr/{ReplayDialog-V0s_eEbR.mjs → ReplayDialog-Cc1dyDuK.mjs} +14 -7
  19. package/.output/server/_ssr/{RequestAnatomy-f1ccwR9d.mjs → RequestAnatomy-CMGSsz5Z.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-BIRrqG4H.mjs → ResponseView-Cp10DM1D.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-V3JFjCgX.mjs → StreamingChunkSequence-B1VGxy3A.mjs} +2 -2
  22. package/.output/server/_ssr/{index-DsykulzS.mjs → index-47XVPghS.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-Dcnm0Ivf.mjs → json-viewer-zDE2rrmJ.mjs} +3 -3
  25. package/.output/server/_ssr/{router-4bdm6Mt2.mjs → router-DVeuZFqI.mjs} +10 -5
  26. package/.output/server/{_tanstack-start-manifest_v-Xp4CO64V.mjs → _tanstack-start-manifest_v-Bp8JxtPW.mjs} +1 -1
  27. package/.output/server/index.mjs +68 -68
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +28 -2
  30. package/src/components/clients/ClientLogo.tsx +132 -0
  31. package/src/components/groups/GroupsDialog.tsx +18 -13
  32. package/src/components/proxy-viewer/AgentTraceSummary.tsx +56 -23
  33. package/src/components/proxy-viewer/CompareDrawer.tsx +38 -8
  34. package/src/components/proxy-viewer/ConversationHeader.tsx +33 -41
  35. package/src/components/proxy-viewer/LogEntry.tsx +10 -1
  36. package/src/components/proxy-viewer/LogEntryHeader.tsx +14 -14
  37. package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
  38. package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
  39. package/src/components/proxy-viewer/ThreadConnector.tsx +18 -7
  40. package/src/components/proxy-viewer/TurnGroup.tsx +14 -25
  41. package/src/components/proxy-viewer/viewerState.ts +5 -2
  42. package/src/lib/sessionInfoContract.ts +1 -0
  43. package/src/lib/stopReason.ts +48 -16
  44. package/src/proxy/sessionInfo.ts +5 -1
  45. package/.output/public/assets/CompareDrawer-DjgjIFx7.js +0 -1
  46. package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +0 -106
  47. package/.output/public/assets/ReplayDialog-CU0Tbb2c.js +0 -1
  48. package/.output/public/assets/_sessionId-Cif8JZdn.js +0 -1
  49. package/.output/public/assets/index-D_WfwzUi.js +0 -1
  50. 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
- <div
1178
- key={log.id}
1179
- className="grid gap-2 px-3 py-2 text-xs sm:grid-cols-[72px_1fr_88px_88px]"
1180
- >
1181
- <span className="font-mono text-muted-foreground">#{log.id}</span>
1182
- <span className="min-w-0 truncate">
1183
- {displayText(log.model)} · {log.apiFormat} · {log.path}
1184
- </span>
1185
- <span className="font-mono text-muted-foreground">{formatMs(log.firstChunkMs)}</span>
1186
- <span className="font-mono text-muted-foreground">{formatMs(log.latencyMs)}</span>
1187
- </div>
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 #${String(toolLog.id)}`,
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 #${String(failure.id)}`,
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 #${String(slowest.id)}`,
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(logIds: number[]): string {
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
- if (last === undefined || first === last) return `#${String(first)}`;
299
- return `#${String(first)}-#${String(last)}`;
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
- <a
509
- key={logId}
510
- href={`#${getLogAnchor(logId)}`}
511
- onClick={(event) => {
512
- event.preventDefault();
513
- jumpToLog(logId);
514
- }}
515
- 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"
516
- aria-label={`Jump to evidence log ${String(logId)}`}
517
- >
518
- #{logId}
519
- </a>
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({ log, side }: { log: CapturedLog; side: "left" | "right" }): JSX.Element {
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">#{log.id}</span>
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({ left, right, onClose }: CompareDrawerProps): JSX.Element {
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 grouped={grouped} left={left} right={right} />
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 #{left.id})</span>
828
- <span>Right (Log #{right.id})</span>
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 { ProviderLogo, type Provider } from "../providers/ProviderLogo";
21
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
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
- {expanded ? (
117
- <ChevronDown className="size-4 text-muted-foreground shrink-0" />
118
- ) : isLoading ? (
119
- <Loader2 className="size-4 animate-spin text-muted-foreground shrink-0" />
120
- ) : (
121
- <ChevronRight className="size-4 text-muted-foreground shrink-0" />
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
- {userAgent !== null && userAgent !== undefined && userAgent !== "" && (
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={userAgent}
198
+ title={clientTooltip ?? clientDisplayLabel}
183
199
  >
184
- <User className="size-3" />
185
- <span className="font-mono tabular-nums truncate max-w-[120px]">{userAgent}</span>
186
- </span>
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
- </TooltipProvider>
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 log={displayLog} open={replayOpen} onOpenChange={setReplayOpen} />
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-[9px] font-semibold uppercase tracking-normal text-current/65">
261
- Shot
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({ log, open, onOpenChange }: ReplayDialogProps): JSX.Element {
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
- Replay Request #{log.id}
319
+ <span title={`Log ID ${String(log.id)}`}>Replay Request #{displayNumber}</span>
314
320
  </DialogTitle>
315
321
  </DialogHeader>
316
322