@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
@@ -32,17 +32,51 @@ 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
- success: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25",
38
- client_error: "bg-amber-500/15 text-amber-400 border-amber-500/25",
39
- server_error: "bg-rose-500/15 text-rose-400 border-rose-500/25",
40
- pending: "bg-muted text-muted-foreground border-border",
38
+ success: "bg-emerald-400/[0.08] text-emerald-200 border-emerald-300/25",
39
+ client_error: "bg-amber-300/[0.10] text-amber-100 border-amber-300/30",
40
+ server_error: "bg-rose-400/[0.10] text-rose-100 border-rose-300/30",
41
+ pending: "bg-cyan-300/[0.08] text-cyan-100 border-cyan-300/25",
42
+ };
43
+
44
+ const STATUS_CUE_CLASSES: Record<
45
+ StatusCategory,
46
+ {
47
+ frame: string;
48
+ dot: string;
49
+ rail: string;
50
+ }
51
+ > = {
52
+ success: {
53
+ frame: "border-emerald-300/20 bg-emerald-300/[0.06] text-emerald-100",
54
+ dot: "bg-emerald-300 shadow-[0_0_14px_rgba(110,231,183,0.8)]",
55
+ rail: "bg-emerald-300/45 shadow-[0_0_22px_rgba(110,231,183,0.28)]",
56
+ },
57
+ client_error: {
58
+ frame: "border-amber-300/25 bg-amber-300/[0.08] text-amber-100",
59
+ dot: "bg-amber-200 shadow-[0_0_14px_rgba(252,211,77,0.8)]",
60
+ rail: "bg-amber-300/50 shadow-[0_0_22px_rgba(252,211,77,0.28)]",
61
+ },
62
+ server_error: {
63
+ frame: "border-rose-300/25 bg-rose-300/[0.08] text-rose-100",
64
+ dot: "bg-rose-300 shadow-[0_0_14px_rgba(253,164,175,0.8)]",
65
+ rail: "bg-rose-300/50 shadow-[0_0_22px_rgba(253,164,175,0.28)]",
66
+ },
67
+ pending: {
68
+ frame: "border-cyan-300/25 bg-cyan-300/[0.08] text-cyan-100",
69
+ dot: "bg-cyan-200 shadow-[0_0_14px_rgba(103,232,249,0.8)]",
70
+ rail: "bg-cyan-300/50 shadow-[0_0_22px_rgba(103,232,249,0.28)]",
71
+ },
41
72
  };
42
73
 
43
74
  const HEADER_ACTION_BUTTON_CLASS =
44
75
  "size-8 border-white/10 bg-[#0a0d11] text-muted-foreground shadow-none hover:border-cyan-300/30 hover:bg-cyan-400/10 hover:text-cyan-100";
45
76
 
