@tonyclaw/agent-inspector 2.1.15 → 2.1.17

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 (73) hide show
  1. package/.output/cli.js +57 -13
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
  7. package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
  9. package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
  10. package/.output/public/assets/index-Blqvndcn.js +1 -0
  11. package/.output/public/assets/index-Cs79WRQj.css +1 -0
  12. package/.output/public/assets/{index-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
  13. package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
  14. package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +255 -201
  16. package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
  17. package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
  19. package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
  20. package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
  21. package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
  23. package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
  26. package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
  27. package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
  28. package/.output/server/index.mjs +63 -63
  29. package/README.md +57 -2
  30. package/package.json +1 -1
  31. package/src/cli/startupOutput.ts +16 -0
  32. package/src/cli.ts +61 -15
  33. package/src/components/ProxyViewer.tsx +535 -163
  34. package/src/components/ProxyViewerContainer.tsx +77 -67
  35. package/src/components/alerts/AlertsDialog.tsx +2 -1
  36. package/src/components/clients/ClientLogo.tsx +1 -1
  37. package/src/components/groups/GroupsDialog.tsx +19 -1
  38. package/src/components/providers/ProviderCard.tsx +133 -32
  39. package/src/components/providers/ProviderForm.tsx +61 -29
  40. package/src/components/providers/ProviderLogo.tsx +6 -1
  41. package/src/components/providers/ProvidersPanel.tsx +7 -7
  42. package/src/components/providers/SettingsDialog.tsx +13 -6
  43. package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
  44. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  45. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  46. package/src/components/proxy-viewer/ConversationHeader.tsx +153 -120
  47. package/src/components/proxy-viewer/LogEntry.tsx +130 -43
  48. package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
  49. package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
  50. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  51. package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
  52. package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
  53. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  54. package/src/components/proxy-viewer/bodyHydration.ts +56 -0
  55. package/src/components/proxy-viewer/viewerState.ts +0 -4
  56. package/src/components/ui/icon-trigger.ts +2 -0
  57. package/src/components/ui/transient-toast.tsx +104 -0
  58. package/src/lib/providerTestContract.ts +56 -10
  59. package/src/lib/providerTestPrompt.ts +23 -1
  60. package/src/lib/sessionInfoContract.ts +4 -0
  61. package/src/lib/upstreamUrl.ts +105 -1
  62. package/src/mcp/server.ts +71 -27
  63. package/src/mcp/toolHandlers.ts +24 -5
  64. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  65. package/src/proxy/sessionInfo.ts +31 -2
  66. package/src/proxy/sessionSupervisor.ts +20 -1
  67. package/src/proxy/store.ts +1 -1
  68. package/src/routes/api/providers.$providerId.test.log.ts +93 -14
  69. package/styles/globals.css +180 -180
  70. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
  71. package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
  72. package/.output/public/assets/index-CI1-G8ua.js +0 -1
  73. package/.output/public/assets/index-DdhFqPsI.css +0 -1
@@ -32,10 +32,9 @@ 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";
36
35
 
37
36
  const STATUS_BADGE_CLASSES: Record<StatusCategory, string> = {
38
- success: "bg-emerald-400/[0.08] text-emerald-200 border-emerald-300/25",
37
+ success: "bg-slate-300/[0.08] text-slate-100 border-slate-300/20",
39
38
  client_error: "bg-amber-300/[0.10] text-amber-100 border-amber-300/30",
40
39
  server_error: "bg-rose-400/[0.10] text-rose-100 border-rose-300/30",
41
40
  pending: "bg-cyan-300/[0.08] text-cyan-100 border-cyan-300/25",
@@ -46,36 +45,31 @@ const STATUS_CUE_CLASSES: Record<
46
45
  {
47
46
  frame: string;
48
47
  dot: string;
49
- rail: string;
50
48
  }
51
49
  > = {
52
50
  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)]",
51
+ frame: "border-cyan-200/15 bg-white/[0.04] text-slate-100",
52
+ dot: "bg-cyan-200/80 shadow-[0_0_14px_rgba(125,211,252,0.65)]",
56
53
  },
57
54
  client_error: {
58
55
  frame: "border-amber-300/25 bg-amber-300/[0.08] text-amber-100",
59
56
  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
57
  },
62
58
  server_error: {
63
59
  frame: "border-rose-300/25 bg-rose-300/[0.08] text-rose-100",
64
60
  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
61
  },
67
62
  pending: {
68
63
  frame: "border-cyan-300/25 bg-cyan-300/[0.08] text-cyan-100",
69
64
  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
65
  },
