@ai-sdk/devtools 1.0.0-beta.8 → 1.0.0-canary.20

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