@ai-sdk/devtools 1.0.0-beta.1 → 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,483 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ ChevronRight,
4
+ ChevronDown,
5
+ Wrench,
6
+ MessageSquare,
7
+ AlertCircle,
8
+ Brain,
9
+ Loader2,
10
+ } from 'lucide-react';
11
+ import { Card } from '@/components/ui/card';
12
+ import {
13
+ Collapsible,
14
+ CollapsibleContent,
15
+ CollapsibleTrigger,
16
+ } from '@/components/ui/collapsible';
17
+ import {
18
+ Tooltip,
19
+ TooltipContent,
20
+ TooltipTrigger,
21
+ } from '@/components/ui/tooltip';
22
+ import type {
23
+ Step,
24
+ ChildRun,
25
+ ParseJson,
26
+ ParsedInput,
27
+ ParsedOutput,
28
+ ParsedUsage,
29
+ PromptMessage,
30
+ ContentPart,
31
+ } from '../types';
32
+ import {
33
+ getStepSummary,
34
+ getStepInputSummary,
35
+ getInputTokenBreakdown,
36
+ getOutputTokenBreakdown,
37
+ formatInputTokens,
38
+ formatOutputTokens,
39
+ } from '../utils';
40
+ import {
41
+ StepConfigBar,
42
+ RawDataSection,
43
+ TokenBreakdownTooltip,
44
+ } from './shared-components';
45
+ import { InputPanel } from './message-components';
46
+ import { OutputDisplay } from './output-components';
47
+
48
+ export function StepCard({
49
+ step,
50
+ index,
51
+ steps,
52
+ isRunInProgress,
53
+ expandedSteps,
54
+ toggleStep,
55
+ parseJson,
56
+ formatDuration,
57
+ childRuns,
58
+ depth,
59
+ }: {
60
+ step: Step;
61
+ index: number;
62
+ steps: Step[];
63
+ isRunInProgress: boolean;
64
+ expandedSteps: Set<string>;
65
+ toggleStep: (id: string) => void;
66
+ parseJson: ParseJson;
67
+ formatDuration: (ms: number | null) => string;
68
+ childRuns: ChildRun[];
69
+ depth: number;
70
+ }) {
71
+ const isExpanded = expandedSteps.has(step.id);
72
+ const isLastStep = index === steps.length - 1;
73
+ const isActiveStep = isLastStep && isRunInProgress;
74
+ const input = parseJson(step.input) as ParsedInput | null;
75
+ const output = parseJson(step.output) as ParsedOutput | null;
76
+ const usage = parseJson(step.usage) as ParsedUsage | null;
77
+
78
+ const nextStep = steps[index + 1];
79
+ const nextInput = nextStep
80
+ ? (parseJson(nextStep.input) as ParsedInput | null)
81
+ : null;
82
+ const toolResults: ContentPart[] =
83
+ nextInput?.prompt
84
+ ?.filter((msg: PromptMessage) => msg.role === 'tool')
85
+ ?.flatMap((msg: PromptMessage) =>
86
+ Array.isArray(msg.content) ? msg.content : [],
87
+ ) ?? [];
88
+
89
+ const summary = getStepSummary(output, step.error);
90
+ const isFirstStep = index === 0;
91
+ const inputSummary = getStepInputSummary(input, isFirstStep);
92
+
93
+ return (
94
+ <Collapsible open={isExpanded} onOpenChange={() => toggleStep(step.id)}>
95
+ <Card
96
+ className={`overflow-hidden py-0 gap-0 ${isActiveStep ? 'ring-2 ring-blue-500/50 ring-offset-1 ring-offset-background' : ''}`}
97
+ >
98
+ <CollapsibleTrigger asChild>
99
+ <button
100
+ className={`w-full flex items-center justify-between px-4 py-3 text-left transition-colors hover:bg-accent/50 ${
101
+ isExpanded ? 'border-b border-border' : ''
102
+ }`}
103
+ >
104
+ <div className="flex items-center gap-3">
105
+ <span className="text-xs text-muted-foreground font-mono w-4">
106
+ {step.step_number}
107
+ </span>
108
+ <div className="flex items-center gap-2">
109
+ {inputSummary && (
110
+ <>
111
+ {inputSummary.type === 'user' ? (
112
+ <MessageSquare className="size-3.5 text-muted-foreground" />
113
+ ) : (
114
+ <Wrench className="size-3.5 text-muted-foreground" />
115
+ )}
116
+ {inputSummary.fullText || inputSummary.toolDetails ? (
117
+ <Tooltip>
118
+ <TooltipTrigger asChild>
119
+ <span
120
+ className={`text-sm text-muted-foreground ${inputSummary.type === 'tool' ? 'font-mono' : ''}`}
121
+ >
122
+ {inputSummary.label}
123
+ </span>
124
+ </TooltipTrigger>
125
+ <TooltipContent
126
+ side="bottom"
127
+ className="max-w-sm max-h-40 overflow-hidden"
128
+ >
129
+ <span className="line-clamp-6">
130
+ {inputSummary.fullText || inputSummary.toolDetails}
131
+ </span>
132
+ </TooltipContent>
133
+ </Tooltip>
134
+ ) : (
135
+ <span
136
+ className={`text-sm text-muted-foreground ${inputSummary.type === 'tool' ? 'font-mono' : ''}`}
137
+ >
138
+ {inputSummary.label}
139
+ </span>
140
+ )}
141
+ <span className="text-muted-foreground/50 mx-1">→</span>
142
+ </>
143
+ )}
144
+
145
+ {summary.icon === 'wrench' && (
146
+ <Wrench className="size-3.5 text-muted-foreground" />
147
+ )}
148
+ {summary.icon === 'message' && (
149
+ <MessageSquare className="size-3.5 text-muted-foreground" />
150
+ )}
151
+ {summary.icon === 'alert' && (
152
+ <AlertCircle className="size-3.5 text-destructive" />
153
+ )}
154
+
155
+ {summary.toolDetails ? (
156
+ <Tooltip>
157
+ <TooltipTrigger asChild>
158
+ <span className="text-sm font-medium font-mono">
159
+ {summary.label}
160
+ </span>
161
+ </TooltipTrigger>
162
+ <TooltipContent>{summary.toolDetails}</TooltipContent>
163
+ </Tooltip>
164
+ ) : (
165
+ <span
166
+ className={`text-sm font-medium ${
167
+ summary.icon === 'wrench' ? 'font-mono' : ''
168
+ }`}
169
+ >
170
+ {summary.label}
171
+ </span>
172
+ )}
173
+ </div>
174
+ </div>
175
+
176
+ <div className="flex items-center gap-4">
177
+ {isActiveStep ? (
178
+ <span className="text-[11px] text-blue-400 font-medium flex items-center gap-1.5">
179
+ <Loader2 className="size-3 animate-spin" />
180
+ streaming
181
+ </span>
182
+ ) : (
183
+ <span className="text-[11px] text-muted-foreground font-mono">
184
+ {formatDuration(step.duration_ms)}
185
+ </span>
186
+ )}
187
+ {usage && (
188
+ <Tooltip>
189
+ <TooltipTrigger asChild>
190
+ <span className="text-[11px] font-mono text-muted-foreground cursor-help">
191
+ {formatInputTokens(
192
+ getInputTokenBreakdown(usage.inputTokens),
193
+ )}{' '}
194
+ <span className="text-muted-foreground/50">→</span>{' '}
195
+ {formatOutputTokens(
196
+ getOutputTokenBreakdown(usage.outputTokens),
197
+ )}
198
+ </span>
199
+ </TooltipTrigger>
200
+ <TooltipContent>
201
+ <TokenBreakdownTooltip
202
+ input={getInputTokenBreakdown(usage.inputTokens)}
203
+ output={getOutputTokenBreakdown(usage.outputTokens)}
204
+ raw={usage.raw}
205
+ />
206
+ </TooltipContent>
207
+ </Tooltip>
208
+ )}
209
+ <ChevronDown
210
+ className={`size-4 text-muted-foreground transition-transform ${
211
+ isExpanded ? 'rotate-180' : ''
212
+ }`}
213
+ />
214
+ </div>
215
+ </button>
216
+ </CollapsibleTrigger>
217
+
218
+ <CollapsibleContent>
219
+ <StepDetailContent
220
+ step={step}
221
+ index={index}
222
+ steps={steps}
223
+ isRunInProgress={isRunInProgress}
224
+ parseJson={parseJson}
225
+ formatDuration={formatDuration}
226
+ childRuns={childRuns}
227
+ expandedSteps={expandedSteps}
228
+ toggleStep={toggleStep}
229
+ depth={depth}
230
+ />
231
+ </CollapsibleContent>
232
+ </Card>
233
+ </Collapsible>
234
+ );
235
+ }
236
+
237
+ export function StepDetailContent({
238
+ step,
239
+ index,
240
+ steps,
241
+ isRunInProgress,
242
+ parseJson,
243
+ formatDuration,
244
+ childRuns,
245
+ expandedSteps,
246
+ toggleStep,
247
+ depth,
248
+ }: {
249
+ step: Step;
250
+ index: number;
251
+ steps: Step[];
252
+ isRunInProgress: boolean;
253
+ parseJson: ParseJson;
254
+ formatDuration: (ms: number | null) => string;
255
+ childRuns: ChildRun[];
256
+ expandedSteps?: Set<string>;
257
+ toggleStep?: (id: string) => void;
258
+ depth: number;
259
+ }) {
260
+ const isLastStep = index === steps.length - 1;
261
+ const isActiveStep = isLastStep && isRunInProgress;
262
+ const input = parseJson(step.input) as ParsedInput | null;
263
+ const output = parseJson(step.output) as ParsedOutput | null;
264
+ const usage = parseJson(step.usage) as ParsedUsage | null;
265
+
266
+ const nextStep = steps[index + 1];
267
+ const nextInput = nextStep
268
+ ? (parseJson(nextStep.input) as ParsedInput | null)
269
+ : null;
270
+ const toolResults: ContentPart[] =
271
+ nextInput?.prompt
272
+ ?.filter((msg: PromptMessage) => msg.role === 'tool')
273
+ ?.flatMap((msg: PromptMessage) =>
274
+ Array.isArray(msg.content) ? msg.content : [],
275
+ ) ?? [];
276
+
277
+ return (
278
+ <>
279
+ <StepConfigBar
280
+ modelId={step.model_id}
281
+ provider={step.provider}
282
+ input={input}
283
+ providerOptions={
284
+ parseJson(step.provider_options) as Record<string, unknown> | null
285
+ }
286
+ usage={usage}
287
+ />
288
+
289
+ <div className="grid grid-cols-2 divide-x divide-border">
290
+ <div className="bg-card/50">
291
+ <InputPanel input={input} />
292
+ </div>
293
+
294
+ <div className="p-4 bg-background min-h-full">
295
+ <h3 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-3">
296
+ Output
297
+ </h3>
298
+
299
+ {step.error ? (
300
+ <div className="p-3 rounded-md bg-destructive/10 border border-destructive/30 text-sm text-destructive-foreground font-mono">
301
+ {step.error}
302
+ </div>
303
+ ) : output ? (
304
+ <OutputDisplay output={output} toolResults={toolResults} />
305
+ ) : isActiveStep ? (
306
+ <div className="flex items-center gap-2 text-sm text-blue-400">
307
+ <Loader2 className="size-4 animate-spin" />
308
+ <span>Waiting for response...</span>
309
+ </div>
310
+ ) : (
311
+ <p className="text-sm text-muted-foreground">No output</p>
312
+ )}
313
+ </div>
314
+ </div>
315
+
316
+ {childRuns.length > 0 && expandedSteps && toggleStep && (
317
+ <NestedChildRuns
318
+ childRuns={childRuns}
319
+ expandedSteps={expandedSteps}
320
+ toggleStep={toggleStep}
321
+ parseJson={parseJson}
322
+ formatDuration={formatDuration}
323
+ depth={depth}
324
+ />
325
+ )}
326
+
327
+ <RawDataSection
328
+ rawRequest={step.raw_request}
329
+ rawResponse={step.raw_response}
330
+ rawChunks={step.raw_chunks}
331
+ isStream={step.type === 'stream'}
332
+ />
333
+ </>
334
+ );
335
+ }
336
+
337
+ function NestedChildRuns({
338
+ childRuns,
339
+ expandedSteps,
340
+ toggleStep,
341
+ parseJson,
342
+ formatDuration,
343
+ depth,
344
+ }: {
345
+ childRuns: ChildRun[];
346
+ expandedSteps: Set<string>;
347
+ toggleStep: (id: string) => void;
348
+ parseJson: ParseJson;
349
+ formatDuration: (ms: number | null) => string;
350
+ depth: number;
351
+ }) {
352
+ return (
353
+ <div className="border-t border-border bg-muted/10">
354
+ <div className="px-4 py-2 flex items-center gap-2">
355
+ <div className="h-px flex-1 bg-border" />
356
+ <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground shrink-0">
357
+ Sub-agent {childRuns.length === 1 ? 'call' : 'calls'} (
358
+ {childRuns.length})
359
+ </span>
360
+ <div className="h-px flex-1 bg-border" />
361
+ </div>
362
+ <div className="px-4 pb-3 flex flex-col gap-2">
363
+ {childRuns.map(childRun => (
364
+ <NestedRunCard
365
+ key={childRun.run.id}
366
+ childRun={childRun}
367
+ expandedSteps={expandedSteps}
368
+ toggleStep={toggleStep}
369
+ parseJson={parseJson}
370
+ formatDuration={formatDuration}
371
+ depth={depth + 1}
372
+ />
373
+ ))}
374
+ </div>
375
+ </div>
376
+ );
377
+ }
378
+
379
+ function NestedRunCard({
380
+ childRun,
381
+ expandedSteps,
382
+ toggleStep,
383
+ parseJson,
384
+ formatDuration,
385
+ depth,
386
+ }: {
387
+ childRun: ChildRun;
388
+ expandedSteps: Set<string>;
389
+ toggleStep: (id: string) => void;
390
+ parseJson: ParseJson;
391
+ formatDuration: (ms: number | null) => string;
392
+ depth: number;
393
+ }) {
394
+ const [expanded, setExpanded] = useState(false);
395
+
396
+ const firstStep = childRun.steps[0];
397
+ const firstInput = firstStep
398
+ ? (parseJson(firstStep.input) as ParsedInput | null)
399
+ : null;
400
+ const promptMessages = firstInput?.prompt ?? [];
401
+ const userMsg = [...promptMessages]
402
+ .reverse()
403
+ .find((m: PromptMessage) => m.role === 'user');
404
+ let label = 'Sub-agent call';
405
+ if (userMsg) {
406
+ const content =
407
+ typeof userMsg.content === 'string'
408
+ ? userMsg.content
409
+ : Array.isArray(userMsg.content)
410
+ ? userMsg.content.find(
411
+ (p): p is { type: 'text'; text: string } => p.type === 'text',
412
+ )?.text || ''
413
+ : '';
414
+ label = content.length > 60 ? content.slice(0, 60) + '...' : content;
415
+ }
416
+
417
+ const totalDuration = childRun.steps.reduce(
418
+ (acc, s) => acc + (s.duration_ms || 0),
419
+ 0,
420
+ );
421
+ const modelId = firstStep?.model_id;
422
+
423
+ return (
424
+ <div className="rounded-md border border-cyan-500/30 overflow-hidden">
425
+ <button
426
+ className="w-full flex items-center gap-2 px-3 py-2 bg-cyan-500/10 hover:bg-cyan-500/15 transition-colors"
427
+ onClick={() => setExpanded(!expanded)}
428
+ >
429
+ <ChevronRight
430
+ className={`size-3 text-cyan-400 transition-transform shrink-0 ${
431
+ expanded ? 'rotate-90' : ''
432
+ }`}
433
+ />
434
+ <Brain className="size-3 text-cyan-400 shrink-0" />
435
+ <span className="text-xs font-medium text-cyan-400 truncate">
436
+ {label}
437
+ </span>
438
+ <span className="ml-auto flex items-center gap-3 shrink-0">
439
+ {modelId && (
440
+ <span className="text-[10px] font-mono text-muted-foreground">
441
+ {modelId}
442
+ </span>
443
+ )}
444
+ <span className="text-[10px] text-muted-foreground">
445
+ {childRun.steps.length}{' '}
446
+ {childRun.steps.length === 1 ? 'step' : 'steps'}
447
+ </span>
448
+ <span className="text-[10px] font-mono text-muted-foreground">
449
+ {formatDuration(totalDuration)}
450
+ </span>
451
+ {childRun.run.isInProgress && (
452
+ <Loader2 className="size-3 text-blue-400 animate-spin" />
453
+ )}
454
+ </span>
455
+ </button>
456
+
457
+ {expanded && (
458
+ <div className="border-t border-cyan-500/20 bg-background/50 p-3 flex flex-col gap-2">
459
+ {childRun.steps.map((step, index) => {
460
+ const nestedChildRuns = (childRun.childRuns ?? []).filter(
461
+ cr => cr.run.parent_step_id === step.id,
462
+ );
463
+ return (
464
+ <StepCard
465
+ key={step.id}
466
+ step={step}
467
+ index={index}
468
+ steps={childRun.steps}
469
+ isRunInProgress={childRun.run.isInProgress ?? false}
470
+ expandedSteps={expandedSteps}
471
+ toggleStep={toggleStep}
472
+ parseJson={parseJson}
473
+ formatDuration={formatDuration}
474
+ childRuns={nestedChildRuns}
475
+ depth={depth}
476
+ />
477
+ );
478
+ })}
479
+ </div>
480
+ )}
481
+ </div>
482
+ );
483
+ }