@tonyclaw/agent-inspector 2.1.13 → 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 (92) 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-BjlBT-Cy.js → RequestAnatomy-Ds1uRLVB.js} +1 -1
  6. package/.output/public/assets/ResponseView-7KPVqKl5.js +3 -0
  7. package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.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-DjKt8XKe.js +14 -0
  12. package/.output/public/assets/json-viewer-CkCu-rka.js +1 -0
  13. package/.output/public/assets/{main-Dtspb4Ui.js → main-DpD1N0S8.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +194 -200
  15. package/.output/server/_libs/react-markdown.mjs +90 -2
  16. package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-DF9Sy8cP.mjs} +6 -57
  17. package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-BoxztaO7.mjs} +50 -74
  18. package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CRBkqFlJ.mjs} +1221 -799
  19. package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-Cc1dyDuK.mjs} +18 -62
  20. package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-CMGSsz5Z.mjs} +6 -57
  21. package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-Cp10DM1D.mjs} +139 -61
  22. package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-B1VGxy3A.mjs} +13 -58
  23. package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-47XVPghS.mjs} +6 -57
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/json-viewer-zDE2rrmJ.mjs +478 -0
  26. package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-DVeuZFqI.mjs} +680 -174
  27. package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Bp8JxtPW.mjs} +1 -1
  28. package/.output/server/index.mjs +72 -58
  29. package/package.json +1 -1
  30. package/src/components/ProxyViewer.tsx +28 -2
  31. package/src/components/ProxyViewerContainer.tsx +25 -17
  32. package/src/components/clients/ClientLogo.tsx +132 -0
  33. package/src/components/groups/GroupsDialog.tsx +18 -13
  34. package/src/components/providers/ImportWizardDialog.tsx +7 -1
  35. package/src/components/providers/ProviderCard.tsx +10 -1
  36. package/src/components/providers/ProviderForm.tsx +157 -41
  37. package/src/components/providers/ProvidersPanel.tsx +5 -1
  38. package/src/components/proxy-viewer/AgentTraceSummary.tsx +56 -23
  39. package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
  40. package/src/components/proxy-viewer/CompareDrawer.tsx +58 -14
  41. package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
  42. package/src/components/proxy-viewer/ConversationHeader.tsx +33 -41
  43. package/src/components/proxy-viewer/LogEntry.tsx +41 -12
  44. package/src/components/proxy-viewer/LogEntryHeader.tsx +122 -51
  45. package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
  46. package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
  47. package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
  48. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
  49. package/src/components/proxy-viewer/ThreadConnector.tsx +18 -7
  50. package/src/components/proxy-viewer/TurnGroup.tsx +67 -32
  51. package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
  52. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
  53. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
  54. package/src/components/proxy-viewer/formats/index.tsx +10 -3
  55. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
  56. package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
  57. package/src/components/proxy-viewer/logFocus.ts +15 -2
  58. package/src/components/proxy-viewer/requestTools.ts +7 -3
  59. package/src/components/proxy-viewer/viewerState.ts +77 -5
  60. package/src/components/ui/json-viewer-bulk.ts +41 -6
  61. package/src/components/ui/json-viewer.tsx +9 -8
  62. package/src/contracts/index.ts +15 -1
  63. package/src/contracts/openai.ts +94 -0
  64. package/src/lib/providerContract.ts +1 -0
  65. package/src/lib/providerModelMetadata.ts +7 -1
  66. package/src/lib/sessionInfoContract.ts +1 -0
  67. package/src/lib/stopReason.ts +59 -15
  68. package/src/lib/upstreamUrl.ts +46 -0
  69. package/src/mcp/previewExtractor.ts +73 -3
  70. package/src/mcp/server.ts +2 -0
  71. package/src/mcp/toolHandlers.ts +2 -0
  72. package/src/proxy/constants.ts +2 -0
  73. package/src/proxy/formats/openai/handler.ts +40 -9
  74. package/src/proxy/formats/openai/index.ts +7 -0
  75. package/src/proxy/formats/openai/schemas.ts +15 -1
  76. package/src/proxy/formats/openai/stream.ts +529 -135
  77. package/src/proxy/formats/registry.ts +9 -1
  78. package/src/proxy/providerImporters.ts +45 -5
  79. package/src/proxy/providers.ts +31 -11
  80. package/src/proxy/schemas.ts +6 -0
  81. package/src/proxy/sessionInfo.ts +5 -1
  82. package/src/proxy/toolSchemaWarnings.ts +25 -3
  83. package/src/proxy/upstream.ts +18 -37
  84. package/src/routes/api/providers.$providerId.ts +1 -0
  85. package/src/routes/api/providers.ts +2 -0
  86. package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
  87. package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
  88. package/.output/public/assets/ReplayDialog-sBA1KAYD.js +0 -1
  89. package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
  90. package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
  91. package/.output/public/assets/index-B-QQLbpz.js +0 -1
  92. package/.output/public/assets/index-CmtfjQPv.css +0 -1
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useRef, useState } from "react";
1
+ import { Suspense, useEffect, useMemo, useRef, useState } from "react";
2
2
  import type { JSX } from "react";
