@ai-sdk/devtools 1.0.0-beta.10 → 1.0.0-beta.11

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.
@@ -0,0 +1,691 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ ChevronRight,
4
+ Copy,
5
+ Check,
6
+ Wrench,
7
+ MessageSquare,
8
+ Brain,
9
+ Settings,
10
+ BarChart3,
11
+ } from 'lucide-react';
12
+ import {
13
+ Drawer,
14
+ DrawerContent,
15
+ DrawerHeader,
16
+ DrawerTitle,
17
+ DrawerTrigger,
18
+ } from '@/components/ui/drawer';
19
+ import type {
20
+ InputTokenBreakdown,
21
+ OutputTokenBreakdown,
22
+ ParsedInput,
23
+ ParsedUsage,
24
+ ToolDefinition,
25
+ } from '../types';
26
+ import {
27
+ safeParseJson,
28
+ formatToolParams,
29
+ getInputTokenBreakdown,
30
+ getOutputTokenBreakdown,
31
+ } from '../utils';
32
+
33
+ export function JsonBlock({
34
+ data,
35
+ compact = false,
36
+ size = 'sm',
37
+ }: {
38
+ data: unknown;
39
+ compact?: boolean;
40
+ size?: 'sm' | 'base' | 'lg';
41
+ }) {
42
+ const [copied, setCopied] = useState(false);
43
+
44
+ const jsonString = JSON.stringify(data, null, 2);
45
+ const displayString =
46
+ compact && jsonString.length > 200 ? JSON.stringify(data) : jsonString;
47
+
48
+ const sizeClasses = {
49
+ sm: 'text-xs',
50
+ base: 'text-sm',
51
+ lg: 'text-base',
52
+ };
53
+
54
+ const handleCopy = async () => {
55
+ await navigator.clipboard.writeText(jsonString);
56
+ setCopied(true);
57
+ setTimeout(() => setCopied(false), 2000);
58
+ };
59
+
60
+ return (
61
+ <div className="relative group">
62
+ <button
63
+ onClick={handleCopy}
64
+ className="absolute top-1.5 right-1.5 p-1.5 rounded-md border border-border bg-background opacity-0 group-hover:opacity-100 transition-opacity z-10"
65
+ title="Copy to clipboard"
66
+ >
67
+ {copied ? (
68
+ <Check className="size-3 text-success" />
69
+ ) : (
70
+ <Copy className="size-3 text-muted-foreground" />
71
+ )}
72
+ </button>
73
+ <pre
74
+ className={`font-mono ${sizeClasses[size]} text-muted-foreground whitespace-pre-wrap wrap-break-words bg-background rounded p-2 ${
75
+ compact ? 'max-h-20 overflow-hidden' : ''
76
+ }`}
77
+ >
78
+ {displayString}
79
+ </pre>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ export function RawDataSection({
85
+ rawRequest,
86
+ rawResponse,
87
+ rawChunks,
88
+ isStream,
89
+ }: {
90
+ rawRequest: string | null;
91
+ rawResponse: string | null;
92
+ rawChunks: string | null;
93
+ isStream: boolean;
94
+ }) {
95
+ const [expanded, setExpanded] = useState(false);
96
+ const [responseView, setResponseView] = useState<'parsed' | 'raw'>('parsed');
97
+
98
+ if (!rawRequest && !rawResponse) return null;
99
+
100
+ const hasRawChunks = isStream && rawChunks;
101
+
102
+ return (
103
+ <div className="border-t border-border">
104
+ <button
105
+ className="w-full flex items-center gap-2 px-4 py-2.5 text-[11px] text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
106
+ onClick={() => setExpanded(!expanded)}
107
+ >
108
+ <ChevronRight
109
+ className={`size-3 transition-transform ${expanded ? 'rotate-90' : ''}`}
110
+ />
111
+ <span className="font-medium uppercase tracking-wider">
112
+ Request / Response
113
+ </span>
114
+ </button>
115
+
116
+ {expanded && (
117
+ <div className="px-4 pb-4 grid grid-cols-2 gap-4">
118
+ {rawRequest && (
119
+ <div className="flex flex-col">
120
+ <div className="h-7 flex items-end mb-2">
121
+ <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
122
+ Request
123
+ </span>
124
+ </div>
125
+ <div className="max-h-[400px] overflow-auto rounded-md border border-border">
126
+ <JsonBlock data={safeParseJson(rawRequest)} />
127
+ </div>
128
+ </div>
129
+ )}
130
+ {(rawResponse || rawChunks) && (
131
+ <div className="flex flex-col">
132
+ <div className="h-7 flex items-end justify-between mb-2">
133
+ <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
134
+ {isStream ? 'Stream' : 'Response'}
135
+ </span>
136
+ {hasRawChunks && (
137
+ <div className="inline-flex rounded-md border border-border bg-muted/30 p-0.5">
138
+ <button
139
+ onClick={() => setResponseView('parsed')}
140
+ className={`px-2 py-0.5 text-[10px] font-medium rounded transition-colors ${
141
+ responseView === 'parsed'
142
+ ? 'bg-background text-foreground shadow-sm'
143
+ : 'text-muted-foreground hover:text-foreground'
144
+ }`}
145
+ >
146
+ AI SDK
147
+ </button>
148
+ <button
149
+ onClick={() => setResponseView('raw')}
150
+ className={`px-2 py-0.5 text-[10px] font-medium rounded transition-colors ${
151
+ responseView === 'raw'
152
+ ? 'bg-background text-foreground shadow-sm'
153
+ : 'text-muted-foreground hover:text-foreground'
154
+ }`}
155
+ >
156
+ Provider
157
+ </button>
158
+ </div>
159
+ )}
160
+ </div>
161
+ <div className="max-h-[400px] overflow-auto rounded-md border border-border">
162
+ <JsonBlock
163
+ data={safeParseJson(
164
+ hasRawChunks && responseView === 'raw'
165
+ ? rawChunks
166
+ : rawResponse,
167
+ )}
168
+ />
169
+ </div>
170
+ </div>
171
+ )}
172
+ </div>
173
+ )}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ export function UsageDetails({ usage }: { usage: ParsedUsage }) {
179
+ const inputBreakdown = getInputTokenBreakdown(usage?.inputTokens);
180
+ const outputBreakdown = getOutputTokenBreakdown(usage?.outputTokens);
181
+
182
+ return (
183
+ <div className="space-y-6">
184
+ <div className="grid grid-cols-2 gap-4">
185
+ <div className="rounded-lg border border-border bg-card p-4">
186
+ <div className="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">
187
+ Input Tokens
188
+ </div>
189
+ <div className="text-2xl font-semibold">{inputBreakdown.total}</div>
190
+ {(inputBreakdown.cacheRead !== undefined ||
191
+ inputBreakdown.cacheWrite !== undefined) && (
192
+ <div className="mt-3 space-y-1 text-sm text-muted-foreground">
193
+ {inputBreakdown.cacheRead !== undefined && (
194
+ <div className="flex justify-between">
195
+ <span>Cache read</span>
196
+ <span className="font-mono">{inputBreakdown.cacheRead}</span>
197
+ </div>
198
+ )}
199
+ {inputBreakdown.cacheWrite !== undefined && (
200
+ <div className="flex justify-between">
201
+ <span>Cache write</span>
202
+ <span className="font-mono">{inputBreakdown.cacheWrite}</span>
203
+ </div>
204
+ )}
205
+ {inputBreakdown.noCache !== undefined && (
206
+ <div className="flex justify-between">
207
+ <span>No cache</span>
208
+ <span className="font-mono">{inputBreakdown.noCache}</span>
209
+ </div>
210
+ )}
211
+ </div>
212
+ )}
213
+ </div>
214
+
215
+ <div className="rounded-lg border border-border bg-card p-4">
216
+ <div className="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">
217
+ Output Tokens
218
+ </div>
219
+ <div className="text-2xl font-semibold">{outputBreakdown.total}</div>
220
+ {(outputBreakdown.text !== undefined ||
221
+ outputBreakdown.reasoning !== undefined) && (
222
+ <div className="mt-3 space-y-1 text-sm text-muted-foreground">
223
+ {outputBreakdown.text !== undefined && (
224
+ <div className="flex justify-between">
225
+ <span>Text</span>
226
+ <span className="font-mono">{outputBreakdown.text}</span>
227
+ </div>
228
+ )}
229
+ {outputBreakdown.reasoning !== undefined && (
230
+ <div className="flex justify-between">
231
+ <span>Reasoning</span>
232
+ <span className="font-mono">{outputBreakdown.reasoning}</span>
233
+ </div>
234
+ )}
235
+ </div>
236
+ )}
237
+ </div>
238
+ </div>
239
+
240
+ {usage?.raw != null && (
241
+ <div>
242
+ <div className="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">
243
+ Raw Provider Usage
244
+ </div>
245
+ <JsonBlock data={usage.raw} size="sm" />
246
+ </div>
247
+ )}
248
+
249
+ <div>
250
+ <div className="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">
251
+ Full Usage Object
252
+ </div>
253
+ <JsonBlock data={usage} size="sm" />
254
+ </div>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ export function TokenBreakdownTooltip({
260
+ input,
261
+ output,
262
+ raw,
263
+ }: {
264
+ input: InputTokenBreakdown;
265
+ output: OutputTokenBreakdown;
266
+ raw?: unknown;
267
+ }) {
268
+ const hasInputBreakdown =
269
+ input.noCache !== undefined ||
270
+ input.cacheRead !== undefined ||
271
+ input.cacheWrite !== undefined;
272
+ const hasOutputBreakdown =
273
+ output.text !== undefined || output.reasoning !== undefined;
274
+ const hasBreakdown = hasInputBreakdown || hasOutputBreakdown;
275
+
276
+ if (!hasBreakdown) {
277
+ return (
278
+ <div className="text-xs">
279
+ <div>Input: {input.total}</div>
280
+ <div>Output: {output.total}</div>
281
+ </div>
282
+ );
283
+ }
284
+
285
+ return (
286
+ <div className="text-xs space-y-2">
287
+ <div>
288
+ <div className="font-medium mb-1">Input: {input.total}</div>
289
+ {input.cacheRead !== undefined && (
290
+ <div className="text-muted-foreground ml-2">
291
+ Cache read: {input.cacheRead}
292
+ </div>
293
+ )}
294
+ {input.cacheWrite !== undefined && (
295
+ <div className="text-muted-foreground ml-2">
296
+ Cache write: {input.cacheWrite}
297
+ </div>
298
+ )}
299
+ </div>
300
+ <div>
301
+ <div className="font-medium mb-1">Output: {output.total}</div>
302
+ {output.text !== undefined && (
303
+ <div className="text-muted-foreground ml-2">Text: {output.text}</div>
304
+ )}
305
+ {output.reasoning !== undefined && (
306
+ <div className="text-muted-foreground ml-2">
307
+ Reasoning: {output.reasoning}
308
+ </div>
309
+ )}
310
+ </div>
311
+ {raw !== undefined && (
312
+ <div className="pt-1 border-t border-border mt-1">
313
+ <div className="text-muted-foreground/70 font-mono text-[10px] max-w-[200px] truncate">
314
+ Raw: {JSON.stringify(raw)}
315
+ </div>
316
+ </div>
317
+ )}
318
+ </div>
319
+ );
320
+ }
321
+
322
+ export function ReasoningBlock({ content }: { content: string }) {
323
+ const [expanded, setExpanded] = useState(false);
324
+
325
+ const previewContent =
326
+ content.length > 200 ? content.slice(0, 200) + '…' : content;
327
+
328
+ return (
329
+ <div className="rounded-md border border-amber-500/30 overflow-hidden">
330
+ <button
331
+ className="w-full flex items-center gap-2 px-3 py-2 bg-amber-500/10 hover:bg-amber-500/20 transition-colors"
332
+ onClick={() => setExpanded(!expanded)}
333
+ >
334
+ <ChevronRight
335
+ className={`size-3 text-amber-500 transition-transform shrink-0 ${
336
+ expanded ? 'rotate-90' : ''
337
+ }`}
338
+ />
339
+ <Brain className="size-3 text-amber-500 shrink-0" />
340
+ <span className="text-xs font-medium text-amber-500">Thinking</span>
341
+ {!expanded && (
342
+ <span className="text-[11px] text-amber-500/70 truncate ml-1">
343
+ {previewContent}
344
+ </span>
345
+ )}
346
+ </button>
347
+
348
+ {expanded && (
349
+ <div className="p-3 bg-card/50 border-t border-amber-500/30">
350
+ <div className="text-xs text-foreground/80 leading-relaxed whitespace-pre-wrap">
351
+ {content}
352
+ </div>
353
+ </div>
354
+ )}
355
+ </div>
356
+ );
357
+ }
358
+
359
+ export function TextBlock({
360
+ content,
361
+ defaultExpanded = false,
362
+ isSystem = false,
363
+ }: {
364
+ content: string;
365
+ defaultExpanded?: boolean;
366
+ isSystem?: boolean;
367
+ }) {
368
+ const [expanded, setExpanded] = useState(defaultExpanded);
369
+ const [copied, setCopied] = useState(false);
370
+
371
+ const previewContent =
372
+ content.length > 200 ? content.slice(0, 200) + '…' : content;
373
+
374
+ const borderColor = isSystem ? 'border-blue-500/30' : 'border-border';
375
+ const bgColor = isSystem ? 'bg-blue-500/10' : 'bg-muted/30';
376
+ const hoverBgColor = isSystem ? 'hover:bg-blue-500/20' : 'hover:bg-muted/50';
377
+ const iconColor = isSystem ? 'text-blue-400' : 'text-muted-foreground';
378
+ const labelColor = isSystem ? 'text-blue-400' : 'text-foreground';
379
+
380
+ const handleCopy = async (e: React.MouseEvent) => {
381
+ e.stopPropagation();
382
+ await navigator.clipboard.writeText(content);
383
+ setCopied(true);
384
+ setTimeout(() => setCopied(false), 2000);
385
+ };
386
+
387
+ return (
388
+ <div className={`rounded-md border ${borderColor} overflow-hidden`}>
389
+ <button
390
+ className={`w-full flex items-center gap-2 px-3 py-2 ${bgColor} ${hoverBgColor} transition-colors`}
391
+ onClick={() => setExpanded(!expanded)}
392
+ >
393
+ <ChevronRight
394
+ className={`size-3 ${iconColor} transition-transform shrink-0 ${
395
+ expanded ? 'rotate-90' : ''
396
+ }`}
397
+ />
398
+ <MessageSquare className={`size-3 ${iconColor} shrink-0`} />
399
+ <span className={`text-xs font-medium ${labelColor}`}>Text</span>
400
+ {!expanded && (
401
+ <span
402
+ className={`text-[11px] ${isSystem ? 'text-blue-400/70' : 'text-muted-foreground'} truncate ml-1`}
403
+ >
404
+ {previewContent}
405
+ </span>
406
+ )}
407
+ </button>
408
+
409
+ {expanded && (
410
+ <div
411
+ className={`p-3 bg-card/50 border-t ${borderColor} group relative`}
412
+ >
413
+ <button
414
+ onClick={handleCopy}
415
+ className="absolute top-1.5 right-1.5 p-1.5 rounded-md border border-border bg-background opacity-0 group-hover:opacity-100 transition-opacity z-10"
416
+ title="Copy to clipboard"
417
+ >
418
+ {copied ? (
419
+ <Check className="size-3 text-success" />
420
+ ) : (
421
+ <Copy className="size-3 text-muted-foreground" />
422
+ )}
423
+ </button>
424
+ <div className="text-xs text-foreground leading-relaxed whitespace-pre-wrap max-h-60 overflow-y-auto">
425
+ {content}
426
+ </div>
427
+ </div>
428
+ )}
429
+ </div>
430
+ );
431
+ }
432
+
433
+ export function ToolItem({ tool }: { tool: ToolDefinition }) {
434
+ const [expanded, setExpanded] = useState(false);
435
+
436
+ return (
437
+ <div className="rounded-md border border-border bg-background overflow-hidden">
438
+ <button
439
+ className="w-full flex items-center justify-between p-2.5 hover:bg-accent/50 transition-colors"
440
+ onClick={() => setExpanded(!expanded)}
441
+ >
442
+ <span className="text-sm font-mono text-purple">{tool.name}</span>
443
+ {tool.parameters && (
444
+ <ChevronRight
445
+ className={`size-3 text-muted-foreground transition-transform ${
446
+ expanded ? 'rotate-90' : ''
447
+ }`}
448
+ />
449
+ )}
450
+ </button>
451
+ {expanded && tool.parameters && (
452
+ <div className="px-2.5 pb-2.5 border-t border-border">
453
+ {tool.description && (
454
+ <p className="text-xs text-muted-foreground mb-2 pt-2">
455
+ {tool.description}
456
+ </p>
457
+ )}
458
+ <JsonBlock data={tool.parameters} compact />
459
+ </div>
460
+ )}
461
+ {!expanded && tool.description && (
462
+ <div className="px-2.5 pb-2 -mt-1">
463
+ <p className="text-[11px] text-muted-foreground truncate">
464
+ {tool.description}
465
+ </p>
466
+ </div>
467
+ )}
468
+ </div>
469
+ );
470
+ }
471
+
472
+ export function CollapsibleToolCall({
473
+ toolName,
474
+ toolCallId,
475
+ data,
476
+ }: {
477
+ toolName: string;
478
+ toolCallId?: string;
479
+ data: unknown;
480
+ }) {
481
+ const [expanded, setExpanded] = useState(false);
482
+ const parsedData = typeof data === 'string' ? safeParseJson(data) : data;
483
+
484
+ return (
485
+ <div className="rounded-md border border-purple/30 overflow-hidden">
486
+ <button
487
+ className="w-full flex items-center gap-2 px-3 py-2 bg-purple/10 hover:bg-purple/20 transition-colors"
488
+ onClick={() => setExpanded(!expanded)}
489
+ >
490
+ <ChevronRight
491
+ className={`size-3 text-purple transition-transform shrink-0 ${
492
+ expanded ? 'rotate-90' : ''
493
+ }`}
494
+ />
495
+ <Wrench className="size-3 text-purple shrink-0" />
496
+ <span className="text-xs font-mono font-medium text-purple">
497
+ {toolName}
498
+ </span>
499
+ {!expanded &&
500
+ parsedData != null &&
501
+ typeof parsedData === 'object' &&
502
+ !Array.isArray(parsedData) && (
503
+ <span className="text-[11px] font-mono text-purple/70 truncate">
504
+ {formatToolParams(parsedData as Record<string, unknown>)}
505
+ </span>
506
+ )}
507
+ {toolCallId && (
508
+ <span className="text-[10px] font-mono text-muted-foreground/60 ml-auto shrink-0">
509
+ {toolCallId}
510
+ </span>
511
+ )}
512
+ </button>
513
+ {expanded && (
514
+ <div className="p-3 bg-card/50 border-t border-purple/30">
515
+ <JsonBlock data={parsedData} />
516
+ </div>
517
+ )}
518
+ </div>
519
+ );
520
+ }
521
+
522
+ export function CollapsibleToolResult({
523
+ toolName,
524
+ toolCallId,
525
+ data,
526
+ }: {
527
+ toolName?: string;
528
+ toolCallId?: string;
529
+ data: unknown;
530
+ }) {
531
+ const [expanded, setExpanded] = useState(false);
532
+
533
+ return (
534
+ <div className="rounded-md border border-success/30 overflow-hidden">
535
+ <button
536
+ className="w-full flex items-center gap-2 px-3 py-2 bg-success/10 hover:bg-success/20 transition-colors"
537
+ onClick={() => setExpanded(!expanded)}
538
+ >
539
+ <ChevronRight
540
+ className={`size-3 text-success transition-transform shrink-0 ${
541
+ expanded ? 'rotate-90' : ''
542
+ }`}
543
+ />
544
+ <span className="text-xs font-medium text-success">Result</span>
545
+ {toolName && (
546
+ <span className="text-[11px] font-mono text-muted-foreground">
547
+ {toolName}
548
+ </span>
549
+ )}
550
+ {toolCallId && (
551
+ <span className="text-[10px] font-mono text-muted-foreground/60 ml-auto shrink-0">
552
+ {toolCallId}
553
+ </span>
554
+ )}
555
+ </button>
556
+ {expanded && (
557
+ <div className="p-3 bg-card/50 border-t border-success/30">
558
+ <JsonBlock data={data} />
559
+ </div>
560
+ )}
561
+ </div>
562
+ );
563
+ }
564
+
565
+ export function StepConfigBar({
566
+ modelId,
567
+ provider,
568
+ input,
569
+ providerOptions,
570
+ usage,
571
+ }: {
572
+ modelId?: string;
573
+ provider?: string | null;
574
+ input: ParsedInput | null;
575
+ providerOptions?: Record<string, unknown> | null;
576
+ usage?: ParsedUsage | null;
577
+ }) {
578
+ const toolCount = input?.tools?.length ?? 0;
579
+
580
+ const params: { label: string; value: string }[] = [];
581
+ if (input?.temperature != null)
582
+ params.push({ label: 'temp', value: String(input.temperature) });
583
+ if (input?.maxOutputTokens != null)
584
+ params.push({ label: 'max tokens', value: String(input.maxOutputTokens) });
585
+ if (input?.topP != null)
586
+ params.push({ label: 'topP', value: String(input.topP) });
587
+ if (input?.topK != null)
588
+ params.push({ label: 'topK', value: String(input.topK) });
589
+ if (input?.toolChoice != null) {
590
+ const choice =
591
+ typeof input.toolChoice === 'string'
592
+ ? input.toolChoice
593
+ : input.toolChoice.type;
594
+ params.push({ label: 'tool choice', value: choice });
595
+ }
596
+
597
+ return (
598
+ <div className="flex items-center gap-2 px-4 py-2 bg-muted/20 border-b border-border text-[11px] text-muted-foreground">
599
+ {provider && (
600
+ <span className="px-1.5 py-0.5 rounded bg-sidebar-primary/10 text-sidebar-primary text-[10px] font-medium">
601
+ {provider}
602
+ </span>
603
+ )}
604
+ <span className="font-mono">{modelId}</span>
605
+
606
+ {params.length > 0 && (
607
+ <>
608
+ <span className="text-muted-foreground/30">·</span>
609
+ {params.map((p, i) => (
610
+ <span key={i}>
611
+ {p.label}: <span className="text-foreground">{p.value}</span>
612
+ {i < params.length - 1 && (
613
+ <span className="text-muted-foreground/30 mx-1">·</span>
614
+ )}
615
+ </span>
616
+ ))}
617
+ </>
618
+ )}
619
+
620
+ {toolCount > 0 && (
621
+ <>
622
+ <span className="text-muted-foreground/30">·</span>
623
+ <Drawer direction="right">
624
+ <DrawerTrigger asChild>
625
+ <button className="inline-flex items-center gap-1 hover:text-foreground transition-colors cursor-pointer">
626
+ <Wrench className="size-3" />
627
+ {toolCount} available {toolCount === 1 ? 'tool' : 'tools'}
628
+ </button>
629
+ </DrawerTrigger>
630
+ <DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
631
+ <DrawerHeader className="border-b border-border shrink-0">
632
+ <DrawerTitle>Available Tools ({toolCount})</DrawerTitle>
633
+ </DrawerHeader>
634
+ <div className="flex-1 overflow-y-auto p-4">
635
+ <div className="space-y-3">
636
+ {input?.tools?.map((tool: ToolDefinition, i: number) => (
637
+ <ToolItem key={i} tool={tool} />
638
+ ))}
639
+ </div>
640
+ </div>
641
+ </DrawerContent>
642
+ </Drawer>
643
+ </>
644
+ )}
645
+
646
+ {providerOptions && Object.keys(providerOptions).length > 0 && (
647
+ <>
648
+ <span className="text-muted-foreground/30">·</span>
649
+ <Drawer direction="right">
650
+ <DrawerTrigger asChild>
651
+ <button className="inline-flex items-center gap-1 hover:text-foreground transition-colors cursor-pointer">
652
+ <Settings className="size-3" />
653
+ Provider options
654
+ </button>
655
+ </DrawerTrigger>
656
+ <DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
657
+ <DrawerHeader className="border-b border-border shrink-0">
658
+ <DrawerTitle>Provider Options</DrawerTitle>
659
+ </DrawerHeader>
660
+ <div className="flex-1 overflow-y-auto p-4">
661
+ <JsonBlock data={providerOptions} size="lg" />
662
+ </div>
663
+ </DrawerContent>
664
+ </Drawer>
665
+ </>
666
+ )}
667
+
668
+ {usage && (
669
+ <>
670
+ <span className="text-muted-foreground/30">·</span>
671
+ <Drawer direction="right">
672
+ <DrawerTrigger asChild>
673
+ <button className="inline-flex items-center gap-1 hover:text-foreground transition-colors cursor-pointer">
674
+ <BarChart3 className="size-3" />
675
+ Usage
676
+ </button>
677
+ </DrawerTrigger>
678
+ <DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
679
+ <DrawerHeader className="border-b border-border shrink-0">
680
+ <DrawerTitle>Token Usage</DrawerTitle>
681
+ </DrawerHeader>
682
+ <div className="flex-1 overflow-y-auto p-4">
683
+ <UsageDetails usage={usage} />
684
+ </div>
685
+ </DrawerContent>
686
+ </Drawer>
687
+ </>
688
+ )}
689
+ </div>
690
+ );
691
+ }