@tonyclaw/agent-inspector 2.1.13 → 2.1.14

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 (81) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-DjgjIFx7.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-sBA1KAYD.js → ReplayDialog-CU0Tbb2c.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-BNahe83D.js} +1 -1
  6. package/.output/public/assets/ResponseView-DSOnGqi6.js +3 -0
  7. package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BEKTDklB.js} +1 -1
  8. package/.output/public/assets/_sessionId-Cif8JZdn.js +1 -0
  9. package/.output/public/assets/index-DWOkqdCa.js +14 -0
  10. package/.output/public/assets/index-D_WfwzUi.js +1 -0
  11. package/.output/public/assets/index-DtLuQrs0.css +1 -0
  12. package/.output/public/assets/json-viewer-C2JpgcW0.js +1 -0
  13. package/.output/public/assets/{main-Dtspb4Ui.js → main-CSONBwwn.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +20 -20
  15. package/.output/server/_libs/react-markdown.mjs +90 -2
  16. package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-CPAa37n5.mjs} +6 -57
  17. package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-ceW5VxMo.mjs} +17 -62
  18. package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CDfEE_w-.mjs} +872 -688
  19. package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-V0s_eEbR.mjs} +8 -59
  20. package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-f1ccwR9d.mjs} +6 -57
  21. package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-BIRrqG4H.mjs} +138 -60
  22. package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-V3JFjCgX.mjs} +13 -58
  23. package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-DsykulzS.mjs} +6 -57
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/json-viewer-Dcnm0Ivf.mjs +478 -0
  26. package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-4bdm6Mt2.mjs} +674 -173
  27. package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Xp4CO64V.mjs} +1 -1
  28. package/.output/server/index.mjs +76 -62
  29. package/package.json +1 -1
  30. package/src/components/ProxyViewerContainer.tsx +25 -17
  31. package/src/components/providers/ImportWizardDialog.tsx +7 -1
  32. package/src/components/providers/ProviderCard.tsx +10 -1
  33. package/src/components/providers/ProviderForm.tsx +157 -41
  34. package/src/components/providers/ProvidersPanel.tsx +5 -1
  35. package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
  36. package/src/components/proxy-viewer/CompareDrawer.tsx +20 -6
  37. package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
  38. package/src/components/proxy-viewer/LogEntry.tsx +31 -11
  39. package/src/components/proxy-viewer/LogEntryHeader.tsx +111 -40
  40. package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
  41. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
  42. package/src/components/proxy-viewer/TurnGroup.tsx +53 -7
  43. package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
  44. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
  45. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
  46. package/src/components/proxy-viewer/formats/index.tsx +10 -3
  47. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
  48. package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
  49. package/src/components/proxy-viewer/logFocus.ts +15 -2
  50. package/src/components/proxy-viewer/requestTools.ts +7 -3
  51. package/src/components/proxy-viewer/viewerState.ts +72 -3
  52. package/src/components/ui/json-viewer-bulk.ts +41 -6
  53. package/src/components/ui/json-viewer.tsx +9 -8
  54. package/src/contracts/index.ts +15 -1
  55. package/src/contracts/openai.ts +94 -0
  56. package/src/lib/providerContract.ts +1 -0
  57. package/src/lib/providerModelMetadata.ts +7 -1
  58. package/src/lib/stopReason.ts +12 -0
  59. package/src/lib/upstreamUrl.ts +46 -0
  60. package/src/mcp/previewExtractor.ts +73 -3
  61. package/src/mcp/server.ts +2 -0
  62. package/src/mcp/toolHandlers.ts +2 -0
  63. package/src/proxy/constants.ts +2 -0
  64. package/src/proxy/formats/openai/handler.ts +40 -9
  65. package/src/proxy/formats/openai/index.ts +7 -0
  66. package/src/proxy/formats/openai/schemas.ts +15 -1
  67. package/src/proxy/formats/openai/stream.ts +529 -135
  68. package/src/proxy/formats/registry.ts +9 -1
  69. package/src/proxy/providerImporters.ts +45 -5
  70. package/src/proxy/providers.ts +31 -11
  71. package/src/proxy/schemas.ts +6 -0
  72. package/src/proxy/toolSchemaWarnings.ts +25 -3
  73. package/src/proxy/upstream.ts +18 -37
  74. package/src/routes/api/providers.$providerId.ts +1 -0
  75. package/src/routes/api/providers.ts +2 -0
  76. package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
  77. package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
  78. package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
  79. package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
  80. package/.output/public/assets/index-B-QQLbpz.js +0 -1
  81. package/.output/public/assets/index-CmtfjQPv.css +0 -1