3
3
  import {
4
4
  Check,
@@ -23,15 +23,19 @@ import {
23
23
  previewNode,
24
24
  } from "./requestDiff";
25
25
  import { getConversationId } from "./ConversationHeader";
26
- import { JsonViewerFromString } from "../ui/json-viewer";
27
26
  import { Badge } from "../ui/badge";
28
27
  import { getLogFormatAdapter, resolveLogFormat } from "./log-formats";
28
+ import { LazyJsonViewerFromString } from "./lazy";
29
29
 
30
30
  export type CompareDrawerProps = {
31
31
  /** Log selected first (shown on the left). */
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
 
@@ -65,6 +69,20 @@ function nodeToJsonValue(node: JsonNode): unknown {
65
69
  }
66
70
  }
67
71
 
72
+ function LazyJsonStringView({
73
+ text,
74
+ defaultExpandDepth,
75
+ }: {
76
+ text: string;
77
+ defaultExpandDepth: number;
78
+ }): JSX.Element {
79
+ return (
80
+ <Suspense fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}>
81
+ <LazyJsonViewerFromString text={text} defaultExpandDepth={defaultExpandDepth} />
82
+ </Suspense>
83
+ );
84
+ }
85
+
68
86
  /** The parent path of a JSON path string. E.g. `messages[3].content` →
69
87
  * `messages[3]`, `messages[3]` → `messages`, `messages` → `""`. */
