@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.
Files changed (70) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-Ccrrcx1j.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +106 -0
  4. package/.output/public/assets/ReplayDialog-C7axhr-l.js +1 -0
  5. package/.output/public/assets/{RequestAnatomy-BNahe83D.js → RequestAnatomy-WOptg8j_.js} +1 -1
  6. package/.output/public/assets/{ResponseView-DSOnGqi6.js → ResponseView-CSAcxh_M.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-BEKTDklB.js → StreamingChunkSequence-DJS5KhPx.js} +1 -1
  8. package/.output/public/assets/_sessionId-DUSalzKH.js +1 -0
  9. package/.output/public/assets/index-BPpA21dY.css +1 -0
  10. package/.output/public/assets/index-Bt0Az2I2.js +1 -0
  11. package/.output/public/assets/{index-DWOkqdCa.js → index-DMPNh46t.js} +1 -1
  12. package/.output/public/assets/{json-viewer-C2JpgcW0.js → json-viewer-inX5QSa3.js} +1 -1
  13. package/.output/public/assets/{main-CSONBwwn.js → main-aCAKYGSD.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +224 -194
  15. package/.output/server/{_sessionId-CPAa37n5.mjs → _sessionId-C0fhvQnn.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-ceW5VxMo.mjs → CompareDrawer-CO-Ti5sg.mjs} +36 -15
  17. package/.output/server/_ssr/{ProxyViewerContainer-CDfEE_w-.mjs → ProxyViewerContainer-N4J7uBCX.mjs} +1153 -494
  18. package/.output/server/_ssr/{ReplayDialog-V0s_eEbR.mjs → ReplayDialog-J1VxC0In.mjs} +14 -7
  19. package/.output/server/_ssr/{RequestAnatomy-f1ccwR9d.mjs → RequestAnatomy-DBH-F3rq.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-BIRrqG4H.mjs → ResponseView-DLFwV7Hd.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-V3JFjCgX.mjs → StreamingChunkSequence-BywmuND0.mjs} +3 -3
  22. package/.output/server/_ssr/{index-DsykulzS.mjs → index-CStymIc0.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-Dcnm0Ivf.mjs → json-viewer-DlQu0N1x.mjs} +3 -3
  25. package/.output/server/_ssr/{router-4bdm6Mt2.mjs → router-CQ4Zxhmr.mjs} +118 -47
  26. package/.output/server/{_tanstack-start-manifest_v-Xp4CO64V.mjs → _tanstack-start-manifest_v-BHeJBGfo.mjs} +1 -1
  27. package/.output/server/index.mjs +75 -75
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +174 -107
  30. package/src/components/ProxyViewerContainer.tsx +77 -67
  31. package/src/components/alerts/AlertsDialog.tsx +2 -1
  32. package/src/components/clients/ClientLogo.tsx +132 -0
  33. package/src/components/groups/GroupsDialog.tsx +37 -14
  34. package/src/components/providers/ProviderCard.tsx +1 -1
  35. package/src/components/providers/ProviderForm.tsx +18 -18
  36. package/src/components/providers/ProviderLogo.tsx +6 -1
  37. package/src/components/providers/ProvidersPanel.tsx +6 -6
  38. package/src/components/providers/SettingsDialog.tsx +13 -6
  39. package/src/components/proxy-viewer/AgentTraceSummary.tsx +62 -29
  40. package/src/components/proxy-viewer/CompareDrawer.tsx +38 -8
  41. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  42. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  43. package/src/components/proxy-viewer/ConversationHeader.tsx +65 -61
  44. package/src/components/proxy-viewer/LogEntry.tsx +31 -18
  45. package/src/components/proxy-viewer/LogEntryHeader.tsx +42 -49
  46. package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
  47. package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
  48. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  49. package/src/components/proxy-viewer/ThreadConnector.tsx +22 -11
  50. package/src/components/proxy-viewer/TurnGroup.tsx +30 -36
  51. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  52. package/src/components/proxy-viewer/viewerState.ts +5 -6
  53. package/src/components/ui/icon-trigger.ts +2 -0
  54. package/src/components/ui/transient-toast.tsx +104 -0
  55. package/src/lib/sessionInfoContract.ts +5 -0
  56. package/src/lib/stopReason.ts +48 -16
  57. package/src/mcp/server.ts +71 -27
  58. package/src/mcp/toolHandlers.ts +23 -4
  59. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  60. package/src/proxy/sessionInfo.ts +36 -3
  61. package/src/proxy/sessionSupervisor.ts +20 -1
  62. package/src/proxy/store.ts +1 -1
  63. package/src/routes/api/providers.$providerId.test.log.ts +8 -4
  64. package/styles/globals.css +180 -180
  65. package/.output/public/assets/CompareDrawer-DjgjIFx7.js +0 -1
  66. package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +0 -106
  67. package/.output/public/assets/ReplayDialog-CU0Tbb2c.js +0 -1
  68. package/.output/public/assets/_sessionId-Cif8JZdn.js +0 -1
  69. package/.output/public/assets/index-D_WfwzUi.js +0 -1
  70. 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="flex items-center justify-between sticky top-0 bg-background z-10 pb-2">
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="flex items-center justify-between sticky top-0 z-10 bg-background pb-2">
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="flex items-center gap-2 text-xs text-muted-foreground bg-muted/30 rounded-md px-3 py-2">
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="flex items-center gap-2 text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
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="text-center py-12 space-y-3">
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 border-b border-border">
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 variant="ghost" size="icon" className="size-8">
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="rounded-md border border-border bg-muted/20 px-3 py-2">
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="rounded-md border border-border bg-muted/20 px-3 py-2">
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="rounded-md border border-border bg-muted/20 px-3 py-2">
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="rounded-md border border-border bg-muted/20 px-3 py-2">
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="min-w-0 rounded-md border border-border bg-muted/20 px-3 py-2">
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 #${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);
@@ -368,7 +392,7 @@ function CandidateItem({
368
392
 
369
393
  if (editing) {
370
394
  return (
371
- <div className="rounded-md border border-border/80 bg-background/60 px-2.5 py-2">
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="h-7 rounded-md border border-input bg-background px-2 text-xs"
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 border border-input bg-background px-2 text-xs"
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 border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed"
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="h-7 rounded-md border border-input bg-background px-2 text-xs"
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
- <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(
@@ -762,7 +794,7 @@ export function AgentTraceSummary({
762
794
  if (logs.length === 0) return null;
763
795
 
764
796
  return (
765
- <section className="mb-2 rounded-lg border border-border bg-muted/10 px-3 py-2">
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({ 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) => {
@@ -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, shouldRenderConversationContent } from "./viewerState";
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
- /** When true, skip the group header and render content directly. */
29
- standalone?: boolean;
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
- standalone = false,
108
- hasPinnedSessionContext = false,
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(false);
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, standalone]);
160
+ }, [group.logs, setExpanded]);
151
161
 
152
162
  return (
153
163
  <div className="mb-2">
154
- {!standalone && (
155
- <ConversationHeader
156
- conversationId={group.conversationId}
157
- startTime={startTime}
158
- endTime={endTime}
159
- totalCalls={group.logs.length}
160
- totalInputTokens={stats.totalInputTokens}
161
- totalOutputTokens={stats.totalOutputTokens}
162
- expanded={expanded}
163
- onToggle={() => setExpanded(!expanded)}
164
- providers={providers}
165
- isLoading={isLoading}
166
- userAgent={group.logs[0]?.userAgent ?? null}
167
- clientPid={clientPid}
168
- clientProjectFolder={clientProjectFolder}
169
- timeDisplayFormat={timeDisplayFormat}
170
- onClear={() => onClearGroup(group.logs.map((l) => l.id))}
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
- {shouldRenderConversationContent(standalone, expanded) && (
184
+ {expanded && (
175
185
  <div>
176
186
  <AgentTraceSummary
177
187
  logs={group.logs}
178
188
  scopeId={group.conversationId}
179
189
  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
180
- showRollupMetrics={showTraceRollupMetrics}
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
  )}