@ai-sdk/devtools 1.0.0-beta.9 → 1.0.0-canary.21

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,342 @@
1
+ import React from 'react';
2
+ import {
3
+ Drawer,
4
+ DrawerContent,
5
+ DrawerHeader,
6
+ DrawerTitle,
7
+ DrawerTrigger,
8
+ } from '@/components/ui/drawer';
9
+ import type {
10
+ ParsedInput,
11
+ PromptMessage,
12
+ ContentPart,
13
+ ToolCallContentPart,
14
+ ToolResultContentPart,
15
+ } from '../types';
16
+ import {
17
+ safeParseJson,
18
+ formatToolParamsInline,
19
+ formatResultPreview,
20
+ } from '../utils';
21
+ import {
22
+ CollapsibleToolCall,
23
+ CollapsibleToolResult,
24
+ ReasoningBlock,
25
+ TextBlock,
26
+ } from './shared-components';
27
+
28
+ export function InputPanel({ input }: { input: ParsedInput | null }) {
29
+ const messages: PromptMessage[] = input?.prompt ?? [];
30
+ const messageCount = messages.length;
31
+
32
+ const lastTwoMessages = messages.slice(-2);
33
+ const previousMessageCount = Math.max(0, messageCount - 2);
34
+
35
+ return (
36
+ <Drawer direction="right">
37
+ <DrawerTrigger asChild>
38
+ <button className="w-full h-full text-left p-4 hover:bg-accent/30 transition-colors cursor-pointer flex flex-col justify-start">
39
+ <h3 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-3">
40
+ Input
41
+ </h3>
42
+
43
+ <div className="space-y-3">
44
+ {previousMessageCount > 0 && (
45
+ <div className="text-[11px] text-muted-foreground/60 text-center py-1.5 rounded-md bg-muted/30">
46
+ + {previousMessageCount} previous{' '}
47
+ {previousMessageCount === 1 ? 'message' : 'messages'}
48
+ </div>
49
+ )}
50
+
51
+ {lastTwoMessages.map((msg, i) => (
52
+ <InputMessagePreview
53
+ key={i}
54
+ message={msg}
55
+ index={previousMessageCount + i + 1}
56
+ />
57
+ ))}
58
+
59
+ {messageCount === 0 && (
60
+ <p className="text-sm text-muted-foreground">No messages</p>
61
+ )}
62
+ </div>
63
+ </button>
64
+ </DrawerTrigger>
65
+ <DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
66
+ <DrawerHeader className="border-b border-border shrink-0">
67
+ <DrawerTitle>All Messages ({messageCount})</DrawerTitle>
68
+ </DrawerHeader>
69
+ <div className="flex-1 overflow-y-auto p-4">
70
+ <div className="space-y-3">
71
+ {messages.map((msg, i) => (
72
+ <MessageBubble key={i} message={msg} index={i + 1} />
73
+ ))}
74
+ </div>
75
+ </div>
76
+ </DrawerContent>
77
+ </Drawer>
78
+ );
79
+ }
80
+
81
+ function getTextContent(content: string | ContentPart[]): string {
82
+ if (typeof content === 'string') return content;
83
+ if (Array.isArray(content)) {
84
+ return content
85
+ .filter((p): p is { type: 'text'; text: string } => p.type === 'text')
86
+ .map(p => p.text)
87
+ .join('');
88
+ }
89
+ return '';
90
+ }
91
+
92
+ function getToolCalls(content: string | ContentPart[]): ToolCallContentPart[] {
93
+ if (Array.isArray(content)) {
94
+ return content.filter(
95
+ (p): p is ToolCallContentPart => p.type === 'tool-call',
96
+ );
97
+ }
98
+ return [];
99
+ }
100
+
101
+ function getToolResults(
102
+ content: string | ContentPart[],
103
+ ): ToolResultContentPart[] {
104
+ if (Array.isArray(content)) {
105
+ return content.filter(
106
+ (p): p is ToolResultContentPart => p.type === 'tool-result',
107
+ );
108
+ }
109
+ return [];
110
+ }
111
+
112
+ function getReasoningContent(content: string | ContentPart[]): string {
113
+ if (Array.isArray(content)) {
114
+ return content
115
+ .filter(
116
+ (
117
+ p,
118
+ ): p is {
119
+ type: 'thinking' | 'reasoning';
120
+ text?: string;
121
+ thinking?: string;
122
+ reasoning?: string;
123
+ } => p.type === 'thinking' || p.type === 'reasoning',
124
+ )
125
+ .map(p => p.thinking || p.text || p.reasoning || '')
126
+ .join('');
127
+ }
128
+ return '';
129
+ }
130
+
131
+ function InputMessagePreview({
132
+ message,
133
+ index,
134
+ }: {
135
+ message: PromptMessage;
136
+ index?: number;
137
+ }) {
138
+ const { role, content } = message;
139
+
140
+ const roleLabels: Record<string, string> = {
141
+ user: 'User',
142
+ assistant: 'Assistant',
143
+ system: 'System',
144
+ tool: 'Tool',
145
+ };
146
+
147
+ const textContent = getTextContent(content);
148
+ const toolCalls = getToolCalls(content);
149
+ const toolResults = getToolResults(content);
150
+ const reasoningContent = getReasoningContent(content);
151
+
152
+ const partCount =
153
+ (textContent ? 1 : 0) +
154
+ (reasoningContent ? 1 : 0) +
155
+ toolCalls.length +
156
+ toolResults.length;
157
+
158
+ return (
159
+ <div className="rounded-md border border-border/50 bg-background/50 p-2.5 space-y-2">
160
+ <div className="flex items-center gap-2">
161
+ {index && (
162
+ <span className="text-[10px] font-mono text-muted-foreground/50">
163
+ {index}
164
+ </span>
165
+ )}
166
+ <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
167
+ {roleLabels[role] || role}
168
+ </span>
169
+ {partCount > 1 && (
170
+ <span className="text-[10px] text-muted-foreground/60">
171
+ {partCount} parts
172
+ </span>
173
+ )}
174
+ </div>
175
+
176
+ {reasoningContent && (
177
+ <div className="text-xs text-amber-500/60">[thinking]</div>
178
+ )}
179
+
180
+ {textContent && (
181
+ <div className="text-xs text-foreground/90 line-clamp-3">
182
+ {textContent}
183
+ </div>
184
+ )}
185
+
186
+ {toolCalls.length > 0 && (
187
+ <div className="space-y-1">
188
+ {toolCalls.slice(0, 3).map((call, i) => {
189
+ const args = call.args ?? call.input;
190
+ const parsedArgs =
191
+ typeof args === 'string'
192
+ ? (safeParseJson(args) as Record<string, unknown>)
193
+ : ((args as Record<string, unknown>) ?? {});
194
+ return (
195
+ <div
196
+ key={i}
197
+ className="text-[11px] font-mono text-muted-foreground truncate"
198
+ >
199
+ {call.toolName}({formatToolParamsInline(parsedArgs)})
200
+ </div>
201
+ );
202
+ })}
203
+ {toolCalls.length > 3 && (
204
+ <div className="text-[11px] text-muted-foreground/60">
205
+ +{toolCalls.length - 3} more tool{' '}
206
+ {toolCalls.length - 3 === 1 ? 'call' : 'calls'}
207
+ </div>
208
+ )}
209
+ </div>
210
+ )}
211
+
212
+ {toolResults.length > 0 && (
213
+ <div className="space-y-1">
214
+ {toolResults.slice(0, 3).map((result, i) => {
215
+ const resultContent = result.result ?? result.output ?? result;
216
+ const resultPreview = formatResultPreview(resultContent);
217
+ return (
218
+ <div
219
+ key={i}
220
+ className="text-[11px] font-mono text-muted-foreground truncate"
221
+ >
222
+ {result.toolName || 'tool'}(…) =&gt; {resultPreview}
223
+ </div>
224
+ );
225
+ })}
226
+ {toolResults.length > 3 && (
227
+ <div className="text-[11px] text-muted-foreground/60">
228
+ +{toolResults.length - 3} more tool{' '}
229
+ {toolResults.length - 3 === 1 ? 'result' : 'results'}
230
+ </div>
231
+ )}
232
+ </div>
233
+ )}
234
+
235
+ {!textContent &&
236
+ !reasoningContent &&
237
+ toolCalls.length === 0 &&
238
+ toolResults.length === 0 && (
239
+ <div className="text-[11px] text-muted-foreground italic">
240
+ Empty message
241
+ </div>
242
+ )}
243
+ </div>
244
+ );
245
+ }
246
+
247
+ export function MessageBubble({
248
+ message,
249
+ index,
250
+ }: {
251
+ message: PromptMessage;
252
+ index?: number;
253
+ }) {
254
+ const { role, content } = message;
255
+
256
+ const roleLabels: Record<string, string> = {
257
+ user: 'User',
258
+ assistant: 'Assistant',
259
+ system: 'System',
260
+ tool: 'Tool',
261
+ };
262
+
263
+ const textContent = getTextContent(content);
264
+ const toolCalls = getToolCalls(content);
265
+ const toolResults = getToolResults(content);
266
+ const reasoningContent = getReasoningContent(content);
267
+
268
+ const partCount =
269
+ (textContent ? 1 : 0) +
270
+ (reasoningContent ? 1 : 0) +
271
+ toolCalls.length +
272
+ toolResults.length;
273
+
274
+ return (
275
+ <div className="rounded-md border border-border/50 bg-background/50 p-3 space-y-2">
276
+ <div className="flex items-center gap-2">
277
+ {index && (
278
+ <span className="text-[10px] font-mono text-muted-foreground/50">
279
+ {index}
280
+ </span>
281
+ )}
282
+ <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
283
+ {roleLabels[role] || role}
284
+ </span>
285
+ {partCount > 1 && (
286
+ <span className="text-[10px] text-muted-foreground/60">
287
+ {partCount} parts
288
+ </span>
289
+ )}
290
+ </div>
291
+
292
+ {reasoningContent && <ReasoningBlock content={reasoningContent} />}
293
+
294
+ {textContent && (
295
+ <TextBlock
296
+ content={textContent}
297
+ defaultExpanded={
298
+ !reasoningContent &&
299
+ toolCalls.length === 0 &&
300
+ toolResults.length === 0
301
+ }
302
+ isSystem={role === 'system'}
303
+ />
304
+ )}
305
+
306
+ {toolCalls.length > 0 && (
307
+ <div className="space-y-2">
308
+ {toolCalls.map((call, i) => (
309
+ <CollapsibleToolCall
310
+ key={i}
311
+ toolName={call.toolName}
312
+ toolCallId={call.toolCallId}
313
+ data={call.args ?? call.input}
314
+ />
315
+ ))}
316
+ </div>
317
+ )}
318
+
319
+ {toolResults.length > 0 && (
320
+ <div className="space-y-2">
321
+ {toolResults.map((result, i) => (
322
+ <CollapsibleToolResult
323
+ key={i}
324
+ toolName={result.toolName}
325
+ toolCallId={result.toolCallId}
326
+ data={result.result ?? result.output ?? result}
327
+ />
328
+ ))}
329
+ </div>
330
+ )}
331
+
332
+ {!textContent &&
333
+ !reasoningContent &&
334
+ toolCalls.length === 0 &&
335
+ toolResults.length === 0 && (
336
+ <div className="text-[11px] text-muted-foreground italic">
337
+ Empty message
338
+ </div>
339
+ )}
340
+ </div>
341
+ );
342
+ }
@@ -0,0 +1,145 @@
1
+ import React, { useState } from 'react';
2
+ import { ChevronRight, Wrench } from 'lucide-react';
3
+ import type {
4
+ ParsedOutput,
5
+ ContentPart,
6
+ ToolCallContentPart,
7
+ ToolResultContentPart,
8
+ TextContentPart,
9
+ ReasoningContentPart,
10
+ } from '../types';
11
+ import { safeParseJson, formatToolParams } from '../utils';
12
+ import { JsonBlock, ReasoningBlock, TextBlock } from './shared-components';
13
+
14
+ export function OutputDisplay({
15
+ output,
16
+ toolResults = [],
17
+ }: {
18
+ output: ParsedOutput;
19
+ toolResults?: ContentPart[];
20
+ }) {
21
+ const getToolResult = (toolCallId: string) => {
22
+ return toolResults.find(
23
+ (r): r is ToolResultContentPart =>
24
+ r.type === 'tool-result' &&
25
+ 'toolCallId' in r &&
26
+ r.toolCallId === toolCallId,
27
+ );
28
+ };
29
+
30
+ const toolCalls: ToolCallContentPart[] =
31
+ output?.toolCalls ||
32
+ (output?.content?.filter(
33
+ (p): p is ToolCallContentPart => p.type === 'tool-call',
34
+ ) ??
35
+ []);
36
+
37
+ const textParts: TextContentPart[] =
38
+ output?.textParts ||
39
+ (output?.content?.filter((p): p is TextContentPart => p.type === 'text') ??
40
+ []);
41
+
42
+ const reasoningParts: ReasoningContentPart[] =
43
+ output?.reasoningParts ||
44
+ (output?.content?.filter(
45
+ (p): p is ReasoningContentPart =>
46
+ p.type === 'thinking' || p.type === 'reasoning',
47
+ ) ??
48
+ []);
49
+
50
+ const textContent = textParts.map(p => p.text).join('');
51
+ const reasoningContent = reasoningParts
52
+ .map(p => p.text || p.thinking || p.reasoning || '')
53
+ .join('');
54
+
55
+ const isTextOnly = textContent && !reasoningContent && toolCalls.length === 0;
56
+
57
+ return (
58
+ <div className="space-y-3">
59
+ {reasoningContent && <ReasoningBlock content={reasoningContent} />}
60
+
61
+ {textContent && (
62
+ <TextBlock content={textContent} defaultExpanded={!!isTextOnly} />
63
+ )}
64
+
65
+ {toolCalls.map((call, i) => {
66
+ const result = call.toolCallId
67
+ ? getToolResult(call.toolCallId)
68
+ : undefined;
69
+ return (
70
+ <ToolCallCard
71
+ key={i}
72
+ toolName={call.toolName}
73
+ args={call.args ?? call.input}
74
+ result={result?.output ?? result?.result}
75
+ />
76
+ );
77
+ })}
78
+ </div>
79
+ );
80
+ }
81
+
82
+ function ToolCallCard({
83
+ toolName,
84
+ args,
85
+ result,
86
+ }: {
87
+ toolName: string;
88
+ args: Record<string, unknown> | string | undefined;
89
+ result?: unknown;
90
+ }) {
91
+ const [expanded, setExpanded] = useState(false);
92
+ const parsedArgs =
93
+ typeof args === 'string'
94
+ ? (safeParseJson(args) as Record<string, unknown>)
95
+ : args;
96
+ const parsedResult =
97
+ typeof result === 'string' ? safeParseJson(result) : result;
98
+
99
+ return (
100
+ <div className="rounded-md border border-purple/30 overflow-hidden">
101
+ <button
102
+ className="w-full flex items-center gap-2 px-3 py-2 bg-purple/10 hover:bg-purple/20 transition-colors"
103
+ onClick={() => setExpanded(!expanded)}
104
+ >
105
+ <ChevronRight
106
+ className={`size-3 text-purple transition-transform shrink-0 ${
107
+ expanded ? 'rotate-90' : ''
108
+ }`}
109
+ />
110
+ <Wrench className="size-3 text-purple shrink-0" />
111
+ <span className="text-xs font-mono font-medium text-purple">
112
+ {toolName}
113
+ </span>
114
+ {!expanded &&
115
+ parsedArgs &&
116
+ typeof parsedArgs === 'object' &&
117
+ !Array.isArray(parsedArgs) && (
118
+ <span className="text-[11px] font-mono text-purple/70 truncate">
119
+ {formatToolParams(parsedArgs)}
120
+ </span>
121
+ )}
122
+ </button>
123
+
124
+ {expanded && (
125
+ <>
126
+ <div className="p-3 bg-card/50 border-t border-purple/30">
127
+ <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mb-2">
128
+ Input
129
+ </div>
130
+ <JsonBlock data={parsedArgs} />
131
+ </div>
132
+
133
+ {parsedResult != null && (
134
+ <div className="p-3 border-t border-border bg-success/5">
135
+ <div className="text-[10px] font-medium uppercase tracking-wider text-success mb-2">
136
+ Output
137
+ </div>
138
+ <JsonBlock data={parsedResult} />
139
+ </div>
140
+ )}
141
+ </>
142
+ )}
143
+ </div>
144
+ );
145
+ }