72
66
  };
73
67
 
74
68
  const HEADER_ACTION_BUTTON_CLASS =
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";
69
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground size-8 text-muted-foreground shadow-none hover:text-cyan-100";
76
70
 
77
71
  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)]";
72
+ "border border-border bg-muted/40 flex h-7 items-center gap-1 rounded-md px-2 text-xs";
79
73
 
80
74
  function formatElapsed(ms: number): string {
81
75
  if (ms < 1000) return `${ms}ms`;
@@ -101,7 +95,7 @@ function statusCueLabel(category: StatusCategory, status: number | null): string
101
95
  }
102
96
 
103
97
  /**
104
- * Inline trend indicator: small arrow (green up / red down) plus the absolute
98
+ * Inline trend indicator: small arrow (up / down) plus the absolute
105
99
  * delta in compact form. Returns `null` when there is no trend to display.
106
100
  */
107
101
  function CacheTrendIndicator({ trend }: { trend: CacheTrend | null }): JSX.Element | null {
@@ -111,7 +105,7 @@ function CacheTrendIndicator({ trend }: { trend: CacheTrend | null }): JSX.Eleme
111
105
  const sign = isUp ? "+" : "-";
112
106
  return (
113
107
  <span className="flex items-center gap-0.5 text-muted-foreground tabular-nums">
114
- <Icon className={isUp ? "size-3 text-emerald-400" : "size-3 text-rose-400"} />
108
+ <Icon className={isUp ? "size-3 text-cyan-300" : "size-3 text-rose-400"} />
115
109
  <span className="font-mono">
116
110
  {sign}
117
111
  {formatTokens(trend.delta)}
@@ -122,12 +116,12 @@ function CacheTrendIndicator({ trend }: { trend: CacheTrend | null }): JSX.Eleme
122
116
 
123
117
  /**
124
118
  * Per-tab action bundle surfaced in the header when the tab is active.
125
- * - `copyText === null` Copy button hidden (e.g. tab has no body to copy)
126
- * - `expansion === null` Expand-all button hidden (e.g. flat dict, or
119
+ * - `copyText === null` - Copy button hidden (e.g. tab has no body to copy)
120
+ * - `expansion === null` - Expand-all button hidden (e.g. flat dict, or
127
121
  * not valid JSON). When present, the button reflects the JSON viewer's
128
122
  * bulk-expansion state and the caller is responsible for forwarding the
129
123
  * matching `bulkDepth` / `bulkRevision` to the underlying viewer.
130
- * - `diffWithRaw` / `diffWithPrevious` optional Diff buttons. Undefined
124
+ * - `diffWithRaw` / `diffWithPrevious` - optional Diff buttons. Undefined
131
125
  * means "not applicable" (hidden). Today only the Request tab sets them.
132
126
  */
133
127
  export type HeaderTabAction = {
@@ -181,6 +175,8 @@ export type LogEntryHeaderProps = {
181
175
  onReplay?: () => void;
182
176
  /** Slow-response threshold in seconds. `0` disables the warning indicator. */
183
177
  slowResponseThresholdSeconds?: number;
178
+ /** Show client PID/project metadata on each log row. Parent process rows can disable this. */
179
+ showClientMetadata?: boolean;
184
180
  /** Controls whether timestamps render as compact local time or full ISO strings. */
185
181
  timeDisplayFormat: TimeDisplayFormat;
186
182
  };
@@ -197,6 +193,7 @@ export const LogEntryHeader = memo(function ({
197
193
  tabActions,
198
194
  onReplay,
199
195
  slowResponseThresholdSeconds = 0,
196
+ showClientMetadata = true,
200
197
  timeDisplayFormat,
201
198
  }: LogEntryHeaderProps): JSX.Element {
202
199
  const statusCategory = getStatusCategory(log.responseStatus);
@@ -221,6 +218,9 @@ export const LogEntryHeader = memo(function ({
221
218
  log.clientCwd !== null && log.clientCwd !== undefined && log.clientCwd !== "";
222
219
  const statusCue = STATUS_CUE_CLASSES[statusCategory];
223
220
  const statusLabel = statusCueLabel(statusCategory, log.responseStatus);
221
+ const showStatusBadge = statusCategory !== "success" || log.responseStatus !== 200;
222
+ const showStandaloneStreamingIndicator =
223
+ log.streaming && firstChunkLabel === null && tokenRateLabel === null;
224
224
 
225
225
  return (
226
226
  <TooltipProvider>
@@ -231,11 +231,11 @@ export const LogEntryHeader = memo(function ({
231
231
  data-nav-id={`log-${log.id}`}
232
232
  data-nav-action={expanded ? "collapse" : "expand"}
233
233
  className={cn(
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]",
234
+ "group/header relative isolate flex min-w-0 cursor-pointer items-start gap-2 overflow-hidden px-3 py-2 transition-[background-color,border-color,box-shadow]",
235
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",
236
+ ? "bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.10)]"
237
+ : "bg-transparent hover:bg-white/[0.035]",
238
+ "select-none before:pointer-events-none before:absolute before:inset-x-3 before:top-0 before:h-px before:bg-white/12",
239
239
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
240
240
  )}
241
241
  onClick={onToggle}
@@ -246,375 +246,378 @@ export const LogEntryHeader = memo(function ({
246
246
  }
247
247
  }}
248
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
-
264
- {/* Request 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}
249
+ <span className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1">
250
+ {/* Request ID */}
251
+ <span
252
+ className={cn(
253
+ "border border-border bg-muted/40 inline-flex h-8 shrink-0 items-center gap-2 rounded-md px-2 font-mono",
254
+ statusCue.frame,
255
+ )}
256
+ >
257
+ <span className={cn("size-1.5 rounded-full", statusCue.dot)} aria-hidden="true" />
258
+ <span className="text-xs font-semibold tabular-nums" title={`Log ID ${String(log.id)}`}>
259
+ #{displayNumber}
260
+ </span>
274
261
  </span>
275
- </span>
276
262
 
277
- {/* Request start time */}
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">
279
- <Clock className="size-3" />
280
- <span className="font-mono tabular-nums" title={log.timestamp}>
281
- {formatTimestamp(log.timestamp, timeDisplayFormat)}
263
+ {/* Request start time */}
264
+ <span className="border border-border bg-muted/40 flex h-7 shrink-0 items-center gap-1 rounded-md px-2 text-xs text-muted-foreground">
265
+ <Clock className="size-3" />
266
+ <span className="font-mono tabular-nums" title={log.timestamp}>
267
+ {formatTimestamp(log.timestamp, timeDisplayFormat)}
268
+ </span>
282
269
  </span>
283
- </span>
284
270
 
285
- {/* Response Status only shown for non-200 or pending. Each category
271
+ {/* Response Status - only shown for non-200 or pending. Each category
286
272
  carries a distinct icon in addition to color so the meaning is
287
273
  legible without color perception. */}
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],
274
+ {showStatusBadge && (
275
+ <Badge
276
+ variant="outline"
277
+ className={cn(
278
+ "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)]",
279
+ STATUS_BADGE_CLASSES[statusCategory],
280
+ )}
281
+ >
282
+ {statusCategory === "success" ? (
283
+ <Check className="size-3" aria-label="Success" />
284
+ ) : statusCategory === "server_error" ? (
285
+ <OctagonAlert className="size-3" aria-label="Server error" />
286
+ ) : statusCategory === "client_error" ? (
287
+ <AlertTriangle className="size-3" aria-label="Client error" />
288
+ ) : (
289
+ <Loader2 className="size-3 animate-spin" aria-label="Pending" />
290
+ )}
291
+ {statusLabel}
292
+ </Badge>
293
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>
306
-
307
- {warningCount > 0 && (
308
- <Tooltip>
309
- <TooltipTrigger asChild>
310
- <Badge
311
- variant="outline"
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"
313
- >
314
- <AlertTriangle className="size-3" aria-label="Tool schema warnings" />
315
- {warningCount}
316
- </Badge>
317
- </TooltipTrigger>
318
- <TooltipContent>
319
- {warningCount === 1
320
- ? "1 tool schema warning"
321
- : `${warningCount} tool schema warnings`}
322
- </TooltipContent>
323
- </Tooltip>
324
- )}
325
294
 
326
- {/* Elapsed time. Tooltip is only shown when this log exceeded the slow-
327
- response threshold — for normal-speed logs the elapsed value is
328
- self-explanatory, so we skip the tooltip wrapper to keep the row
329
- quiet. */}
330
- {log.elapsedMs !== null &&
331
- (isSlowResponse ? (
295
+ {warningCount > 0 && (
332
296
  <Tooltip>
333
297
  <TooltipTrigger asChild>
334
- <span
335
- className={cn(
336
- TELEMETRY_CHIP_CLASS,
337
- "shrink-0 border-amber-300/25 text-amber-200",
338
- )}
298
+ <Badge
299
+ variant="outline"
300
+ 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"
339
301
  >
340
- <Clock className="size-3" />
341
- <span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
342
- <AlertTriangle className="size-3" aria-label="Slow response" />
343
- </span>
302
+ <AlertTriangle className="size-3" aria-label="Tool schema warnings" />
303
+ {warningCount}
304
+ </Badge>
344
305
  </TooltipTrigger>
345
306
  <TooltipContent>
346
- Slow response: {formatElapsed(log.elapsedMs)} exceeds{" "}
347
- {formatElapsed(slowResponseThresholdSeconds * 1000)}
307
+ {warningCount === 1
308
+ ? "1 tool schema warning"
309
+ : `${warningCount} tool schema warnings`}
348
310
  </TooltipContent>
349
311
  </Tooltip>
350
- ) : (
351
- <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
352
- <Clock className="size-3" />
353
- <span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
354
- </span>
355
- ))}
312
+ )}
356
313
 
357
- {/* Streaming timing */}
358
- {log.streaming && firstChunkLabel !== null && (
359
- <Tooltip>
360
- <TooltipTrigger asChild>
361
- <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-cyan-300")}>
362
- <Radio className="size-3" />
363
- <span className="font-mono tabular-nums">first {firstChunkLabel}</span>
364
- </span>
365
- </TooltipTrigger>
366
- <TooltipContent>Time to first streaming chunk</TooltipContent>
367
- </Tooltip>
368
- )}
369
- {log.streaming && tokenRateLabel !== null && (
370
- <Tooltip>
371
- <TooltipTrigger asChild>
372
- <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-emerald-300")}>
373
- <Gauge className="size-3" />
374
- <span className="font-mono tabular-nums">{tokenRateLabel}</span>
314
+ {/* Elapsed time. Tooltip is only shown when this log exceeded the slow-
315
+ response threshold - for normal-speed logs the elapsed value is
316
+ self-explanatory, so we skip the tooltip wrapper to keep the row
317
+ quiet. */}
318
+ {log.elapsedMs !== null &&
319
+ (isSlowResponse ? (
320
+ <Tooltip>
321
+ <TooltipTrigger asChild>
322
+ <span
323
+ className={cn(
324
+ TELEMETRY_CHIP_CLASS,
325
+ "shrink-0 border-amber-300/25 text-amber-200",
326
+ )}
327
+ >
328
+ <Clock className="size-3" />
329
+ <span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
330
+ <AlertTriangle className="size-3" aria-label="Slow response" />
331
+ </span>
332
+ </TooltipTrigger>
333
+ <TooltipContent>
334
+ Slow response: {formatElapsed(log.elapsedMs)} exceeds{" "}
335
+ {formatElapsed(slowResponseThresholdSeconds * 1000)}
336
+ </TooltipContent>
337
+ </Tooltip>
338
+ ) : (
339
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
340
+ <Clock className="size-3" />
341
+ <span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
375
342
  </span>
376
- </TooltipTrigger>
377
- <TooltipContent>Output token throughput after first chunk</TooltipContent>
378
- </Tooltip>
379
- )}
343
+ ))}
380
344
 
381
- {/* Token counts */}
382
- {hasTokens && (
383
- <span className={cn(TELEMETRY_CHIP_CLASS, "min-w-0 sm:shrink-0")}>
384
- <Zap className="size-3 text-muted-foreground" />
385
- <span className="flex min-w-0 flex-wrap items-center gap-x-1 font-mono tabular-nums">
386
- <span
387
- className={cn(
388
- "whitespace-nowrap",
389
- log.inputTokens !== null ? "text-blue-400" : "text-muted-foreground",
390
- )}
391
- >
392
- IN {log.inputTokens !== null ? formatTokens(log.inputTokens) : "—"}
393
- </span>
394
- <span className="text-muted-foreground">/</span>
395
- <span
396
- className={cn(
397
- "whitespace-nowrap",
398
- log.outputTokens !== null ? "text-amber-400" : "text-muted-foreground",
399
- )}
400
- >
401
- OUT {log.outputTokens !== null ? formatTokens(log.outputTokens) : "—"}
345
+ {/* Streaming timing */}
346
+ {log.streaming && firstChunkLabel !== null && (
347
+ <Tooltip>
348
+ <TooltipTrigger asChild>
349
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-cyan-300")}>
350
+ <Radio className="size-3" />
351
+ <span className="font-mono tabular-nums">first {firstChunkLabel}</span>
352
+ </span>
353
+ </TooltipTrigger>
354
+ <TooltipContent>Time to first streaming chunk</TooltipContent>
355
+ </Tooltip>
356
+ )}
357
+ {log.streaming && tokenRateLabel !== null && (
358
+ <Tooltip>
359
+ <TooltipTrigger asChild>
360
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-cyan-300")}>
361
+ <Gauge className="size-3" />
362
+ <span className="font-mono tabular-nums">{tokenRateLabel}</span>
363
+ </span>
364
+ </TooltipTrigger>
365
+ <TooltipContent>Output token throughput after first chunk</TooltipContent>
366
+ </Tooltip>
367
+ )}
368
+
369
+ {/* Token counts */}
370
+ {hasTokens && (
371
+ <span className={cn(TELEMETRY_CHIP_CLASS, "min-w-0 sm:shrink-0")}>
372
+ <Zap className="size-3 text-muted-foreground" />
373
+ <span className="flex min-w-0 flex-wrap items-center gap-x-1 font-mono tabular-nums">
374
+ <span
375
+ className={cn(
376
+ "whitespace-nowrap",
377
+ log.inputTokens !== null ? "text-blue-400" : "text-muted-foreground",
378
+ )}
379
+ >
380
+ IN {log.inputTokens !== null ? formatTokens(log.inputTokens) : "n/a"}
381
+ </span>
382
+ <span className="text-muted-foreground">/</span>
383
+ <span
384
+ className={cn(
385
+ "whitespace-nowrap",
386
+ log.outputTokens !== null ? "text-amber-400" : "text-muted-foreground",
387
+ )}
388
+ >
389
+ OUT {log.outputTokens !== null ? formatTokens(log.outputTokens) : "n/a"}
390
+ </span>
402
391
  </span>
403
392
  </span>
404
- </span>
405
- )}
406
- {/* Cache tokens */}
407
- {log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
408
- <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0")}>
409
- <CacheTrendIndicator trend={cacheTrend?.creation ?? null} />
410
- <span className="font-mono tabular-nums text-emerald-400">
411
- KV Cache +{formatTokens(log.cacheCreationInputTokens)}
393
+ )}
394
+ {/* Cache tokens */}
395
+ {log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
396
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0")}>
397
+ <CacheTrendIndicator trend={cacheTrend?.creation ?? null} />
398
+ <span className="font-mono tabular-nums text-cyan-300">
399
+ KV Cache +{formatTokens(log.cacheCreationInputTokens)}
400
+ </span>
412
401
  </span>
413
- </span>
414
- )}
415
- {log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
416
- <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0")}>
417
- <CacheTrendIndicator trend={cacheTrend?.read ?? null} />
418
- <span className="font-mono tabular-nums text-purple-400">
419
- KV Cache ~{formatTokens(log.cacheReadInputTokens)}
402
+ )}
403
+ {log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
404
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0")}>
405
+ <CacheTrendIndicator trend={cacheTrend?.read ?? null} />
406
+ <span className="font-mono tabular-nums text-purple-400">
407
+ KV Cache ~{formatTokens(log.cacheReadInputTokens)}
408
+ </span>
420
409
  </span>
421
- </span>
422
- )}
423
-
424
- {/* Message count */}
425
- {messageCount !== null && (
426
- <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
427
- <MessageSquare className="size-3" />
428
- <span className="font-mono tabular-nums">{messageCount}</span>
429
- </span>
430
- )}
410
+ )}
431
411
 
432
- {/* Tool count */}
433
- {toolCount !== null && (
434
- <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
435
- <Wrench className="size-3" />
436
- <span className="font-mono tabular-nums">{toolCount}</span>
437
- </span>
438
- )}
412
+ {/* Message count */}
413
+ {messageCount !== null && (
414
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
415
+ <MessageSquare className="size-3" />
416
+ <span className="font-mono tabular-nums">{messageCount}</span>
417
+ </span>
418
+ )}
439
419
 
440
- {/* Origin */}
441
- {log.origin !== null && (
442
- <span
443
- className={cn(TELEMETRY_CHIP_CLASS, "hidden shrink-0 text-muted-foreground xl:flex")}
444
- title={`Origin: ${log.origin}`}
445
- >
446
- <Globe className="size-3" />
447
- <span className="font-mono tabular-nums truncate max-w-[120px]" title={log.origin}>
448
- {log.origin}
420
+ {/* Tool count */}
421
+ {toolCount !== null && (
422
+ <span className={cn(TELEMETRY_CHIP_CLASS, "shrink-0 text-muted-foreground")}>
423
+ <Wrench className="size-3" />
424
+ <span className="font-mono tabular-nums">{toolCount}</span>
449
425
  </span>
450
- </span>
451
- )}
426
+ )}
452
427
 
453
- {/* Client info (PID + project folder) */}
454
- {(hasClientPid || hasClientProjectFolder) && (
455
- <Tooltip>
456
- <TooltipTrigger asChild>
457
- <span className={cn(TELEMETRY_CHIP_CLASS, "hidden shrink-0 text-purple-300 xl:flex")}>
458
- <FileTerminal className="size-3" />
459
- {hasClientProjectFolder ? (
460
- <span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
461
- ) : (
462
- <span className="font-mono tabular-nums">PID {log.clientPid}</span>
463
- )}
428
+ {/* Origin */}
429
+ {log.origin !== null && (
430
+ <span
431
+ className={cn(TELEMETRY_CHIP_CLASS, "hidden shrink-0 text-muted-foreground xl:flex")}
432
+ title={`Origin: ${log.origin}`}
433
+ >
434
+ <Globe className="size-3" />
435
+ <span className="font-mono tabular-nums truncate max-w-[120px]" title={log.origin}>
436
+ {log.origin}
464
437
  </span>
465
- </TooltipTrigger>
466
- <TooltipContent>
467
- {hasClientCwd
468
- ? `PID: ${log.clientPid ?? "?"} CWD: ${log.clientCwd}`
469
- : `Process ID: ${log.clientPid ?? "?"}`}
470
- </TooltipContent>
471
- </Tooltip>
472
- )}
438
+ </span>
439
+ )}
473
440
 
474
- {/* Streaming indicator */}
475
- {log.streaming && (
476
- <Tooltip>
477
- <TooltipTrigger asChild>
478
- <Radio className="size-3 text-muted-foreground/60 shrink-0" />
479
- </TooltipTrigger>
480
- <TooltipContent>Request used SSE streaming</TooltipContent>
481
- </Tooltip>
482
- )}
441
+ {/* Client info (PID + project folder) */}
442
+ {showClientMetadata && (hasClientPid || hasClientProjectFolder) && (
443
+ <Tooltip>
444
+ <TooltipTrigger asChild>
445
+ <span
446
+ className={cn(TELEMETRY_CHIP_CLASS, "hidden shrink-0 text-purple-300 xl:flex")}
447
+ >
448
+ <FileTerminal className="size-3" />
449
+ {hasClientProjectFolder ? (
450
+ <span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
451
+ ) : (
452
+ <span className="font-mono tabular-nums">PID {log.clientPid}</span>
453
+ )}
454
+ </span>
455
+ </TooltipTrigger>
456
+ <TooltipContent>
457
+ {hasClientCwd
458
+ ? `PID: ${log.clientPid ?? "?"} CWD: ${log.clientCwd}`
459
+ : `Process ID: ${log.clientPid ?? "?"}`}
460
+ </TooltipContent>
461
+ </Tooltip>
462
+ )}
483
463
 
484
- {/* Spacer */}
485
- <span className="flex-1 min-w-0" />
464
+ {/* Streaming indicator */}
465
+ {showStandaloneStreamingIndicator && (
466
+ <Tooltip>
467
+ <TooltipTrigger asChild>
468
+ <Radio className="size-3 text-muted-foreground/60 shrink-0" />
469
+ </TooltipTrigger>
470
+ <TooltipContent>Request used SSE streaming</TooltipContent>
471
+ </Tooltip>
472
+ )}
473
+ </span>
486
474
 
487
- {/* Header actions only when expanded, so the collapsed view stays
475
+ <span className="flex shrink-0 items-start gap-1.5">
476
+ {/* Header actions - only when expanded, so the collapsed view stays
488
477
  compact. Buttons stop propagation so they don't toggle the log. */}
489
- {expanded && (
478
+ {expanded && (
479
+ <span
480
+ className="flex items-center gap-1.5 shrink-0"
481
+ onClick={(e) => e.stopPropagation()}
482
+ onKeyDown={(e) => e.stopPropagation()}
483
+ >
484
+ {tabActions !== undefined &&
485
+ activeTab !== undefined &&
486
+ (() => {
487
+ const action = tabActions[activeTab];
488
+ if (action === undefined) return null;
489
+ return (
490
+ <>
491
+ {action.expansion !== null && (
492
+ <Tooltip>
493
+ <TooltipTrigger asChild>
494
+ <Button
495
+ variant="outline"
496
+ size="icon"
497
+ className={HEADER_ACTION_BUTTON_CLASS}
498
+ onClick={action.expansion.onToggle}
499
+ disabled={action.expansion.isPending}
500
+ aria-pressed={action.expansion.isExpanded}
501
+ aria-label={
502
+ action.expansion.isExpanded
503
+ ? "Collapse all JSON"
504
+ : "Expand all JSON"
505
+ }
506
+ >
507
+ {action.expansion.isExpanded ? (
508
+ <ChevronsUp className="size-3.5" />
509
+ ) : (
510
+ <ChevronsDown className="size-3.5" />
511
+ )}
512
+ </Button>
513
+ </TooltipTrigger>
514
+ <TooltipContent>
515
+ {action.expansion.isExpanded
516
+ ? "Collapse all JSON nodes"
517
+ : "Expand all JSON nodes"}
518
+ </TooltipContent>
519
+ </Tooltip>
520
+ )}
521
+ {action.diffWithRaw !== undefined && (
522
+ <Tooltip>
523
+ <TooltipTrigger asChild>
524
+ <Button
525
+ variant="outline"
526
+ size="icon"
527
+ className={cn(
528
+ HEADER_ACTION_BUTTON_CLASS,
529
+ action.diffWithRaw.active &&
530
+ "border-cyan-300/35 bg-cyan-400/15 text-cyan-100",
531
+ )}
532
+ onClick={action.diffWithRaw.onToggle}
533
+ aria-pressed={action.diffWithRaw.active}
534
+ aria-label={
535
+ action.diffWithRaw.active ? "Hide raw diff" : "Diff with raw"
536
+ }
537
+ >
538
+ <FileDiff className="size-3.5" />
539
+ </Button>
540
+ </TooltipTrigger>
541
+ <TooltipContent>
542
+ {action.diffWithRaw.active
543
+ ? "Hide diff with raw request"
544
+ : "Show diff between displayed and raw request body"}
545
+ </TooltipContent>
546
+ </Tooltip>
547
+ )}
548
+ {action.diffWithPrevious !== undefined && (
549
+ <Tooltip>
550
+ <TooltipTrigger asChild>
551
+ <Button
552
+ variant="outline"
553
+ size="icon"
554
+ className={HEADER_ACTION_BUTTON_CLASS}
555
+ onClick={action.diffWithPrevious}
556
+ aria-label="Diff with previous"
557
+ >
558
+ <History className="size-3.5" />
559
+ </Button>
560
+ </TooltipTrigger>
561
+ <TooltipContent>
562
+ Compare this request with the immediately preceding one
563
+ </TooltipContent>
564
+ </Tooltip>
565
+ )}
566
+ {action.copyText !== null && (
567
+ <Tooltip>
568
+ <TooltipTrigger asChild>
569
+ <Button
570
+ variant="outline"
571
+ size="icon"
572
+ className={HEADER_ACTION_BUTTON_CLASS}
573
+ onClick={action.onCopy}
574
+ aria-label={action.copyCopied ? "Copied" : action.copyLabel}
575
+ >
576
+ {action.copyCopied ? (
577
+ <Check className="size-3.5 text-cyan-300" />
578
+ ) : (
579
+ <Copy className="size-3.5" />
580
+ )}
581
+ </Button>
582
+ </TooltipTrigger>
583
+ <TooltipContent>
584
+ {action.copyCopied ? "Copied to clipboard" : action.copyLabel}
585
+ </TooltipContent>
586
+ </Tooltip>
587
+ )}
588
+ </>
589
+ );
590
+ })()}
591
+ {onReplay !== undefined && (
592
+ <Tooltip>
593
+ <TooltipTrigger asChild>
594
+ <Button
595
+ variant="outline"
596
+ size="icon"
597
+ className={HEADER_ACTION_BUTTON_CLASS}
598
+ onClick={onReplay}
599
+ aria-label="Replay request"
600
+ >
601
+ <RotateCcw className="size-3.5" />
602
+ </Button>
603
+ </TooltipTrigger>
604
+ <TooltipContent>Re-send this request to the provider</TooltipContent>
605
+ </Tooltip>
606
+ )}
607
+ </span>
608
+ )}
609
+
490
610
  <span
491
- className="flex items-center gap-1.5 shrink-0"
492
- onClick={(e) => e.stopPropagation()}
493
- onKeyDown={(e) => e.stopPropagation()}
611
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-md text-slate-200"
612
+ aria-hidden="true"
494
613
  >
495
- {tabActions !== undefined &&
496
- activeTab !== undefined &&
497
- (() => {
498
- const action = tabActions[activeTab];
499
- if (action === undefined) return null;
500
- return (
501
- <>
502
- {action.expansion !== null && (
503
- <Tooltip>
504
- <TooltipTrigger asChild>
505
- <Button
506
- variant="outline"
507
- size="icon"
508
- className={HEADER_ACTION_BUTTON_CLASS}
509
- onClick={action.expansion.onToggle}
510
- disabled={action.expansion.isPending}
511
- aria-pressed={action.expansion.isExpanded}
512
- aria-label={
513
- action.expansion.isExpanded ? "Collapse all JSON" : "Expand all JSON"
514
- }
515
- >
516
- {action.expansion.isExpanded ? (
517
- <ChevronsUp className="size-3.5" />
518
- ) : (
519
- <ChevronsDown className="size-3.5" />
520
- )}
521
- </Button>
522
- </TooltipTrigger>
523
- <TooltipContent>
524
- {action.expansion.isExpanded
525
- ? "Collapse all JSON nodes"
526
- : "Expand all JSON nodes"}
527
- </TooltipContent>
528
- </Tooltip>
529
- )}
530
- {action.diffWithRaw !== undefined && (
531
- <Tooltip>
532
- <TooltipTrigger asChild>
533
- <Button
534
- variant="outline"
535
- size="icon"
536
- className={cn(
537
- HEADER_ACTION_BUTTON_CLASS,
538
- action.diffWithRaw.active &&
539
- "border-cyan-300/35 bg-cyan-400/15 text-cyan-100",
540
- )}
541
- onClick={action.diffWithRaw.onToggle}
542
- aria-pressed={action.diffWithRaw.active}
543
- aria-label={
544
- action.diffWithRaw.active ? "Hide raw diff" : "Diff with raw"
545
- }
546
- >
547
- <FileDiff className="size-3.5" />
548
- </Button>
549
- </TooltipTrigger>
550
- <TooltipContent>
551
- {action.diffWithRaw.active
552
- ? "Hide diff with raw request"
553
- : "Show diff between displayed and raw request body"}
554
- </TooltipContent>
555
- </Tooltip>
556
- )}
557
- {action.diffWithPrevious !== undefined && (
558
- <Tooltip>
559
- <TooltipTrigger asChild>
560
- <Button
561
- variant="outline"
562
- size="icon"
563
- className={HEADER_ACTION_BUTTON_CLASS}
564
- onClick={action.diffWithPrevious}
565
- aria-label="Diff with previous"
566
- >
567
- <History className="size-3.5" />
568
- </Button>
569
- </TooltipTrigger>
570
- <TooltipContent>
571
- Compare this request with the immediately preceding one
572
- </TooltipContent>
573
- </Tooltip>
574
- )}
575
- {action.copyText !== null && (
576
- <Tooltip>
577
- <TooltipTrigger asChild>
578
- <Button
579
- variant="outline"
580
- size="icon"
581
- className={HEADER_ACTION_BUTTON_CLASS}
582
- onClick={action.onCopy}
583
- aria-label={action.copyCopied ? "Copied" : action.copyLabel}
584
- >
585
- {action.copyCopied ? (
586
- <Check className="size-3.5 text-emerald-500" />
587
- ) : (
588
- <Copy className="size-3.5" />
589
- )}
590
- </Button>
591
- </TooltipTrigger>
592
- <TooltipContent>
593
- {action.copyCopied ? "Copied to clipboard" : action.copyLabel}
594
- </TooltipContent>
595
- </Tooltip>
596
- )}
597
- </>
598
- );
599
- })()}
600
- {onReplay !== undefined && (
601
- <Tooltip>
602
- <TooltipTrigger asChild>
603
- <Button
604
- variant="outline"
605
- size="icon"
606
- className={HEADER_ACTION_BUTTON_CLASS}
607
- onClick={onReplay}
608
- aria-label="Replay request"
609
- >
610
- <RotateCcw className="size-3.5" />
611
- </Button>
612
- </TooltipTrigger>
613
- <TooltipContent>Re-send this request to the provider</TooltipContent>
614
- </Tooltip>
614
+ {expanded ? (
615
+ <ChevronDown className="size-4 shrink-0" />
616
+ ) : (
617
+ <ChevronRight className="size-4 shrink-0" />
615
618
  )}
616
619
  </span>
617
- )}
620
+ </span>
618
621
  </div>
619
622
  </TooltipProvider>
620
623
  );