77
+ const TELEMETRY_CHIP_CLASS =
78
+ "flex h-7 items-center gap-1 rounded-md border border-white/10 bg-black/20 px-2 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]";
79
+
46
80
  function formatElapsed(ms: number): string {
47
81
  if (ms < 1000) return `${ms}ms`;
48
82
  return `${(ms / 1000).toFixed(1)}s`;
@@ -53,6 +87,19 @@ function formatTokenRate(tokensPerSecond: number): string {
53
87
  return `${tokensPerSecond.toFixed(1)}/s`;
54
88
  }
55
89
 
90
+ function statusCueLabel(category: StatusCategory, status: number | null): string {
91
+ switch (category) {
92
+ case "success":
93
+ return status === null ? "OK" : String(status);
94
+ case "client_error":
95
+ return status === null ? "WARN" : String(status);
96
+ case "server_error":
97
+ return status === null ? "FAIL" : String(status);
98
+ case "pending":
99
+ return "ROLL";
100
+ }
101
+ }
102
+
56
103
  /**
57
104
  * Inline trend indicator: small arrow (green up / red down) plus the absolute
58
105
  * delta in compact form. Returns `null` when there is no trend to display.
@@ -109,6 +156,8 @@ export type HeaderTabActions = Record<string, HeaderTabAction | undefined>;
109
156
 
110
157
  export type LogEntryHeaderProps = {
111
158
  log: CapturedLog;
159
+ /** Human-facing ordinal inside the current session. Internal actions still use `log.id`. */
160
+ displayNumber: number;
112
161
  /** Number of messages in the request (supports both Anthropic and OpenAI formats). */
113
162
  messageCount?: number | null;
114
163
  /** Number of tools defined in the request (supports both Anthropic and OpenAI formats). */
@@ -138,6 +187,7 @@ export type LogEntryHeaderProps = {
138
187
 
139
188
  export const LogEntryHeader = memo(function ({
140
189
  log,
190
+ displayNumber,
141
191
  messageCount = null,
142
192
  toolCount = null,
143
193
  expanded,
@@ -169,6 +219,8 @@ export const LogEntryHeader = memo(function ({
169
219
  log.clientProjectFolder !== "";
170
220
  const hasClientCwd =
171
221
  log.clientCwd !== null && log.clientCwd !== undefined && log.clientCwd !== "";
222
+ const statusCue = STATUS_CUE_CLASSES[statusCategory];
223
+ const statusLabel = statusCueLabel(statusCategory, log.responseStatus);
172
224
 
173
225
  return (
174
226
  <TooltipProvider>
@@ -179,9 +231,11 @@ export const LogEntryHeader = memo(function ({
179
231
  data-nav-id={`log-${log.id}`}
180
232
  data-nav-action={expanded ? "collapse" : "expand"}
181
233
  className={cn(
182
- "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-3 py-1.5 cursor-pointer transition-colors",
183
- expanded ? "bg-[#0c1015]" : "hover:bg-muted/30",
184
- "select-none",
234
+ "group/header relative isolate flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 overflow-hidden px-3 py-2 cursor-pointer transition-[background-color,border-color,box-shadow]",
235
+ expanded
236
+ ? "bg-[linear-gradient(90deg,#0f1720_0%,#090d12_50%,#07090d_100%)] shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]"
237
+ : "bg-[#080b10] hover:bg-[#0b1016]",
238
+ "select-none before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-white/10",
185
239
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
186
240
  )}
187
241
  onClick={onToggle}
@@ -192,51 +246,70 @@ export const LogEntryHeader = memo(function ({
192
246
  }
193
247
  }}
194
248
  >
249
+ <span
250
+ className={cn(
251
+ "absolute left-0 top-2 bottom-2 w-0.5 rounded-r-full transition-colors",
252
+ statusCue.rail,
253
+ )}
254
+ aria-hidden="true"
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
+
195
264
  {/* Request ID */}
196
- <span className="text-blue-400/80 font-mono text-xs font-semibold tabular-nums shrink-0">
197
- #{log.id}
265
+ <span
266
+ className={cn(
267
+ "inline-flex h-8 shrink-0 items-center gap-2 rounded-md border px-2 font-mono shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]",
268
+ statusCue.frame,
269
+ )}
270
+ >
271
+ <span className={cn("size-1.5 rounded-full", statusCue.dot)} aria-hidden="true" />
272
+ <span className="text-xs font-semibold tabular-nums" title={`Log ID ${String(log.id)}`}>
273
+ #{displayNumber}
274
+ </span>
198
275
  </span>
199
276
 
200
277
  {/* Request start time */}
201
- <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
278
+ <span className="flex h-7 items-center gap-1 rounded-md border border-white/10 bg-black/20 px-2 text-muted-foreground text-xs shrink-0">
202
279
  <Clock className="size-3" />
203
280
  <span className="font-mono tabular-nums" title={log.timestamp}>
204
281
  {formatTimestamp(log.timestamp, timeDisplayFormat)}
205
282
  </span>
206
283
  </span>
207
284
 
208
- {/* Model logo moved to the session/conversation header — per-log rows
209
- no longer render the logo (sessions with many logs would show the
210
- same provider logo dozens of times). */}
211
-
212
285
  {/* Response Status — only shown for non-200 or pending. Each category
213
286
  carries a distinct icon in addition to color so the meaning is
214
287
  legible without color perception. */}
215
- {statusCategory !== "success" && (
216
- <Badge
217
- variant="outline"
218
- className={cn(
219
- "text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums gap-1",
220
- STATUS_BADGE_CLASSES[statusCategory],
221
- )}
222
- >
223
- {statusCategory === "server_error" ? (
224
- <OctagonAlert className="size-3" aria-label="Server error" />
225
- ) : statusCategory === "client_error" ? (
226
- <AlertTriangle className="size-3" aria-label="Client error" />
227
- ) : statusCategory === "pending" ? (
228
- <Loader2 className="size-3 animate-spin" aria-label="Pending" />
229
- ) : null}
230
- {log.responseStatus}
231
- </Badge>
232
- )}
288
+ <Badge
289
+ variant="outline"
290
+ className={cn(
291
+ "h-6 gap-1 rounded-md px-2 py-0 font-mono text-[10px] font-semibold uppercase tabular-nums shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]",
292
+ STATUS_BADGE_CLASSES[statusCategory],
293
+ )}
294
+ >
295
+ {statusCategory === "success" ? (
296
+ <Check className="size-3" aria-label="Success" />
297
+ ) : statusCategory === "server_error" ? (
298
+ <OctagonAlert className="size-3" aria-label="Server error" />
299
+ ) : statusCategory === "client_error" ? (
300
+ <AlertTriangle className="size-3" aria-label="Client error" />
301
+ ) : (
302
+ <Loader2 className="size-3 animate-spin" aria-label="Pending" />
303
+ )}
304
+ {statusLabel}
305
+ </Badge>
233
306
 
234
307
  {warningCount > 0 && (
235
308
  <Tooltip>
236
309
  <TooltipTrigger asChild>
237
310
  <Badge
238
311
  variant="outline"
239
- className="text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums gap-1 bg-amber-500/15 text-amber-400 border-amber-500/25"
312
+ className="h-6 gap-1 rounded-md border-amber-300/30 bg-amber-300/[0.10] px-2 py-0 font-mono text-[10px] tabular-nums text-amber-100"
240
313
  >
241
314
  <AlertTriangle className="size-3" aria-label="Tool schema warnings" />
242
315
  {warningCount}
@@ -258,7 +331,12 @@ export const LogEntryHeader = memo(function ({
258
331
  (isSlowResponse ? (
259
332
  <Tooltip>
260
333
  <TooltipTrigger asChild>
261
- <span className="flex items-center gap-1 text-xs text-amber-400 shrink-0">
334
+ <span
335
+ className={cn(
336
+ TELEMETRY_CHIP_CLASS,
337
+ "shrink-0 border-amber-300/25 text-amber-200",
338
+ )}
339
+ >
262
340
  <Clock className="size-3" />
263
341
  <span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
264
342
  <AlertTriangle className="size-3" aria-label="Slow response" />
@@ -270,7 +348,7 @@ export const LogEntryHeader = memo(function ({
270
348
  </TooltipContent>
271
349
  </Tooltip>
272
350
  ) : (
273
- <span className="flex items-center gap-1 text-xs text-muted-foreground shrink-0">
351
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
274
352
  <Clock className="size-3" />
275
353
  <span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
276
354
  </span>
@@ -280,7 +358,7 @@ export const LogEntryHeader = memo(function ({
280
358
  {log.streaming && firstChunkLabel !== null && (
281
359
  <Tooltip>
282
360
  <TooltipTrigger asChild>
283
- <span className="flex items-center gap-1 text-xs text-cyan-400 shrink-0">
361
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-cyan-300")}>
284
362
  <Radio className="size-3" />
285
363
  <span className="font-mono tabular-nums">first {firstChunkLabel}</span>
286
364
  </span>
@@ -291,7 +369,7 @@ export const LogEntryHeader = memo(function ({
291
369
  {log.streaming && tokenRateLabel !== null && (
292
370
  <Tooltip>
293
371
  <TooltipTrigger asChild>
294
- <span className="flex items-center gap-1 text-xs text-emerald-400 shrink-0">
372
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-emerald-300")}>
295
373
  <Gauge className="size-3" />
296
374
  <span className="font-mono tabular-nums">{tokenRateLabel}</span>
297
375
  </span>
@@ -302,7 +380,7 @@ export const LogEntryHeader = memo(function ({
302
380
 
303
381
  {/* Token counts */}
304
382
  {hasTokens && (
305
- <span className="flex min-w-0 items-center gap-1 text-xs sm:shrink-0">
383
+ <span className={cn(TELEMETRY_CHIP_CLASS, "min-w-0 sm:shrink-0")}>
306
384
  <Zap className="size-3 text-muted-foreground" />
307
385
  <span className="flex min-w-0 flex-wrap items-center gap-x-1 font-mono tabular-nums">
308
386
  <span
@@ -327,7 +405,7 @@ export const LogEntryHeader = memo(function ({
327
405
  )}
328
406
  {/* Cache tokens */}
329
407
  {log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
330
- <span className="flex items-center gap-1 text-xs shrink-0">
408
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0")}>
331
409
  <CacheTrendIndicator trend={cacheTrend?.creation ?? null} />
332
410
  <span className="font-mono tabular-nums text-emerald-400">
333
411
  KV Cache +{formatTokens(log.cacheCreationInputTokens)}
@@ -335,7 +413,7 @@ export const LogEntryHeader = memo(function ({
335
413
  </span>
336
414
  )}
337
415
  {log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
338
- <span className="flex items-center gap-1 text-xs shrink-0">
416
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0")}>
339
417
  <CacheTrendIndicator trend={cacheTrend?.read ?? null} />
340
418
  <span className="font-mono tabular-nums text-purple-400">
341
419
  KV Cache ~{formatTokens(log.cacheReadInputTokens)}
@@ -345,7 +423,7 @@ export const LogEntryHeader = memo(function ({
345
423
 
346
424
  {/* Message count */}
347
425
  {messageCount !== null && (
348
- <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
426
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
349
427
  <MessageSquare className="size-3" />
350
428
  <span className="font-mono tabular-nums">{messageCount}</span>
351
429
  </span>
@@ -353,7 +431,7 @@ export const LogEntryHeader = memo(function ({
353
431
 
354
432
  {/* Tool count */}
355
433
  {toolCount !== null && (
356
- <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
434
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
357
435
  <Wrench className="size-3" />
358
436
  <span className="font-mono tabular-nums">{toolCount}</span>
359
437
  </span>
@@ -362,7 +440,7 @@ export const LogEntryHeader = memo(function ({
362
440
  {/* Origin */}
363
441
  {log.origin !== null && (
364
442
  <span
365
- className="hidden xl:flex items-center gap-1 text-muted-foreground text-xs shrink-0"
443
+ className={cn(TELEMETRY_CHIP_CLASS, "hidden shrink-0 text-muted-foreground xl:flex")}
366
444
  title={`Origin: ${log.origin}`}
367
445
  >
368
446
  <Globe className="size-3" />
@@ -376,7 +454,7 @@ export const LogEntryHeader = memo(function ({
376
454
  {(hasClientPid || hasClientProjectFolder) && (
377
455
  <Tooltip>
378
456
  <TooltipTrigger asChild>
379
- <span className="hidden xl:flex items-center gap-1 text-purple-400/80 text-xs shrink-0">
457
+ <span className={cn(TELEMETRY_CHIP_CLASS, "hidden shrink-0 text-purple-300 xl:flex")}>
380
458
  <FileTerminal className="size-3" />
381
459
  {hasClientProjectFolder ? (
382
460
  <span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
@@ -537,13 +615,6 @@ export const LogEntryHeader = memo(function ({
537
615
  )}
538
616
  </span>
539
617
  )}
540
-
541
- {/* Expand chevron */}
542
- {expanded ? (
543
- <ChevronDown className="size-4 text-muted-foreground shrink-0" />
544
- ) : (
545
- <ChevronRight className="size-4 text-muted-foreground shrink-0" />
546
- )}
547
618
  </div>
548
619
  </TooltipProvider>
549
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
 
@@ -13,12 +13,11 @@ import {
13
13
  Terminal,
14
14
  Wrench,
15
15
  } from "lucide-react";
16
- import { memo, useMemo, useState, type JSX } from "react";
16
+ import { memo, Suspense, useMemo, useState, type JSX } from "react";
17
17
  import { cn } from "../../lib/utils";
18
18
  import { Badge } from "../ui/badge";
19
19
  import { Button } from "../ui/button";
20
20
  import { Collapsible, CollapsibleContent } from "../ui/collapsible";
21
- import { JsonViewer } from "../ui/json-viewer";
22
21
  import { safeJsonValue } from "../ui/json-viewer-bulk";
23
22
  import { ScrollArea } from "../ui/scroll-area";
24
23
  import type {
@@ -26,6 +25,7 @@ import type {
26
25
  RequestToolDefinition,
27
26
  RequestToolsSummary,
28
27
  } from "./requestTools";
28
+ import { LazyJsonViewer } from "./lazy";
29
29
  import { useCopyFeedback } from "./useCopyFeedback";
30
30
 
31
31
  type RequestToolsProps = {
@@ -152,17 +152,23 @@ function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolea
152
152
  </button>
153
153
  </div>
154
154
  <CollapsibleContent>
155
- <div className="space-y-2 border-t border-white/10 bg-white/[0.02] px-2.5 py-2">
156
- {tool.description !== null && (
157
- <p className="text-xs leading-relaxed text-muted-foreground">{tool.description}</p>
158
- )}
159
- <RequiredParameters names={tool.requiredParameters} />
160
- {tool.schema !== null && (
161
- <ScrollArea className="max-h-64 rounded-md border border-white/10 bg-[#06080b] p-2">
162
- <JsonViewer data={safeJsonValue(tool.schema)} defaultExpandDepth={0} />
163
- </ScrollArea>
164
- )}
165
- </div>
155
+ {open && (
156
+ <div className="space-y-2 border-t border-white/10 bg-white/[0.02] px-2.5 py-2">
157
+ {tool.description !== null && (
158
+ <p className="text-xs leading-relaxed text-muted-foreground">{tool.description}</p>
159
+ )}
160
+ <RequiredParameters names={tool.requiredParameters} />
161
+ {tool.schema !== null && (
162
+ <ScrollArea className="max-h-64 rounded-md border border-white/10 bg-[#06080b] p-2">
163
+ <Suspense
164
+ fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
165
+ >
166
+ <LazyJsonViewer data={safeJsonValue(tool.schema)} defaultExpandDepth={0} />
167
+ </Suspense>
168
+ </ScrollArea>
169
+ )}
170
+ </div>
171
+ )}
166
172
  </CollapsibleContent>
167
173
  </div>
168
174
  </Collapsible>
@@ -1,9 +1,9 @@
1
- import { memo, useState, useEffect, useMemo, type JSX } from "react";
1
+ import { memo, Suspense, useState, useEffect, useMemo, type JSX } from "react";
2
2
  import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
3
3
  import { Badge } from "../ui/badge";
4
- import { JsonViewer } from "../ui/json-viewer";
5
4
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
6
5
  import type { StreamingChunk } from "../../contracts";
6
+ import { LazyJsonViewer } from "./lazy";
7
7
 
8
8
  export type StreamingChunkSequenceProps = {
9
9
  logId: number;
@@ -140,7 +140,13 @@ export const StreamingChunkSequence = memo(function StreamingChunkSequence({
140
140
  {chunk.type}
141
141
  </span>
142
142
  </div>
143
- <JsonViewer data={chunk} defaultExpandDepth={0} showCopy />
143
+ <Suspense
144
+ fallback={
145
+ <div className="text-xs text-muted-foreground">Loading JSON...</div>
146
+ }
147
+ >
148
+ <LazyJsonViewer data={chunk} defaultExpandDepth={0} showCopy />
149
+ </Suspense>
144
150
  </div>
145
151
  ))}
146
152
  </div>
@@ -1,6 +1,6 @@
1
1
  import { type JSX, useMemo } from "react";
2
2
  import { cn } from "../../lib/utils";
3
- import type { StopReason } from "../../lib/stopReason";
3
+ import { isTurnBoundary, type StopReason } from "../../lib/stopReason";
4
4
  import { getCrabVariant, getInteriorCrabVariant } from "../ui/crab-variants";
5
5
 
6
6
  export type ThreadConnectorProps = {
@@ -18,10 +18,24 @@ export type ThreadConnectorProps = {
18
18
  onToggle?: () => void;
19
19
  };
20
20
 
21
+ function boundaryTitle(stopReason: StopReason): string {
22
+ switch (stopReason) {
23
+ case "end_turn":
24
+ return "End of Turn (Anthropic)";
25
+ case "stop":
26
+ return "End of Turn (OpenAI)";
27
+ case "length":
28
+ return "End of Turn (length limit)";
29
+ case "tool_use":
30
+ case null:
31
+ return "End of Turn";
32
+ }
33
+ }
34
+
21
35
  /**
22
36
  * Vertical timeline connector. Top spacer uses a calc() that
23
37
  * mixes rem (py-1 + half-line) and px (border) so the crab
24
- * centre-line matches the LogEntry `#id` index regardless of the
38
+ * centre-line matches the LogEntry display number regardless of the
25
39
  * root font-size. The bottom spacer is flex-1 so the outgoing
26
40
  * line adapts to whatever height the LogEntry occupies.
27
41
  */
@@ -35,7 +49,7 @@ export function ThreadConnector({
35
49
  collapsible = false,
36
50
  onToggle,
37
51
  }: ThreadConnectorProps): JSX.Element {
38
- const isBoundary = stopReason === "end_turn" || stopReason === "stop";
52
+ const isBoundary = isTurnBoundary(stopReason);
39
53
  const isFusedBoundary = isOnlyEntry && isTurnStart && isBoundary;
40
54
  const isRunning = isPending && !isBoundary;
41
55
  const Crab = useMemo(() => getCrabVariant(crabIndex), [crabIndex]);
@@ -84,10 +98,7 @@ export function ThreadConnector({
84
98
  />
85
99
  </span>
86
100
  ) : isBoundary ? (
87
- <span
88
- title={stopReason === "end_turn" ? "End of Turn (Anthropic)" : "End of Turn (OpenAI)"}
89
- {...interactiveProps}
90
- >
101
+ <span title={boundaryTitle(stopReason)} {...interactiveProps}>
91
102
  <Crab
92
103
  className={cn(
93
104
  "size-3.5 text-amber-400",