@tonyclaw/llm-inspector 1.14.6 → 1.14.8

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 (31) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/index-CdnotuLh.js +105 -0
  3. package/.output/public/assets/index-vP91146S.css +1 -0
  4. package/.output/public/assets/{main-Dp5657Eq.js → main-CJ4MreBr.js} +1 -1
  5. package/.output/server/_libs/lucide-react.mjs +87 -79
  6. package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
  7. package/.output/server/_libs/radix-ui__react-tooltip.mjs +1 -1
  8. package/.output/server/_ssr/{index-D2yS8VvO.mjs → index-9uTJ4xYR.mjs} +813 -595
  9. package/.output/server/_ssr/index.mjs +2 -2
  10. package/.output/server/_ssr/{router-DCfjmmJu.mjs → router-BKnjB_zi.mjs} +2 -2
  11. package/.output/server/{_tanstack-start-manifest_v-DupqJc5d.mjs → _tanstack-start-manifest_v-IsglLVKy.mjs} +1 -1
  12. package/.output/server/index.mjs +26 -26
  13. package/package.json +1 -1
  14. package/src/components/ProxyViewer.tsx +114 -146
  15. package/src/components/providers/ImportWizardDialog.tsx +6 -0
  16. package/src/components/providers/ProviderCard.tsx +79 -26
  17. package/src/components/providers/ProviderForm.tsx +37 -22
  18. package/src/components/providers/ProvidersPanel.tsx +118 -58
  19. package/src/components/providers/SettingsDialog.tsx +25 -15
  20. package/src/components/proxy-viewer/ConversationGroup.tsx +50 -10
  21. package/src/components/proxy-viewer/ConversationHeader.tsx +48 -2
  22. package/src/components/proxy-viewer/LogEntry.tsx +116 -45
  23. package/src/components/proxy-viewer/LogEntryHeader.tsx +89 -71
  24. package/src/components/proxy-viewer/ReplayDialog.tsx +16 -6
  25. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +24 -16
  26. package/src/components/proxy-viewer/ThreadConnector.tsx +104 -0
  27. package/src/components/proxy-viewer/index.ts +2 -1
  28. package/src/components/ui/confirm-dialog.tsx +51 -0
  29. package/src/lib/stopReason.ts +57 -0
  30. package/.output/public/assets/index-BFNoWwFI.css +0 -1
  31. package/.output/public/assets/index-LH-YtFEM.js +0 -105
@@ -1,4 +1,12 @@
1
- import { ChevronDown, ChevronRight, Clock, MessageSquare, Zap } from "lucide-react";
1
+ import {
2
+ ChevronDown,
3
+ ChevronRight,
4
+ Clock,
5
+ GitBranch,
6
+ Loader2,
7
+ MessageSquare,
8
+ Zap,
9
+ } from "lucide-react";
2
10
  import type { JSX } from "react";
3
11
  import { cn, formatTokens } from "../../lib/utils";
4
12
  import type { CapturedLog } from "../../proxy/schemas";
@@ -10,6 +18,8 @@ const API_FORMAT_LABELS: Record<"anthropic" | "openai" | "unknown", string> = {
10
18
  unknown: "Unknown",
11
19
  };
12
20
 