@@ -34,15 +34,48 @@ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/
34
34
  import type { CacheTrend } from "./cacheTrend";
35
35
 
36
36
  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",
37
+ success: "bg-emerald-400/[0.08] text-emerald-200 border-emerald-300/25",
38
+ client_error: "bg-amber-300/[0.10] text-amber-100 border-amber-300/30",
39
+ server_error: "bg-rose-400/[0.10] text-rose-100 border-rose-300/30",
40
+ pending: "bg-cyan-300/[0.08] text-cyan-100 border-cyan-300/25",
41
+ };
42
+
43
+ const STATUS_CUE_CLASSES: Record<
44
+ StatusCategory,
45
+ {
46
+ frame: string;
47
+ dot: string;
48
+ rail: string;
49
+ }
50
+ > = {
51
+ success: {
52
+ frame: "border-emerald-300/20 bg-emerald-300/[0.06] text-emerald-100",
53
+ dot: "bg-emerald-300 shadow-[0_0_14px_rgba(110,231,183,0.8)]",
54
+ rail: "bg-emerald-300/45 shadow-[0_0_22px_rgba(110,231,183,0.28)]",
55
+ },
56
+ client_error: {
57
+ frame: "border-amber-300/25 bg-amber-300/[0.08] text-amber-100",
58
+ dot: "bg-amber-200 shadow-[0_0_14px_rgba(252,211,77,0.8)]",
59
+ rail: "bg-amber-300/50 shadow-[0_0_22px_rgba(252,211,77,0.28)]",
60
+ },
61
+ server_error: {
62
+ frame: "border-rose-300/25 bg-rose-300/[0.08] text-rose-100",
63
+ dot: "bg-rose-300 shadow-[0_0_14px_rgba(253,164,175,0.8)]",
64
+ rail: "bg-rose-300/50 shadow-[0_0_22px_rgba(253,164,175,0.28)]",
65
+ },
66
+ pending: {
67
+ frame: "border-cyan-300/25 bg-cyan-300/[0.08] text-cyan-100",
68
+ dot: "bg-cyan-200 shadow-[0_0_14px_rgba(103,232,249,0.8)]",
69
+ rail: "bg-cyan-300/50 shadow-[0_0_22px_rgba(103,232,249,0.28)]",
70
+ },
41
71
  };
42
72
 
43
73
  const HEADER_ACTION_BUTTON_CLASS =
44
74
  "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
75
 
76
+ const TELEMETRY_CHIP_CLASS =
77
+ "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)]";
78
+
46
79
  function formatElapsed(ms: number): string {
47
80
  if (ms < 1000) return `${ms}ms`;
48
81
  return `${(ms / 1000).toFixed(1)}s`;
@@ -53,6 +86,19 @@ function formatTokenRate(tokensPerSecond: number): string {
53
86
  return `${tokensPerSecond.toFixed(1)}/s`;
54
87
  }
55
88
 
89
+ function statusCueLabel(category: StatusCategory, status: number | null): string {
90
+ switch (category) {
91
+ case "success":
92
+ return status === null ? "OK" : String(status);
93
+ case "client_error":
94
+ return status === null ? "WARN" : String(status);
95
+ case "server_error":
96
+ return status === null ? "FAIL" : String(status);
97
+ case "pending":
98
+ return "ROLL";
99
+ }
100
+ }
101
+
56
102
  /**
57
103
  * Inline trend indicator: small arrow (green up / red down) plus the absolute
58
104
  * delta in compact form. Returns `null` when there is no trend to display.
@@ -169,6 +215,8 @@ export const LogEntryHeader = memo(function ({
169
215
  log.clientProjectFolder !== "";
170
216
  const hasClientCwd =
171
217
  log.clientCwd !== null && log.clientCwd !== undefined && log.clientCwd !== "";
218
+ const statusCue = STATUS_CUE_CLASSES[statusCategory];
219
+ const statusLabel = statusCueLabel(statusCategory, log.responseStatus);
172
220
 
173
221
  return (
174
222
  <TooltipProvider>
@@ -179,9 +227,11 @@ export const LogEntryHeader = memo(function ({
179
227
  data-nav-id={`log-${log.id}`}
180
228
  data-nav-action={expanded ? "collapse" : "expand"}
181
229
  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",
230
+ "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]",
231
+ expanded
232
+ ? "bg-[linear-gradient(90deg,#0f1720_0%,#090d12_50%,#07090d_100%)] shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]"
233
+ : "bg-[#080b10] hover:bg-[#0b1016]",
234
+ "select-none before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-white/10",
185
235
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
186
236
  )}
187
237
  onClick={onToggle}
@@ -192,13 +242,29 @@ export const LogEntryHeader = memo(function ({
192
242
  }
193
243
  }}
194
244
  >
245
+ <span
246
+ className={cn(
247
+ "absolute left-0 top-2 bottom-2 w-0.5 rounded-r-full transition-colors",
248
+ statusCue.rail,
249
+ )}
250
+ aria-hidden="true"
251
+ />
195
252
  {/* Request ID */}