70
88
  function parentPath(path: string): string {
@@ -222,7 +240,7 @@ function EqualRunRow({
222
240
  {ops.map((op) => (
223
241
  <div key={op.path} className="border border-border/50 rounded p-2 bg-muted/20">
224
242
  <div className="font-mono text-xs text-muted-foreground mb-1">{op.path}</div>
225
- <JsonViewerFromString text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
243
+ <LazyJsonStringView text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
226
244
  </div>
227
245
  ))}
228
246
  </div>
@@ -393,7 +411,7 @@ function ExpandedSubtree({ op }: { op: AddedOp | RemovedOp | ChangedOp }): JSX.E
393
411
  if (op.kind === "added" || op.kind === "removed") {
394
412
  return (
395
413
  <div className="pl-5 mt-2 border border-border/50 rounded p-2 bg-muted/20">
396
- <JsonViewerFromString text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
414
+ <LazyJsonStringView text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
397
415
  </div>
398
416
  );
399
417
  }
@@ -411,11 +429,11 @@ function ExpandedSubtree({ op }: { op: AddedOp | RemovedOp | ChangedOp }): JSX.E
411
429
  <div className="pl-5 mt-2 grid grid-cols-1 md:grid-cols-2 gap-2">
412
430
  <div className="border border-rose-500/30 rounded p-2 bg-rose-500/5">
413
431
  <div className="text-[10px] uppercase tracking-wider text-rose-500 mb-1">Old</div>
414
- <JsonViewerFromString text={nodeToJsonString(op.left)} defaultExpandDepth={0} />
432
+ <LazyJsonStringView text={nodeToJsonString(op.left)} defaultExpandDepth={0} />
415
433
  </div>
416
434
  <div className="border border-emerald-500/30 rounded p-2 bg-emerald-500/5">
417
435
  <div className="text-[10px] uppercase tracking-wider text-emerald-500 mb-1">New</div>
418
- <JsonViewerFromString text={nodeToJsonString(op.right)} defaultExpandDepth={0} />
436
+ <LazyJsonStringView text={nodeToJsonString(op.right)} defaultExpandDepth={0} />
419
437
  </div>
420
438
  </div>
421
439
  );
@@ -527,7 +545,15 @@ function ModeToggle({
527
545
  );
528
546
  }
529
547
 
530
- 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 {
531
557
  const conversationId = getConversationId(log);
532
558
  return (
533
559
  <div className="flex-1 min-w-0 space-y-1 text-xs">
@@ -543,7 +569,9 @@ function SideSummary({ log, side }: { log: CapturedLog; side: "left" | "right" }
543
569
  >
544
570
  {side === "left" ? "← Left" : "Right →"}
545
571
  </Badge>
546
- <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>
547
575
  {log.model !== null && (
548
576
  <span className="font-mono text-muted-foreground truncate">{log.model}</span>
549
577
  )}
@@ -570,7 +598,13 @@ function SideSummary({ log, side }: { log: CapturedLog; side: "left" | "right" }
570
598
  );
571
599
  }
572
600
 
573
- 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 {
574
608
  // Memoize the diff so re-renders (e.g. parent re-renders) don't recompute.
575
609
  const ops = useMemo<DiffOp[]>(() => {
576
610
  const leftRequest = getLogFormatAdapter(resolveLogFormat(left)).analyzeRequest(
@@ -727,8 +761,8 @@ export function CompareDrawer({ left, right, onClose }: CompareDrawerProps): JSX
727
761
  {/* Header */}
728
762
  <div className="flex items-start gap-4 px-4 py-3 border-b border-border">
729
763
  <div className="flex-1 flex gap-4 min-w-0">
730
- <SideSummary log={left} side="left" />
731
- <SideSummary log={right} side="right" />
764
+ <SideSummary log={left} side="left" displayNumber={leftDisplayNumber} />
765
+ <SideSummary log={right} side="right" displayNumber={rightDisplayNumber} />
732
766
  </div>
733
767
  <div className="flex items-center gap-2 shrink-0">
734
768
  <ModeToggle mode={mode} onChange={setMode} />
@@ -785,7 +819,13 @@ export function CompareDrawer({ left, right, onClose }: CompareDrawerProps): JSX
785
819
  })}
786
820
  </div>
787
821
  ) : (
788
- <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
+ />
789
829
  )}
790
830
  </div>
791
831
  </>
@@ -800,18 +840,22 @@ function SplitBody({
800
840
  grouped,
801
841
  left,
802
842
  right,
843
+ leftDisplayNumber,
844
+ rightDisplayNumber,
803
845
  }: {
804
846
  grouped: GroupedOp[];
805
847
  left: CapturedLog;
806
848
  right: CapturedLog;
849
+ leftDisplayNumber: number;
850
+ rightDisplayNumber: number;
807
851
  }): JSX.Element {
808
852
  return (
809
853
  <div className="grid grid-cols-[200px_1fr_1fr] gap-x-2 gap-y-0.5 px-3 py-2 text-xs">
810
854
  {/* Column headers */}
811
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">
812
856
  <span>Path</span>
813
- <span>Left (Log #{left.id})</span>
814
- <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>
815
859
  </div>
816
860
 
817
861
  {grouped.map((g, i) => {
@@ -4,7 +4,7 @@ import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
4
4
  import type { CapturedLog } from "../../contracts";
5
5
  import { ConversationHeader, type ConversationGroupData } from "./ConversationHeader";
6
6
  import { detectProvider } from "../providers/ProviderLogo";
7
- import { TurnGroup } from "./TurnGroup";
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";
@@ -180,20 +180,16 @@ export const ConversationGroup = memo(function ({
180
180
  showRollupMetrics={showTraceRollupMetrics}
181
181
  timeDisplayFormat={timeDisplayFormat}
182
182
  />
183
- {turnGroups.map((tg) => (
184
- <TurnGroup
185
- key={tg.turnIndex}
186
- entries={tg.entries}
187
- viewMode={viewMode}
188
- strip={strip}
189
- slowResponseThresholdSeconds={slowResponseThresholdSeconds}
190
- timeDisplayFormat={timeDisplayFormat}
191
- cacheTrends={cacheTrends}
192
- onCompareWithPrevious={onCompareWithPrevious}
193
- comparisonPredecessors={comparisonPredecessors}
194
- turnIndex={tg.turnIndex}
195
- />
196
- ))}
183
+ <TurnGroupList
184
+ turnGroups={turnGroups}
185
+ viewMode={viewMode}
186
+ strip={strip}
187
+ slowResponseThresholdSeconds={slowResponseThresholdSeconds}
188
+ timeDisplayFormat={timeDisplayFormat}
189
+ cacheTrends={cacheTrends}
190
+ onCompareWithPrevious={onCompareWithPrevious}
191
+ comparisonPredecessors={comparisonPredecessors}
192
+ />
197
193
  </div>
198
194
  )}
199
195
  </div>
@@ -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 */}
@@ -75,7 +75,7 @@ const BODY_CHUNK_BYTES = 256 * 1024;
75
75
  const CHUNKED_BODY_THRESHOLD_BYTES = 1024 * 1024;
76
76
 
77
77
  const TAB_TRIGGER_CLASS =
78
- "h-9 flex-none rounded-none border-0 border-b-2 border-transparent bg-transparent px-3 text-xs font-semibold text-muted-foreground shadow-none hover:bg-cyan-400/[0.04] hover:text-foreground data-[state=active]:border-cyan-300 data-[state=active]:bg-transparent data-[state=active]:text-cyan-100 data-[state=active]:shadow-none";
78
+ "h-9 flex-none rounded-none border-0 border-b-2 border-transparent bg-transparent px-3 text-xs font-semibold text-muted-foreground shadow-none transition-colors hover:bg-cyan-400/[0.05] hover:text-foreground data-[state=active]:border-cyan-300 data-[state=active]:bg-cyan-300/[0.04] data-[state=active]:text-cyan-100 data-[state=active]:shadow-none";
79
79
 
80
80
  function createEmptyBodyPreviewState(totalBytes: number | null = null): BodyPreviewState {
81
81
  return {
@@ -140,7 +140,7 @@ function ExpandedPanel({
140
140
  return (
141
141
  <div
142
142
  className={cn(
143
- "inspector-scrollbar mx-3 mb-3 max-h-[72vh] overflow-auto rounded-md border border-white/10 bg-[#06080b] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
143
+ "inspector-scrollbar mx-3 mb-3 max-h-[72vh] overflow-auto rounded-md border border-cyan-300/10 bg-[#05070a] p-3 shadow-[0_16px_45px_rgba(0,0,0,0.24),inset_0_1px_0_rgba(255,255,255,0.05)]",
144
144
  className,
145
145
  )}
146
146
  >
@@ -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,
@@ -384,6 +387,7 @@ export const LogEntry = memo(function ({
384
387
  );
385
388
  const requestJsonRef = useRef<HTMLDivElement | null>(null);
386
389
  const logIdRef = useRef(log.id);
390
+ const bodyHydrationInFlightRef = useRef(false);
387
391
  const displayLog = hydratedLog ?? log;
388
392
  const useChunkedBody = hydratedLog === null && shouldUseChunkedBodyLoading(log);
389
393
  const requestPreviewBody = bodyPreviewTextForView(useChunkedBody, requestPreview);
@@ -421,10 +425,10 @@ export const LogEntry = memo(function ({
421
425
  requestPreviewShouldParse ? displayedRequestBody : null,
422
426
  );
423
427
  const rawRequestExpansion = useJsonBulkExpansion(
424
- requestPreviewShouldParse ? rawRequestBodyForView : null,
428
+ activeTab === "raw-request" && requestPreviewShouldParse ? rawRequestBodyForView : null,
425
429
  );
426
430
  const responseExpansion = useJsonBulkExpansion(
427
- responsePreviewShouldParse ? responseTextForView : null,
431
+ activeTab === "raw" && responsePreviewShouldParse ? responseTextForView : null,
428
432
  );
429
433
 
430
434
  // Headers are rendered as a flat list, so we copy them as pretty-printed JSON.
@@ -634,6 +638,7 @@ export const LogEntry = memo(function ({
634
638
 
635
639
  useEffect(() => {
636
640
  logIdRef.current = log.id;
641
+ bodyHydrationInFlightRef.current = false;
637
642
  setHydratedLog(null);
638
643
  setBodyHydrationState("idle");
639
644
  setRequestPreview(createEmptyBodyPreviewState(log.rawRequestBodyBytes ?? null));
@@ -643,9 +648,10 @@ export const LogEntry = memo(function ({
643
648
  useEffect(() => {
644
649
  if (!expanded) return;
645
650
  if (!shouldHydrateLogBody(log)) return;
646
- if (hydratedLog !== null || bodyHydrationState !== "idle") return;
651
+ if (hydratedLog !== null || bodyHydrationInFlightRef.current) return;
647
652
 
648
653
  let cancelled = false;
654
+ bodyHydrationInFlightRef.current = true;
649
655
  setBodyHydrationState("loading");
650
656
  void fetchJsonWithTimeout(
651
657
  `/api/logs/${String(log.id)}`,
@@ -654,19 +660,21 @@ export const LogEntry = memo(function ({
654
660
  )
655
661
  .then((fullLog) => {
656
662
  if (cancelled) return;
663
+ bodyHydrationInFlightRef.current = false;
657
664
  setHydratedLog(fullLog);
658
665
  setBodyHydrationState("idle");
659
666
  })
660
667
  .catch(() => {
661
668
  if (cancelled) return;
669
+ bodyHydrationInFlightRef.current = false;
662
670
  setBodyHydrationState("failed");
663
671
  });
664
672
 
665
673
  return () => {
666
674
  cancelled = true;
675
+ bodyHydrationInFlightRef.current = false;
667
676
  };
668
677
  }, [
669
- bodyHydrationState,
670
678
  expanded,
671
679
  hydratedLog,
672
680
  log.bodyContentMode,
@@ -710,14 +718,26 @@ export const LogEntry = memo(function ({
710
718
  <TooltipProvider>
711
719
  <div
712
720
  className={cn(
713
- "mb-2 overflow-hidden rounded-md border bg-[#090a0d] transition-[background-color,border-color,box-shadow]",
721
+ "relative mb-2 overflow-hidden rounded-lg border bg-[#07090d] transition-[background-color,border-color,box-shadow]",
714
722
  expanded
715
- ? "border-cyan-300/20 shadow-[0_18px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.05)]"
716
- : "border-border/70 hover:border-border",
723
+ ? "border-cyan-300/25 shadow-[0_24px_80px_rgba(0,0,0,0.34),0_0_0_1px_rgba(103,232,249,0.04),inset_0_1px_0_rgba(255,255,255,0.06)]"
724
+ : "border-white/10 shadow-[0_6px_24px_rgba(0,0,0,0.16)] hover:border-cyan-300/15",
717
725
  )}
718
726
  >
727
+ <div
728
+ className={cn(
729
+ "pointer-events-none absolute inset-y-3 left-1 w-px rounded-full",
730
+ expanded ? "bg-cyan-300/45 shadow-[0_0_20px_rgba(103,232,249,0.28)]" : "bg-white/10",
731
+ )}
732
+ aria-hidden="true"
733
+ />
734
+ <div
735
+ className="pointer-events-none absolute inset-x-3 top-0 h-px bg-gradient-to-r from-transparent via-white/15 to-transparent"
736
+ aria-hidden="true"
737
+ />
719
738
  <LogEntryHeader
720
739
  log={displayLog}
740
+ displayNumber={displayNumber}
721
741
  messageCount={requestAnalysis.messageCount}
722
742
  toolCount={requestAnalysis.toolCount}
723
743
  expanded={expanded}
@@ -738,14 +758,18 @@ export const LogEntry = memo(function ({
738
758
 
739
759
  {expanded && (
740
760
  <div
741
- className="border-t border-white/10 bg-[#050607]/80 pt-2"
761
+ className="relative border-t border-cyan-300/10 bg-[linear-gradient(180deg,#06090d_0%,#040506_100%)] pt-2"
742
762
  onClick={(e) => e.stopPropagation()}
743
763
  onKeyDown={(e) => e.stopPropagation()}
744
764
  >
765
+ <div
766
+ className="pointer-events-none absolute inset-x-0 top-0 h-12 bg-gradient-to-b from-cyan-300/[0.035] to-transparent"
767
+ aria-hidden="true"
768
+ />
745
769
  <Tabs value={activeTab} onValueChange={setActiveTab} className="gap-2">
746
770
  <TabsList
747
771
  variant="line"
748
- className="inspector-scrollbar mx-3 h-auto w-[calc(100%-1.5rem)] justify-start overflow-x-auto rounded-none border-b border-white/10 bg-transparent p-0"
772
+ className="inspector-scrollbar relative mx-3 h-auto w-[calc(100%-1.5rem)] justify-start overflow-x-auto rounded-none border-b border-cyan-300/10 bg-black/10 p-0"
749
773
  >
750
774
  {viewMode === "full" && (
751
775
  <TabsTrigger className={TAB_TRIGGER_CLASS} value="raw-headers">
@@ -1031,7 +1055,12 @@ export const LogEntry = memo(function ({
1031
1055
  )}
1032
1056
  </div>
1033
1057
  <Suspense fallback={null}>
1034
- <LazyReplayDialog log={displayLog} open={replayOpen} onOpenChange={setReplayOpen} />
1058
+ <LazyReplayDialog
1059
+ log={displayLog}
1060
+ displayNumber={displayNumber}
1061
+ open={replayOpen}
1062
+ onOpenChange={setReplayOpen}
1063
+ />
1035
1064
  </Suspense>
1036
1065
  </TooltipProvider>
1037
1066
  );