21
+ export type ViewMode = "flat" | "thread";
22
+
13
23
  export type ConversationHeaderProps = {
14
24
  conversationId: string;
15
25
  startTime: string;
@@ -23,6 +33,13 @@ export type ConversationHeaderProps = {
23
33
  /** Hide the API format badge on the header (used when the group contains
24
34
  * mixed formats — the per-log badges are shown instead). */
25
35
  hideApiFormat?: boolean;
36
+ /** When true and the group is collapsed, show a spinner instead of the
37
+ * expand chevron to indicate an in-flight request inside the group. */
38
+ isLoading?: boolean;
39
+ /** Current display mode for this group (flat cards or threaded timeline). */
40
+ viewMode?: ViewMode;
41
+ /** Toggle between flat and thread display modes for this group. */
42
+ onToggleViewMode?: () => void;
26
43
  };
27
44
 
28
45
  function formatTimestamp(iso: string): string {
@@ -41,6 +58,9 @@ export function ConversationHeader({
41
58
  expanded,
42
59
  onToggle,
43
60
  hideApiFormat = false,
61
+ isLoading = false,
62
+ viewMode,
63
+ onToggleViewMode,
44
64
  }: ConversationHeaderProps): JSX.Element {
45
65
  return (
46
66
  <div
@@ -60,13 +80,39 @@ export function ConversationHeader({
60
80
  }
61
81
  }}
62
82
  >
63
- {/* Expand chevron */}
83
+ {/* Expand chevron — shows spinner when collapsed and group has pending logs */}
64
84
  {expanded ? (
65
85
  <ChevronDown className="size-4 text-muted-foreground shrink-0" />
86
+ ) : isLoading ? (
87
+ <Loader2 className="size-4 animate-spin text-muted-foreground shrink-0" />
66
88
  ) : (
67
89
  <ChevronRight className="size-4 text-muted-foreground shrink-0" />
68
90
  )}
69
91
 
92
+ {/* Thread/flat view toggle — only shown when expanded */}
93
+ {expanded && onToggleViewMode !== undefined && (
94
+ <button
95
+ type="button"
96
+ onClick={(e) => {
97
+ e.stopPropagation();
98
+ onToggleViewMode();
99
+ }}
100
+ className={cn(
101
+ "px-1.5 py-0.5 rounded text-[10px] font-mono transition-colors shrink-0 cursor-pointer",
102
+ viewMode === "thread"
103
+ ? "bg-amber-500/15 text-amber-400 border border-amber-500/30"
104
+ : "bg-muted text-muted-foreground border border-border hover:text-foreground",
105
+ )}
106
+ title={
107
+ viewMode === "thread"
108
+ ? "Thread view — click for flat view"
109
+ : "Flat view — click for thread view"
110
+ }
111
+ >
112
+ <GitBranch className="size-3" />
113
+ </button>
114
+ )}
115
+
70
116
  {/* Conversation ID */}
71
117
  <span
72
118
  className="text-purple-400/90 font-mono text-xs font-semibold shrink-0"
@@ -5,6 +5,7 @@ import { cn } from "../../lib/utils";
5
5
  import { type CapturedLog, parseRequest } from "../../proxy/schemas";
6
6
  import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
7
7
  import { Button } from "../ui/button";
8
+ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
8
9
  import { JsonViewerFromString } from "../ui/json-viewer";
9
10
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
10
11
  import { computeHeadersDiff, computeRequestDiff, DiffView } from "./diff";
@@ -31,10 +32,8 @@ export type LogEntryProps = {
31
32
  * `null` (or absent) means the header should render with no arrows.
32
33
  */
33
34
  cacheTrend?: CacheTrendEntry | null;
34
- /** Whether this log is currently marked for comparison. */
35
- isSelected?: boolean;
36
- /** Toggle this log in/out of the comparison selection. */
37
- onToggleSelect?: (logId: number) => void;
35
+ /** Callback to open CompareDrawer with this log and its immediate predecessor. */
36
+ onCompareWithPrevious?: () => void;
38
37
  };
39
38
 
40
39
  /**
@@ -118,21 +117,29 @@ function DiffToggleButton({
118
117
  onClick: (e: React.MouseEvent) => void;
119
118
  }): JSX.Element {
120
119
  return (
121
- <button
122
- type="button"
123
- onClick={onClick}
124
- aria-pressed={active}
125
- className={cn(
126
- "flex items-center gap-1.5 text-xs px-2 py-1 rounded transition-colors",
127
- active
128
- ? "bg-primary/10 text-primary"
129
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
130
- )}
131
- title={active ? "Hide diff with raw" : "Diff this view against the raw version"}
132
- >
133
- <GitCompareArrows className="size-3" />
134
- {active ? "Showing diff" : "Diff with Raw"}
135
- </button>
120
+ <TooltipProvider>
121
+ <Tooltip>
122
+ <TooltipTrigger asChild>
123
+ <button
124
+ type="button"
125
+ onClick={onClick}
126
+ aria-pressed={active}
127
+ className={cn(
128
+ "flex items-center gap-1.5 text-xs px-2 py-1 rounded transition-colors",
129
+ active
130
+ ? "bg-primary/10 text-primary"
131
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
132
+ )}
133
+ >
134
+ <GitCompareArrows className="size-3" />
135
+ {active ? "Showing diff" : "Diff with Raw"}
136
+ </button>
137
+ </TooltipTrigger>
138
+ <TooltipContent>
139
+ {active ? "Hide diff view" : "Compare proxy output against the original raw version"}
140
+ </TooltipContent>
141
+ </Tooltip>
142
+ </TooltipProvider>
136
143
  );
137
144
  }
138
145
 
@@ -142,8 +149,7 @@ export const LogEntry = memo(function ({
142
149
  suppressApiFormatBadge = false,
143
150
  strip,
144
151
  cacheTrend = null,
145
- isSelected = false,
146
- onToggleSelect,
152
+ onCompareWithPrevious,
147
153
  }: LogEntryProps): JSX.Element {
148
154
  const [expanded, setExpanded] = useState<boolean>(false);
149
155
  const [requestCopied, setRequestCopied] = useState<boolean>(false);
@@ -199,12 +205,7 @@ export const LogEntry = memo(function ({
199
205
 
200
206
  return (
201
207
  <>
202
- <div
203
- className={cn(
204
- "border border-border rounded-lg mb-3 overflow-hidden",
205
- isSelected && "border-l-2 border-l-amber-400",
206
- )}
207
- >
208
+ <div className="border border-border rounded-lg mb-3 overflow-hidden">
208
209
  <LogEntryHeader
209
210
  log={log}
210
211
  parsedRequest={parsedRequest}
@@ -212,21 +213,61 @@ export const LogEntry = memo(function ({
212
213
  onToggle={() => setExpanded(!expanded)}
213
214
  suppressApiFormatBadge={suppressApiFormatBadge}
214
215
  cacheTrend={cacheTrend}
215
- isSelected={isSelected}
216
- onToggleSelect={onToggleSelect}
217
216
  />
218
217
 
219
218
  {expanded && (
220
219
  <div onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
221
220
  <Tabs defaultValue="request">
222
221
  <TabsList className="mx-4 mt-2">
223
- {viewMode === "full" && <TabsTrigger value="raw-headers">Raw Headers</TabsTrigger>}
224
- {viewMode === "full" && <TabsTrigger value="headers">Headers</TabsTrigger>}
222
+ {viewMode === "full" && (
223
+ <TooltipProvider>
224
+ <Tooltip>
225
+ <TooltipTrigger asChild>
226
+ <TabsTrigger value="raw-headers">Raw Headers</TabsTrigger>
227
+ </TooltipTrigger>
228
+ <TooltipContent>
229
+ HTTP headers received from the upstream provider
230
+ </TooltipContent>
231
+ </Tooltip>
232
+ </TooltipProvider>
233
+ )}
234
+ {viewMode === "full" && (
235
+ <TooltipProvider>
236
+ <Tooltip>
237
+ <TooltipTrigger asChild>
238
+ <TabsTrigger value="headers">Headers</TabsTrigger>
239
+ </TooltipTrigger>
240
+ <TooltipContent>
241
+ Request and response headers sent and received
242
+ </TooltipContent>
243
+ </Tooltip>
244
+ </TooltipProvider>
245
+ )}
225
246
  {shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && (
226
- <TabsTrigger value="raw-request">Raw Request</TabsTrigger>
247
+ <TooltipProvider>
248
+ <Tooltip>
249
+ <TooltipTrigger asChild>
250
+ <TabsTrigger value="raw-request">Raw Request</TabsTrigger>
251
+ </TooltipTrigger>
252
+ <TooltipContent>
253
+ Exact HTTP request sent to the upstream provider
254
+ </TooltipContent>
255
+ </Tooltip>
256
+ </TooltipProvider>
227
257
  )}
228
258
  <TabsTrigger value="request">Request</TabsTrigger>
229
- {viewMode === "full" && <TabsTrigger value="raw">Raw Response</TabsTrigger>}
259
+ {viewMode === "full" && (
260
+ <TooltipProvider>
261
+ <Tooltip>
262
+ <TooltipTrigger asChild>
263
+ <TabsTrigger value="raw">Raw Response</TabsTrigger>
264
+ </TooltipTrigger>
265
+ <TooltipContent>
266
+ Exact HTTP response from the upstream provider
267
+ </TooltipContent>
268
+ </Tooltip>
269
+ </TooltipProvider>
270
+ )}
230
271
  <TabsTrigger value="parsed">Response</TabsTrigger>
231
272
  </TabsList>
232
273
 
@@ -267,18 +308,48 @@ export const LogEntry = memo(function ({
267
308
  }}
268
309
  />
269
310
  )}
270
- <Button
271
- variant="outline"
272
- size="sm"
273
- className="h-7 text-xs"
274
- onClick={(e) => {
275
- e.stopPropagation();
276
- setReplayOpen(true);
277
- }}
278
- >
279
- <RotateCcw className="size-3 mr-1" />
280
- Replay
281
- </Button>
311
+ {onCompareWithPrevious !== undefined && (
312
+ <TooltipProvider>
313
+ <Tooltip>
314
+ <TooltipTrigger asChild>
315
+ <Button
316
+ variant="outline"
317
+ size="sm"
318
+ className="h-7 text-xs"
319
+ onClick={(e) => {
320
+ e.stopPropagation();
321
+ onCompareWithPrevious();
322
+ }}
323
+ >
324
+ <GitCompareArrows className="size-3 mr-1" />
325
+ Diff with Previous
326
+ </Button>
327
+ </TooltipTrigger>
328
+ <TooltipContent>
329
+ Compare this request with the immediately preceding one
330
+ </TooltipContent>
331
+ </Tooltip>
332
+ </TooltipProvider>
333
+ )}
334
+ <TooltipProvider>
335
+ <Tooltip>
336
+ <TooltipTrigger asChild>
337
+ <Button
338
+ variant="outline"
339
+ size="sm"
340
+ className="h-7 text-xs"
341
+ onClick={(e) => {
342
+ e.stopPropagation();
343
+ setReplayOpen(true);
344
+ }}
345
+ >
346
+ <RotateCcw className="size-3 mr-1" />
347
+ Replay
348
+ </Button>
349
+ </TooltipTrigger>
350
+ <TooltipContent>Re-send this request to the provider</TooltipContent>
351
+ </Tooltip>
352
+ </TooltipProvider>
282
353
  <CopyButton
283
354
  text={displayedRequestBody}
284
355
  label="Copy Request"
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  ArrowDown,
3
3
  ArrowUp,
4
- Check,
5
4
  ChevronDown,
6
5
  ChevronRight,
7
6
  Clock,
@@ -20,6 +19,7 @@ import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../
20
19
  import type { CapturedLog, InspectorRequest } from "../../proxy/schemas";
21
20
  import { Badge } from "../ui/badge";
22
21
  import { ProviderLogo, detectProvider } from "../providers/ProviderLogo";
22
+ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
23
23
  import type { CacheTrend } from "./cacheTrend";
24
24
 
25
25
  function formatElapsed(ms: number): string {
@@ -67,10 +67,6 @@ export type LogEntryHeaderProps = {
67
67
  * the corresponding cache span renders as it did before — no arrow.
68
68
  */
69
69
  cacheTrend?: { creation: CacheTrend | null; read: CacheTrend | null } | null;
70
- /** Whether this log is currently marked for comparison. */
71
- isSelected?: boolean;
72
- /** Toggle this log in/out of the comparison selection. */
73
- onToggleSelect?: (logId: number) => void;
74
70
  };
75
71
 
76
72
  export const LogEntryHeader = memo(function ({
@@ -80,8 +76,6 @@ export const LogEntryHeader = memo(function ({
80
76
  onToggle,
81
77
  suppressApiFormatBadge = false,
82
78
  cacheTrend = null,
83
- isSelected = false,
84
- onToggleSelect,
85
79
  }: LogEntryHeaderProps): JSX.Element {
86
80
  const statusCategory = getStatusCategory(log.responseStatus);
87
81
 
@@ -111,40 +105,23 @@ export const LogEntryHeader = memo(function ({
111
105
  }
112
106
  }}
113
107
  >
114
- {/* Selection checkbox (for log-request comparison) */}
115
- {onToggleSelect !== undefined && (
116
- <button
117
- type="button"
118
- onClick={(e) => {
119
- e.stopPropagation();
120
- onToggleSelect(log.id);
121
- }}
122
- aria-label={isSelected ? "Deselect for comparison" : "Select for comparison"}
123
- aria-pressed={isSelected}
124
- className={cn(
125
- "shrink-0 size-4 rounded-sm border flex items-center justify-center transition-colors cursor-pointer",
126
- isSelected
127
- ? "bg-amber-400 border-amber-400 text-amber-950"
128
- : "border-muted-foreground/40 hover:border-amber-400 hover:bg-amber-400/10",
129
- )}
130
- >
131
- {isSelected && <Check className="size-3" strokeWidth={3} />}
132
- </button>
133
- )}
134
-
135
108
  {/* Request ID */}
136
109
  <span className="text-blue-400/80 font-mono text-xs font-semibold tabular-nums shrink-0">
137
110
  #{log.id}
138
111
  </span>
139
112
 
140
- {/* Model */}
113
+ {/* Model — logo icon only, model name in tooltip */}
141
114
  {log.model !== null && (
142
- <>
143
- <ProviderLogo provider={detectProvider(log.model)} className="size-4 shrink-0" />
144
- <Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 font-mono">
145
- {log.model}
146
- </Badge>
147
- </>
115
+ <TooltipProvider>
116
+ <Tooltip>
117
+ <TooltipTrigger asChild>
118
+ <span className="shrink-0">
119
+ <ProviderLogo provider={detectProvider(log.model)} className="size-4" />
120
+ </span>
121
+ </TooltipTrigger>
122
+ <TooltipContent>{log.model}</TooltipContent>
123
+ </Tooltip>
124
+ </TooltipProvider>
148
125
  )}
149
126
 
150
127
  {/* API Format Badge */}
@@ -220,36 +197,64 @@ export const LogEntryHeader = memo(function ({
220
197
  )}
221
198
  {/* Cache tokens */}
222
199
  {log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
223
- <span className="flex items-center gap-1 text-xs shrink-0">
224
- <CacheTrendIndicator trend={cacheTrend?.creation ?? null} />
225
- <span className="font-mono tabular-nums text-emerald-400">
226
- Cache +{formatTokens(log.cacheCreationInputTokens)}
227
- </span>
228
- </span>
200
+ <TooltipProvider>
201
+ <Tooltip>
202
+ <TooltipTrigger asChild>
203
+ <span className="flex items-center gap-1 text-xs shrink-0">
204
+ <CacheTrendIndicator trend={cacheTrend?.creation ?? null} />
205
+ <span className="font-mono tabular-nums text-emerald-400">
206
+ Cache +{formatTokens(log.cacheCreationInputTokens)}
207
+ </span>
208
+ </span>
209
+ </TooltipTrigger>
210
+ <TooltipContent>Tokens cached for reuse, reducing future API cost</TooltipContent>
211
+ </Tooltip>
212
+ </TooltipProvider>
229
213
  )}
230
214
  {log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
231
- <span className="flex items-center gap-1 text-xs shrink-0">
232
- <CacheTrendIndicator trend={cacheTrend?.read ?? null} />
233
- <span className="font-mono tabular-nums text-purple-400">
234
- Cache ~{formatTokens(log.cacheReadInputTokens)}
235
- </span>
236
- </span>
215
+ <TooltipProvider>
216
+ <Tooltip>
217
+ <TooltipTrigger asChild>
218
+ <span className="flex items-center gap-1 text-xs shrink-0">
219
+ <CacheTrendIndicator trend={cacheTrend?.read ?? null} />
220
+ <span className="font-mono tabular-nums text-purple-400">
221
+ Cache ~{formatTokens(log.cacheReadInputTokens)}
222
+ </span>
223
+ </span>
224
+ </TooltipTrigger>
225
+ <TooltipContent>Tokens served from cache, reducing API cost</TooltipContent>
226
+ </Tooltip>
227
+ </TooltipProvider>
237
228
  )}
238
229
 
239
230
  {/* Message count */}
240
231
  {messageCount !== null && (
241
- <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
242
- <MessageSquare className="size-3" />
243
- <span className="font-mono tabular-nums">{messageCount}</span>
244
- </span>
232
+ <TooltipProvider>
233
+ <Tooltip>
234
+ <TooltipTrigger asChild>
235
+ <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
236
+ <MessageSquare className="size-3" />
237
+ <span className="font-mono tabular-nums">{messageCount}</span>
238
+ </span>
239
+ </TooltipTrigger>
240
+ <TooltipContent>Number of messages in the conversation</TooltipContent>
241
+ </Tooltip>
242
+ </TooltipProvider>
245
243
  )}
246
244
 
247
245
  {/* Tool count */}
248
246
  {toolCount !== null && (
249
- <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
250
- <Wrench className="size-3" />
251
- <span className="font-mono tabular-nums">{toolCount}</span>
252
- </span>
247
+ <TooltipProvider>
248
+ <Tooltip>
249
+ <TooltipTrigger asChild>
250
+ <span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
251
+ <Wrench className="size-3" />
252
+ <span className="font-mono tabular-nums">{toolCount}</span>
253
+ </span>
254
+ </TooltipTrigger>
255
+ <TooltipContent>Number of tools defined in the request</TooltipContent>
256
+ </Tooltip>
257
+ </TooltipProvider>
253
258
  )}
254
259
 
255
260
  {/* Origin */}
@@ -280,25 +285,38 @@ export const LogEntryHeader = memo(function ({
280
285
 
281
286
  {/* Client info (PID + project folder) */}
282
287
  {(log.clientPid !== null || log.clientProjectFolder !== null) && (
283
- <span
284
- className="flex items-center gap-1 text-purple-400/80 text-xs shrink-0"
285
- title={
286
- log.clientCwd !== null
287
- ? `PID: ${log.clientPid ?? "?"} | CWD: ${log.clientCwd}`
288
- : `PID: ${log.clientPid ?? "?"}`
289
- }
290
- >
291
- <FileTerminal className="size-3" />
292
- {log.clientProjectFolder !== null ? (
293
- <span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
294
- ) : (
295
- <span className="font-mono tabular-nums">PID {log.clientPid}</span>
296
- )}
297
- </span>
288
+ <TooltipProvider>
289
+ <Tooltip>
290
+ <TooltipTrigger asChild>
291
+ <span className="flex items-center gap-1 text-purple-400/80 text-xs shrink-0">
292
+ <FileTerminal className="size-3" />
293
+ {log.clientProjectFolder !== null ? (
294
+ <span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
295
+ ) : (
296
+ <span className="font-mono tabular-nums">PID {log.clientPid}</span>
297
+ )}
298
+ </span>
299
+ </TooltipTrigger>
300
+ <TooltipContent>
301
+ {log.clientCwd !== null
302
+ ? `PID: ${log.clientPid ?? "?"} CWD: ${log.clientCwd}`
303
+ : `Process ID: ${log.clientPid ?? "?"}`}
304
+ </TooltipContent>
305
+ </Tooltip>
306
+ </TooltipProvider>
298
307
  )}
299
308
 
300
309
  {/* Streaming indicator */}
301
- {log.streaming && <Radio className="size-3 text-muted-foreground/60 shrink-0" />}
310
+ {log.streaming && (
311
+ <TooltipProvider>
312
+ <Tooltip>
313
+ <TooltipTrigger asChild>
314
+ <Radio className="size-3 text-muted-foreground/60 shrink-0" />
315
+ </TooltipTrigger>
316
+ <TooltipContent>Request used SSE streaming</TooltipContent>
317
+ </Tooltip>
318
+ </TooltipProvider>
319
+ )}
302
320
 
303
321
  {/* Spacer */}
304
322
  <span className="flex-1 min-w-0" />
@@ -3,6 +3,7 @@ import type { JSX } from "react";
3
3
  import { useState } from "react";
4
4
  import { z } from "zod";
5
5
  import { Button } from "../ui/button";
6
+ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
6
7
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
7
8
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
8
9
  import { ResponseView } from "./ResponseView";
@@ -92,12 +93,21 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
92
93
  <TabsContent value="modified" className="space-y-4">
93
94
  <div>
94
95
  <label className="text-sm font-medium mb-2 block">Request Body (JSON)</label>
95
- <textarea
96
- className="w-full h-64 p-3 font-mono text-xs bg-muted rounded-md border border-input resize-none focus:outline-none focus:ring-2 focus:ring-ring"
97
- value={modifiedBody}
98
- onChange={(e) => setModifiedBody(e.target.value)}
99
- spellCheck={false}
100
- />
96
+ <TooltipProvider>
97
+ <Tooltip>
98
+ <TooltipTrigger asChild>
99
+ <textarea
100
+ className="w-full h-64 p-3 font-mono text-xs bg-muted rounded-md border border-input resize-none focus:outline-none focus:ring-2 focus:ring-ring"
101
+ value={modifiedBody}
102
+ onChange={(e) => setModifiedBody(e.target.value)}
103
+ spellCheck={false}
104
+ />
105
+ </TooltipTrigger>
106
+ <TooltipContent>
107
+ Edit the request body before re-sending to the provider
108
+ </TooltipContent>
109
+ </Tooltip>
110
+ </TooltipProvider>
101
111
  </div>
102
112
 
103
113
  {error !== null && error !== "" && (
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, type JSX } from "react";
2
2
  import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
3
3
  import { Badge } from "../ui/badge";
4
4
  import { JsonViewer } from "../ui/json-viewer";
5
+ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
5
6
  import type { StreamingChunk } from "../../proxy/schemas";
6
7
 
7
8
  export type StreamingChunkSequenceProps = {
@@ -153,22 +154,29 @@ export function StreamingChunkSequence({
153
154
 
154
155
  return (
155
156
  <div className="space-y-1">
156
- <button
157
- type="button"
158
- className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
159
- onClick={() => setContainerExpanded((v) => !v)}
160
- >
161
- {containerExpanded ? (
162
- <ChevronDown className="size-3" />
163
- ) : (
164
- <ChevronRight className="size-3" />
165
- )}
166
- <span>Raw SSE Events</span>
167
- <Badge variant="outline" className="text-[9px] px-1 py-0 h-4 font-mono ml-1">
168
- {logId}
169
- {truncated === true ? "+" : ""}
170
- </Badge>
171
- </button>
157
+ <TooltipProvider>
158
+ <Tooltip>
159
+ <TooltipTrigger asChild>
160
+ <button
161
+ type="button"
162
+ className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
163
+ onClick={() => setContainerExpanded((v) => !v)}
164
+ >
165
+ {containerExpanded ? (
166
+ <ChevronDown className="size-3" />
167
+ ) : (
168
+ <ChevronRight className="size-3" />
169
+ )}
170
+ <span>Raw SSE Events</span>
171
+ <Badge variant="outline" className="text-[9px] px-1 py-0 h-4 font-mono ml-1">
172
+ {logId}
173
+ {truncated === true ? "+" : ""}
174
+ </Badge>
175
+ </button>
176
+ </TooltipTrigger>
177
+ <TooltipContent>Server-Sent Events streaming chunks from the provider</TooltipContent>
178
+ </Tooltip>
179
+ </TooltipProvider>
172
180
 
173
181
  {containerExpanded === true ? (
174
182
  <div className="rounded-md border border-border bg-muted/20 overflow-auto max-h-64">