196
- <span className="text-blue-400/80 font-mono text-xs font-semibold tabular-nums shrink-0">
197
- #{log.id}
253
+ <span
254
+ className={cn(
255
+ "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)]",
256
+ statusCue.frame,
257
+ )}
258
+ >
259
+ <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
262
+ </span>
263
+ <span className="text-xs font-semibold tabular-nums">#{log.id}</span>
198
264
  </span>
199
265
 
200
266
  {/* Request start time */}
201
- <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
267
+ <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
268
  <Clock className="size-3" />
203
269
  <span className="font-mono tabular-nums" title={log.timestamp}>
204
270
  {formatTimestamp(log.timestamp, timeDisplayFormat)}
@@ -212,31 +278,31 @@ export const LogEntryHeader = memo(function ({
212
278
  {/* Response Status — only shown for non-200 or pending. Each category
213
279
  carries a distinct icon in addition to color so the meaning is
214
280
  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
- )}
281
+ <Badge
282
+ variant="outline"
283
+ className={cn(
284
+ "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)]",
285
+ STATUS_BADGE_CLASSES[statusCategory],
286
+ )}
287
+ >
288
+ {statusCategory === "success" ? (
289
+ <Check className="size-3" aria-label="Success" />
290
+ ) : statusCategory === "server_error" ? (
291
+ <OctagonAlert className="size-3" aria-label="Server error" />
292
+ ) : statusCategory === "client_error" ? (
293
+ <AlertTriangle className="size-3" aria-label="Client error" />
294
+ ) : (
295
+ <Loader2 className="size-3 animate-spin" aria-label="Pending" />
296
+ )}
297
+ {statusLabel}
298
+ </Badge>
233
299
 
234
300
  {warningCount > 0 && (
235
301
  <Tooltip>
236
302
  <TooltipTrigger asChild>
237
303
  <Badge
238
304
  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"
305
+ 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
306
  >
241
307
  <AlertTriangle className="size-3" aria-label="Tool schema warnings" />
242
308
  {warningCount}
@@ -258,7 +324,12 @@ export const LogEntryHeader = memo(function ({
258
324
  (isSlowResponse ? (
259
325
  <Tooltip>
260
326
  <TooltipTrigger asChild>
261
- <span className="flex items-center gap-1 text-xs text-amber-400 shrink-0">
327
+ <span
328
+ className={cn(
329
+ TELEMETRY_CHIP_CLASS,
330
+ "shrink-0 border-amber-300/25 text-amber-200",
331
+ )}
332
+ >
262
333
  <Clock className="size-3" />
263
334
  <span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
264
335
  <AlertTriangle className="size-3" aria-label="Slow response" />
@@ -270,7 +341,7 @@ export const LogEntryHeader = memo(function ({
270
341
  </TooltipContent>
271
342
  </Tooltip>
272
343
  ) : (
273
- <span className="flex items-center gap-1 text-xs text-muted-foreground shrink-0">
344
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
274
345
  <Clock className="size-3" />
275
346
  <span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
276
347
  </span>
@@ -280,7 +351,7 @@ export const LogEntryHeader = memo(function ({
280
351
  {log.streaming && firstChunkLabel !== null && (
281
352
  <Tooltip>
282
353
  <TooltipTrigger asChild>
283
- <span className="flex items-center gap-1 text-xs text-cyan-400 shrink-0">
354
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-cyan-300")}>
284
355
  <Radio className="size-3" />
285
356
  <span className="font-mono tabular-nums">first {firstChunkLabel}</span>
286
357
  </span>
@@ -291,7 +362,7 @@ export const LogEntryHeader = memo(function ({
291
362
  {log.streaming && tokenRateLabel !== null && (
292
363
  <Tooltip>
293
364
  <TooltipTrigger asChild>
294
- <span className="flex items-center gap-1 text-xs text-emerald-400 shrink-0">
365
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-emerald-300")}>
295
366
  <Gauge className="size-3" />
296
367
  <span className="font-mono tabular-nums">{tokenRateLabel}</span>
297
368
  </span>
@@ -302,7 +373,7 @@ export const LogEntryHeader = memo(function ({
302
373
 
303
374
  {/* Token counts */}
304
375
  {hasTokens && (
305
- <span className="flex min-w-0 items-center gap-1 text-xs sm:shrink-0">
376
+ <span className={cn(TELEMETRY_CHIP_CLASS, "min-w-0 sm:shrink-0")}>
306
377
  <Zap className="size-3 text-muted-foreground" />
307
378
  <span className="flex min-w-0 flex-wrap items-center gap-x-1 font-mono tabular-nums">
308
379
  <span
@@ -327,7 +398,7 @@ export const LogEntryHeader = memo(function ({
327
398
  )}
328
399
  {/* Cache tokens */}
329
400
  {log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
330
- <span className="flex items-center gap-1 text-xs shrink-0">
401
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0")}>
331
402
  <CacheTrendIndicator trend={cacheTrend?.creation ?? null} />
332
403
  <span className="font-mono tabular-nums text-emerald-400">
333
404
  KV Cache +{formatTokens(log.cacheCreationInputTokens)}
@@ -335,7 +406,7 @@ export const LogEntryHeader = memo(function ({
335
406
  </span>
336
407
  )}
337
408
  {log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
338
- <span className="flex items-center gap-1 text-xs shrink-0">
409
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0")}>
339
410
  <CacheTrendIndicator trend={cacheTrend?.read ?? null} />
340
411
  <span className="font-mono tabular-nums text-purple-400">
341
412
  KV Cache ~{formatTokens(log.cacheReadInputTokens)}
@@ -345,7 +416,7 @@ export const LogEntryHeader = memo(function ({
345
416
 
346
417
  {/* Message count */}
347
418
  {messageCount !== null && (
348
- <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
419
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
349
420
  <MessageSquare className="size-3" />
350
421
  <span className="font-mono tabular-nums">{messageCount}</span>
351
422
  </span>
@@ -353,7 +424,7 @@ export const LogEntryHeader = memo(function ({
353
424
 
354
425
  {/* Tool count */}
355
426
  {toolCount !== null && (
356
- <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
427
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
357
428
  <Wrench className="size-3" />
358
429
  <span className="font-mono tabular-nums">{toolCount}</span>
359
430
  </span>
@@ -362,7 +433,7 @@ export const LogEntryHeader = memo(function ({
362
433
  {/* Origin */}
363
434
  {log.origin !== null && (
364
435
  <span
365
- className="hidden xl:flex items-center gap-1 text-muted-foreground text-xs shrink-0"
436
+ className={cn(TELEMETRY_CHIP_CLASS, "hidden shrink-0 text-muted-foreground xl:flex")}
366
437
  title={`Origin: ${log.origin}`}
367
438
  >
368
439
  <Globe className="size-3" />
@@ -376,7 +447,7 @@ export const LogEntryHeader = memo(function ({
376
447
  {(hasClientPid || hasClientProjectFolder) && (
377
448
  <Tooltip>
378
449
  <TooltipTrigger asChild>
379
- <span className="hidden xl:flex items-center gap-1 text-purple-400/80 text-xs shrink-0">
450
+ <span className={cn(TELEMETRY_CHIP_CLASS, "hidden shrink-0 text-purple-300 xl:flex")}>
380
451
  <FileTerminal className="size-3" />
381
452
  {hasClientProjectFolder ? (
382
453
  <span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
@@ -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,4 +1,13 @@
1
- import { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import {
2
+ type CSSProperties,
3
+ type JSX,
4
+ memo,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
2
11
  import { AlertTriangle, ChevronRight, Clock, Zap } from "lucide-react";
3
12
  import { isTurnBoundary } from "../../lib/stopReason";
4
13
  import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
@@ -9,7 +18,12 @@ import { ProviderLogo, detectProvider, type Provider } from "../providers/Provid
9
18
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
10
19
  import type { CacheTrendEntry } from "./cacheTrend";
11
20
  import { LogEntry } from "./LogEntry";
12
- import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
21
+ import {
22
+ dispatchLogFocusRequest,
23
+ LOG_FOCUS_REQUEST_EVENT,
24
+ readLogFocusRequest,
25
+ type LogFocusRequest,
26
+ } from "./logFocus";
13
27
  import { ThreadConnector } from "./ThreadConnector";
14
28
  import { ToolTraceEvents } from "./ToolTraceEvents";
15
29
  import { extractToolTraceEvents, isTurnCollapsible, type TurnEntry } from "./viewerState";
@@ -19,7 +33,11 @@ function formatElapsed(ms: number): string {
19
33
  return `${(ms / 1000).toFixed(1)}s`;
20
34
  }
21
35
 
22
- type TurnGroupProps = {
36
+ export type PendingTurnFocusRequest = LogFocusRequest & {
37
+ nonce: number;
38
+ };
39
+
40
+ export type TurnGroupProps = {
23
41
  entries: TurnEntry[];
24
42
  viewMode: "simple" | "full";
25
43
  strip: boolean;
@@ -29,6 +47,7 @@ type TurnGroupProps = {
29
47
  comparisonPredecessors: Map<number, CapturedLog>;
30
48
  turnIndex?: number;
31
49
  timeDisplayFormat: TimeDisplayFormat;
50
+ pendingFocusRequest?: PendingTurnFocusRequest;
32
51
  };
33
52
 
34
53
  export const TurnGroup = memo(function TurnGroup({
@@ -41,6 +60,7 @@ export const TurnGroup = memo(function TurnGroup({
41
60
  comparisonPredecessors,
42
61
  turnIndex = 0,
43
62
  timeDisplayFormat,
63
+ pendingFocusRequest,
44
64
  }: TurnGroupProps): JSX.Element {
45
65
  const lastIdx = entries.length - 1;
46
66
  const lastStop = entries[lastIdx]?.stopReason ?? null;
@@ -75,6 +95,22 @@ export const TurnGroup = memo(function TurnGroup({
75
95
  };
76
96
  }, [collapsible, entries]);
77
97
 
98
+ useEffect(() => {
99
+ if (pendingFocusRequest === undefined) return undefined;
100
+ if (!entries.some((entry) => entry.log.id === pendingFocusRequest.logId)) return undefined;
101
+ if (collapsible) setCollapsed(false);
102
+
103
+ const timer = window.setTimeout(() => {
104
+ dispatchLogFocusRequest({
105
+ logId: pendingFocusRequest.logId,
106
+ tab: pendingFocusRequest.tab,
107
+ source: "virtualizer",
108
+ });
109
+ }, 0);
110
+
111
+ return () => window.clearTimeout(timer);
112
+ }, [collapsible, entries, pendingFocusRequest]);
113
+
78
114
  const toggleCollapse = useCallback(() => {
79
115
  if (collapsible) setCollapsed((prev) => !prev);
80
116
  }, [collapsible]);
@@ -120,7 +156,7 @@ export const TurnGroup = memo(function TurnGroup({
120
156
  const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
121
157
  const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
122
158
 
123
- const bgClass = turnIndex % 2 === 0 ? "bg-muted/10" : "bg-muted/25";
159
+ const bgClass = turnIndex % 2 === 0 ? "bg-[#070a0f]" : "bg-[#090d12]";
124
160
  const aggregateIsSlow =
125
161
  aggregate.maxElapsed !== null &&
126
162
  slowResponseThresholdSeconds > 0 &&
@@ -154,17 +190,27 @@ export const TurnGroup = memo(function TurnGroup({
154
190
 
155
191
  const firstLogId = entries[0]?.log.id ?? turnIndex;
156
192
  const turnLabel = `Turn ${String(turnIndex + 1)}`;
193
+ const containmentStyle = useMemo<CSSProperties>(
194
+ () => ({
195
+ contentVisibility: "auto",
196
+ containIntrinsicSize: collapsed ? "48px" : "160px",
197
+ }),
198
+ [collapsed],
199
+ );
157
200
 
158
201
  return (
159
202
  <div
160
203
  ref={containerRef}
204
+ style={containmentStyle}
161
205
  tabIndex={collapsed ? undefined : 0}
162
206
  role={collapsed ? undefined : "group"}
163
207
  aria-label={collapsed ? undefined : turnLabel}
164
208
  data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
165
209
  className={cn(
166
- "border rounded-lg",
167
- isPending ? "border-amber-500/10" : "border-transparent",
210
+ "border rounded-lg transition-[border-color,box-shadow]",
211
+ isPending
212
+ ? "border-amber-300/15 shadow-[0_0_0_1px_rgba(252,211,77,0.04)]"
213
+ : "border-transparent",
168
214
  !collapsed &&
169
215
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
170
216
  )}
@@ -252,7 +298,7 @@ export const TurnGroup = memo(function TurnGroup({
252
298
  {entries.length > 1 && (
253
299
  <div
254
300
  className={cn(
255
- "flex-1 min-w-0 mb-0.5 rounded-lg border border-border py-1 px-3 flex items-center gap-3 text-xs",
301
+ "flex-1 min-w-0 mb-0.5 rounded-lg border border-cyan-300/10 py-1.5 px-3 flex items-center gap-3 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
256
302
  bgClass,
257
303
  )}
258
304
  >
@@ -0,0 +1,153 @@
1
+ import { useWindowVirtualizer } from "@tanstack/react-virtual";
2
+ import { type JSX, useEffect, useMemo, useState } from "react";
3
+ import type { CapturedLog } from "../../contracts";
4
+ import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
5
+ import type { CacheTrendEntry } from "./cacheTrend";
6
+ import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
7
+ import { TurnGroup } from "./TurnGroup";
8
+ import type { PendingTurnFocusRequest } from "./TurnGroup";
9
+ import type { TurnGroupData } from "./viewerState";
10
+
11
+ const VIRTUALIZE_TURN_THRESHOLD = 24;
12
+ const ESTIMATED_TURN_HEIGHT = 132;
13
+ const VIRTUAL_OVERSCAN = 8;
14
+
15
+ type TurnGroupListProps = {
16
+ turnGroups: TurnGroupData[];
17
+ viewMode: "simple" | "full";
18
+ strip: boolean;
19
+ slowResponseThresholdSeconds: number;
20
+ cacheTrends?: Map<number, CacheTrendEntry>;
21
+ onCompareWithPrevious: (log: CapturedLog) => void;
22
+ comparisonPredecessors: Map<number, CapturedLog>;
23
+ timeDisplayFormat: TimeDisplayFormat;
24
+ };
25
+
26
+ type PendingVirtualFocusRequest = PendingTurnFocusRequest & {
27
+ turnIndex: number;
28
+ };
29
+
30
+ export function shouldVirtualizeTurnGroups(turnCount: number): boolean {
31
+ return turnCount > VIRTUALIZE_TURN_THRESHOLD;
32
+ }
33
+
34
+ export function findTurnGroupIndexForLog(
35
+ turnGroups: readonly TurnGroupData[],
36
+ logId: number,
37
+ ): number | null {
38
+ for (let index = 0; index < turnGroups.length; index++) {
39
+ const group = turnGroups[index];
40
+ if (group === undefined) continue;
41
+ if (group.entries.some((entry) => entry.log.id === logId)) return index;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function buildLogTurnIndex(turnGroups: readonly TurnGroupData[]): Map<number, number> {
47
+ const result = new Map<number, number>();
48
+ for (let index = 0; index < turnGroups.length; index++) {
49
+ const group = turnGroups[index];
50
+ if (group === undefined) continue;
51
+ for (const entry of group.entries) {
52
+ result.set(entry.log.id, index);
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+
58
+ function renderTurnGroup(
59
+ turnGroup: TurnGroupData,
60
+ props: TurnGroupListProps,
61
+ pendingFocusRequest?: PendingTurnFocusRequest,
62
+ ): JSX.Element {
63
+ return (
64
+ <TurnGroup
65
+ key={turnGroup.turnIndex}
66
+ entries={turnGroup.entries}
67
+ viewMode={props.viewMode}
68
+ strip={props.strip}
69
+ slowResponseThresholdSeconds={props.slowResponseThresholdSeconds}
70
+ timeDisplayFormat={props.timeDisplayFormat}
71
+ cacheTrends={props.cacheTrends}
72
+ onCompareWithPrevious={props.onCompareWithPrevious}
73
+ comparisonPredecessors={props.comparisonPredecessors}
74
+ turnIndex={turnGroup.turnIndex}
75
+ pendingFocusRequest={pendingFocusRequest}
76
+ />
77
+ );
78
+ }
79
+
80
+ export function TurnGroupList(props: TurnGroupListProps): JSX.Element {
81
+ if (!shouldVirtualizeTurnGroups(props.turnGroups.length)) {
82
+ return <>{props.turnGroups.map((turnGroup) => renderTurnGroup(turnGroup, props))}</>;
83
+ }
84
+
85
+ return <VirtualizedTurnGroupList {...props} />;
86
+ }
87
+
88
+ function VirtualizedTurnGroupList(props: TurnGroupListProps): JSX.Element {
89
+ const { turnGroups } = props;
90
+ const [pendingFocusRequest, setPendingFocusRequest] = useState<PendingVirtualFocusRequest | null>(
91
+ null,
92
+ );
93
+
94
+ const virtualizer = useWindowVirtualizer({
95
+ count: turnGroups.length,
96
+ estimateSize: () => ESTIMATED_TURN_HEIGHT,
97
+ overscan: VIRTUAL_OVERSCAN,
98
+ getItemKey: (index) => turnGroups[index]?.turnIndex ?? index,
99
+ });
100
+
101
+ const turnIndexByLogId = useMemo(() => buildLogTurnIndex(turnGroups), [turnGroups]);
102
+
103
+ useEffect(() => {
104
+ const handleLogFocusRequest = (event: Event): void => {
105
+ const request = readLogFocusRequest(event);
106
+ if (request === null) return;
107
+ if (request.source === "virtualizer") return;
108
+
109
+ const turnIndex = turnIndexByLogId.get(request.logId);
110
+ if (turnIndex === undefined) return;
111
+ virtualizer.scrollToIndex(turnIndex, { align: "center" });
112
+ setPendingFocusRequest((previous) => ({
113
+ ...request,
114
+ turnIndex,
115
+ nonce: previous === null ? 1 : previous.nonce + 1,
116
+ }));
117
+ };
118
+
119
+ window.addEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
120
+ return () => {
121
+ window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
122
+ };
123
+ }, [turnIndexByLogId, virtualizer]);
124
+
125
+ return (
126
+ <div
127
+ className="relative w-full"
128
+ style={{ height: `${virtualizer.getTotalSize()}px` }}
129
+ data-virtualized-turn-list="true"
130
+ >
131
+ {virtualizer.getVirtualItems().map((virtualItem) => {
132
+ const turnGroup = turnGroups[virtualItem.index];
133
+ if (turnGroup === undefined) return null;
134
+ const itemFocusRequest =
135
+ pendingFocusRequest !== null && pendingFocusRequest.turnIndex === virtualItem.index
136
+ ? pendingFocusRequest
137
+ : undefined;
138
+
139
+ return (
140
+ <div
141
+ key={virtualItem.key}
142
+ data-index={virtualItem.index}
143
+ ref={virtualizer.measureElement}
144
+ className="absolute left-0 top-0 w-full"
145
+ style={{ transform: `translateY(${virtualItem.start}px)` }}
146
+ >
147
+ {renderTurnGroup(turnGroup, props, itemFocusRequest)}
148
+ </div>
149
+ );
150
+ })}
151
+ </div>
152
+ );
153
+ }