@ai-sdk/devtools 1.0.8 → 1.0.9

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.
@@ -10,8 +10,8 @@
10
10
  href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600;700&display=swap"
11
11
  rel="stylesheet"
12
12
  />
13
- <script type="module" crossorigin src="/assets/index-Beo4T2zg.js"></script>
14
- <link rel="stylesheet" crossorigin href="/assets/index-CIZI8r26.css">
13
+ <script type="module" crossorigin src="/assets/index-DgZG5_rI.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/assets/index-CltCAda7.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
package/dist/index.js CHANGED
@@ -122,6 +122,83 @@ var getStepsForRun = async (runId) => {
122
122
  return db.steps.filter((s) => s.run_id === runId).sort((a, b) => a.step_number - b.step_number);
123
123
  };
124
124
 
125
+ // src/serialize.ts
126
+ function normalizeBinaryData(value) {
127
+ if (value instanceof ArrayBuffer) {
128
+ return Buffer.from(value).toString("base64");
129
+ }
130
+ if (ArrayBuffer.isView(value)) {
131
+ return Buffer.from(
132
+ value.buffer,
133
+ value.byteOffset,
134
+ value.byteLength
135
+ ).toString("base64");
136
+ }
137
+ return value;
138
+ }
139
+ function isRecord(value) {
140
+ return value != null && typeof value === "object" && !Array.isArray(value);
141
+ }
142
+ function collectMediaBinaryValues(value) {
143
+ const mediaBinaryValues = /* @__PURE__ */ new WeakSet();
144
+ const visited = /* @__PURE__ */ new WeakSet();
145
+ const markBinary = (candidate) => {
146
+ if (candidate instanceof ArrayBuffer || ArrayBuffer.isView(candidate)) {
147
+ mediaBinaryValues.add(candidate);
148
+ }
149
+ };
150
+ const markGeneratedFile = (candidate) => {
151
+ if (!isRecord(candidate)) return;
152
+ if (Object.hasOwn(candidate, "uint8Array")) {
153
+ markBinary(candidate.uint8Array);
154
+ }
155
+ markBinary(candidate.uint8ArrayData);
156
+ };
157
+ const markDataContent = (candidate) => {
158
+ markBinary(candidate);
159
+ if (!isRecord(candidate)) return;
160
+ if (candidate.type === "data") {
161
+ markBinary(candidate.data);
162
+ }
163
+ };
164
+ const visit = (candidate) => {
165
+ if (candidate == null || typeof candidate !== "object" || candidate instanceof ArrayBuffer || ArrayBuffer.isView(candidate) || visited.has(candidate)) {
166
+ return;
167
+ }
168
+ visited.add(candidate);
169
+ if (isRecord(candidate) && typeof candidate.type === "string") {
170
+ switch (candidate.type) {
171
+ case "file":
172
+ case "reasoning-file":
173
+ markDataContent(candidate.data);
174
+ markGeneratedFile(candidate);
175
+ markGeneratedFile(candidate.file);
176
+ break;
177
+ case "image":
178
+ markDataContent(candidate.image);
179
+ break;
180
+ case "media":
181
+ case "file-data":
182
+ case "image-data":
183
+ markBinary(candidate.data);
184
+ break;
185
+ }
186
+ }
187
+ for (const child of Array.isArray(candidate) ? candidate : Object.values(candidate)) {
188
+ visit(child);
189
+ }
190
+ };
191
+ visit(value);
192
+ return mediaBinaryValues;
193
+ }
194
+ function serializeForDevTools(value) {
195
+ const mediaBinaryValues = collectMediaBinaryValues(value);
196
+ return JSON.stringify(value, function(key, serializedValue) {
197
+ const originalValue = this[key];
198
+ return originalValue != null && typeof originalValue === "object" && mediaBinaryValues.has(originalValue) ? normalizeBinaryData(originalValue) : serializedValue;
199
+ });
200
+ }
201
+
125
202
  // src/middleware.ts
126
203
  var generateId = () => crypto.randomUUID();
127
204
  var activeSteps = /* @__PURE__ */ new Map();
@@ -136,7 +213,7 @@ var registerSignalHandlers = () => {
136
213
  const durationMs = Date.now() - data.startTime;
137
214
  await updateStepResult(stepId, {
138
215
  duration_ms: durationMs,
139
- output: JSON.stringify(data.collectedOutput),
216
+ output: serializeForDevTools(data.collectedOutput),
140
217
  usage: null,
141
218
  error: "Request aborted",
142
219
  raw_request: data.request && typeof data.request === "object" && "body" in data.request ? JSON.stringify(data.request.body) : null,
@@ -197,7 +274,7 @@ var devToolsMiddleware = () => {
197
274
  // @ts-expect-error broken type
198
275
  provider: model.config?.provider,
199
276
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
200
- input: JSON.stringify({
277
+ input: serializeForDevTools({
201
278
  prompt: params.prompt,
202
279
  tools: params.tools,
203
280
  toolChoice: params.toolChoice,
@@ -217,7 +294,7 @@ var devToolsMiddleware = () => {
217
294
  const durationMs = Date.now() - startTime;
218
295
  await updateStepResult(stepId, {
219
296
  duration_ms: durationMs,
220
- output: JSON.stringify({
297
+ output: serializeForDevTools({
221
298
  content: result.content,
222
299
  finishReason: result.finishReason,
223
300
  response: result.response
@@ -257,7 +334,7 @@ var devToolsMiddleware = () => {
257
334
  // @ts-expect-error broken type
258
335
  provider: model.config?.provider,
259
336
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
260
- input: JSON.stringify({
337
+ input: serializeForDevTools({
261
338
  prompt: params.prompt,
262
339
  tools: params.tools,
263
340
  toolChoice: params.toolChoice,
@@ -334,6 +411,11 @@ var devToolsMiddleware = () => {
334
411
  case "tool-call":
335
412
  collectedOutput.toolCalls.push(chunk);
336
413
  break;
414
+ case "file":
415
+ case "reasoning-file":
416
+ case "tool-result":
417
+ (collectedOutput.content ??= []).push(chunk);
418
+ break;
337
419
  case "finish":
338
420
  collectedOutput.finishReason = chunk.finishReason;
339
421
  collectedOutput.usage = chunk.usage;
@@ -346,7 +428,7 @@ var devToolsMiddleware = () => {
346
428
  const durationMs = Date.now() - startTime;
347
429
  await updateStepResult(stepId, {
348
430
  duration_ms: durationMs,
349
- output: JSON.stringify(collectedOutput),
431
+ output: serializeForDevTools(collectedOutput),
350
432
  usage: collectedOutput.usage ? JSON.stringify(collectedOutput.usage) : null,
351
433
  error: null,
352
434
  raw_request: request?.body ? JSON.stringify(request.body) : null,
@@ -360,7 +442,7 @@ var devToolsMiddleware = () => {
360
442
  const durationMs = Date.now() - startTime;
361
443
  await updateStepResult(stepId, {
362
444
  duration_ms: durationMs,
363
- output: JSON.stringify(collectedOutput),
445
+ output: serializeForDevTools(collectedOutput),
364
446
  usage: collectedOutput.usage ? JSON.stringify(collectedOutput.usage) : null,
365
447
  error: "Request aborted",
366
448
  raw_request: request?.body ? JSON.stringify(request.body) : null,
@@ -527,7 +609,7 @@ function DevToolsTelemetry(options = {}) {
527
609
  model_id: stepStartEvent.modelId,
528
610
  provider: stepStartEvent.provider ?? null,
529
611
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
530
- input: JSON.stringify({
612
+ input: serializeForDevTools({
531
613
  prompt,
532
614
  tools: stepStartEvent.tools ? Object.entries(stepStartEvent.tools).map(([name, tool]) => ({
533
615
  name,
@@ -566,7 +648,7 @@ function DevToolsTelemetry(options = {}) {
566
648
  model_id: stepStartEvent.modelId,
567
649
  provider: stepStartEvent.provider ?? null,
568
650
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
569
- input: JSON.stringify({
651
+ input: serializeForDevTools({
570
652
  prompt: stepStartEvent.promptMessages,
571
653
  maxOutputTokens: state.settings.maxOutputTokens,
572
654
  temperature: state.settings.temperature,
@@ -599,7 +681,7 @@ function DevToolsTelemetry(options = {}) {
599
681
  };
600
682
  await updateStepResult(stepState.stepId, {
601
683
  duration_ms: durationMs,
602
- output: JSON.stringify(output),
684
+ output: serializeForDevTools(output),
603
685
  usage: stepResult.usage ? JSON.stringify(stepResult.usage) : null,
604
686
  error: null,
605
687
  raw_request: stepResult.request?.body ? JSON.stringify(stepResult.request.body) : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/devtools",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -55,7 +55,7 @@
55
55
  "vite": "^6.4.3",
56
56
  "vitest": "^4.1.6",
57
57
  "zod": "3.25.76",
58
- "ai": "7.0.38"
58
+ "ai": "7.0.42"
59
59
  },
60
60
  "publishConfig": {
61
61
  "access": "public",
@@ -15,6 +15,7 @@ import {
15
15
  updateStepResult,
16
16
  notifyServerAsync,
17
17
  } from './db.js';
18
+ import { serializeForDevTools } from './serialize.js';
18
19
 
19
20
  type OperationType = 'generate' | 'stream';
20
21
 
@@ -261,7 +262,7 @@ export function DevToolsTelemetry(
261
262
  model_id: stepStartEvent.modelId,
262
263
  provider: stepStartEvent.provider ?? null,
263
264
  started_at: new Date().toISOString(),
264
- input: JSON.stringify({
265
+ input: serializeForDevTools({
265
266
  prompt,
266
267
  tools: stepStartEvent.tools
267
268
  ? Object.entries(stepStartEvent.tools).map(([name, tool]) => ({
@@ -311,7 +312,7 @@ export function DevToolsTelemetry(
311
312
  model_id: stepStartEvent.modelId,
312
313
  provider: stepStartEvent.provider ?? null,
313
314
  started_at: new Date().toISOString(),
314
- input: JSON.stringify({
315
+ input: serializeForDevTools({
315
316
  prompt: stepStartEvent.promptMessages,
316
317
  maxOutputTokens: state.settings.maxOutputTokens,
317
318
  temperature: state.settings.temperature,
@@ -353,7 +354,7 @@ export function DevToolsTelemetry(
353
354
 
354
355
  await updateStepResult(stepState.stepId, {
355
356
  duration_ms: durationMs,
356
- output: JSON.stringify(output),
357
+ output: serializeForDevTools(output),
357
358
  usage: stepResult.usage ? JSON.stringify(stepResult.usage) : null,
358
359
  error: null,
359
360
  raw_request: stepResult.request?.body
package/src/middleware.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  updateStepResult,
11
11
  notifyServerAsync,
12
12
  } from './db.js';
13
+ import { serializeForDevTools } from './serialize.js';
13
14
 
14
15
  const generateId = () => crypto.randomUUID();
15
16
 
@@ -39,7 +40,7 @@ const registerSignalHandlers = () => {
39
40
  const durationMs = Date.now() - data.startTime;
40
41
  await updateStepResult(stepId, {
41
42
  duration_ms: durationMs,
42
- output: JSON.stringify(data.collectedOutput),
43
+ output: serializeForDevTools(data.collectedOutput),
43
44
  usage: null,
44
45
  error: 'Request aborted',
45
46
  raw_request:
@@ -143,7 +144,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
143
144
  // @ts-expect-error broken type
144
145
  provider: model.config?.provider,
145
146
  started_at: new Date().toISOString(),
146
- input: JSON.stringify({
147
+ input: serializeForDevTools({
147
148
  prompt: params.prompt,
148
149
  tools: params.tools,
149
150
  toolChoice: params.toolChoice,
@@ -167,7 +168,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
167
168
 
168
169
  await updateStepResult(stepId, {
169
170
  duration_ms: durationMs,
170
- output: JSON.stringify({
171
+ output: serializeForDevTools({
171
172
  content: result.content,
172
173
  finishReason: result.finishReason,
173
174
  response: result.response,
@@ -217,7 +218,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
217
218
  // @ts-expect-error broken type
218
219
  provider: model.config?.provider,
219
220
  started_at: new Date().toISOString(),
220
- input: JSON.stringify({
221
+ input: serializeForDevTools({
221
222
  prompt: params.prompt,
222
223
  tools: params.tools,
223
224
  toolChoice: params.toolChoice,
@@ -243,6 +244,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
243
244
  textParts: Array<{ id: string; text: string }>;
244
245
  reasoningParts: Array<{ id: string; text: string }>;
245
246
  toolCalls: LanguageModelV4StreamPart[];
247
+ content?: LanguageModelV4StreamPart[];
246
248
  finishReason?: LanguageModelV4FinishReason;
247
249
  usage?: LanguageModelV4Usage;
248
250
  } = {
@@ -319,6 +321,11 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
319
321
  case 'tool-call':
320
322
  collectedOutput.toolCalls.push(chunk);
321
323
  break;
324
+ case 'file':
325
+ case 'reasoning-file':
326
+ case 'tool-result':
327
+ (collectedOutput.content ??= []).push(chunk);
328
+ break;
322
329
  case 'finish':
323
330
  collectedOutput.finishReason = chunk.finishReason;
324
331
  collectedOutput.usage = chunk.usage;
@@ -335,7 +342,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
335
342
  const durationMs = Date.now() - startTime;
336
343
  await updateStepResult(stepId, {
337
344
  duration_ms: durationMs,
338
- output: JSON.stringify(collectedOutput),
345
+ output: serializeForDevTools(collectedOutput),
339
346
  usage: collectedOutput.usage
340
347
  ? JSON.stringify(collectedOutput.usage)
341
348
  : null,
@@ -354,7 +361,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
354
361
  const durationMs = Date.now() - startTime;
355
362
  await updateStepResult(stepId, {
356
363
  duration_ms: durationMs,
357
- output: JSON.stringify(collectedOutput),
364
+ output: serializeForDevTools(collectedOutput),
358
365
  usage: collectedOutput.usage
359
366
  ? JSON.stringify(collectedOutput.usage)
360
367
  : null,
@@ -0,0 +1,111 @@
1
+ function normalizeBinaryData(value: unknown): unknown {
2
+ if (value instanceof ArrayBuffer) {
3
+ return Buffer.from(value).toString('base64');
4
+ }
5
+
6
+ if (ArrayBuffer.isView(value)) {
7
+ return Buffer.from(
8
+ value.buffer,
9
+ value.byteOffset,
10
+ value.byteLength,
11
+ ).toString('base64');
12
+ }
13
+
14
+ return value;
15
+ }
16
+
17
+ function isRecord(value: unknown): value is Record<string, unknown> {
18
+ return value != null && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+
21
+ function collectMediaBinaryValues(value: unknown): WeakSet<object> {
22
+ const mediaBinaryValues = new WeakSet<object>();
23
+ const visited = new WeakSet<object>();
24
+
25
+ const markBinary = (candidate: unknown) => {
26
+ if (candidate instanceof ArrayBuffer || ArrayBuffer.isView(candidate)) {
27
+ mediaBinaryValues.add(candidate);
28
+ }
29
+ };
30
+
31
+ const markGeneratedFile = (candidate: unknown) => {
32
+ if (!isRecord(candidate)) return;
33
+
34
+ if (Object.hasOwn(candidate, 'uint8Array')) {
35
+ markBinary(candidate.uint8Array);
36
+ }
37
+ markBinary(candidate.uint8ArrayData);
38
+ };
39
+
40
+ const markDataContent = (candidate: unknown) => {
41
+ markBinary(candidate);
42
+
43
+ if (!isRecord(candidate)) return;
44
+
45
+ if (candidate.type === 'data') {
46
+ markBinary(candidate.data);
47
+ }
48
+ };
49
+
50
+ const visit = (candidate: unknown) => {
51
+ if (
52
+ candidate == null ||
53
+ typeof candidate !== 'object' ||
54
+ candidate instanceof ArrayBuffer ||
55
+ ArrayBuffer.isView(candidate) ||
56
+ visited.has(candidate)
57
+ ) {
58
+ return;
59
+ }
60
+ visited.add(candidate);
61
+
62
+ if (isRecord(candidate) && typeof candidate.type === 'string') {
63
+ switch (candidate.type) {
64
+ case 'file':
65
+ case 'reasoning-file':
66
+ markDataContent(candidate.data);
67
+ markGeneratedFile(candidate);
68
+ markGeneratedFile(candidate.file);
69
+ break;
70
+ case 'image':
71
+ markDataContent(candidate.image);
72
+ break;
73
+ case 'media':
74
+ case 'file-data':
75
+ case 'image-data':
76
+ markBinary(candidate.data);
77
+ break;
78
+ }
79
+ }
80
+
81
+ for (const child of Array.isArray(candidate)
82
+ ? candidate
83
+ : Object.values(candidate)) {
84
+ visit(child);
85
+ }
86
+ };
87
+
88
+ visit(value);
89
+ return mediaBinaryValues;
90
+ }
91
+
92
+ /**
93
+ * Serializes captured DevTools values while preserving binary values in
94
+ * recognized media-bearing fields as base64. Unrelated binary values retain
95
+ * their normal JSON.stringify representation.
96
+ */
97
+ export function serializeForDevTools(value: unknown): string {
98
+ const mediaBinaryValues = collectMediaBinaryValues(value);
99
+
100
+ return JSON.stringify(value, function (key, serializedValue) {
101
+ // JSON.stringify invokes toJSON before the replacer. Read the original
102
+ // property from the holder so binary values can still be normalized while
103
+ // preserving custom toJSON behavior for every other object.
104
+ const originalValue = (this as Record<string, unknown>)[key];
105
+ return originalValue != null &&
106
+ typeof originalValue === 'object' &&
107
+ mediaBinaryValues.has(originalValue)
108
+ ? normalizeBinaryData(originalValue)
109
+ : serializedValue;
110
+ });
111
+ }
@@ -0,0 +1,127 @@
1
+ import React, { useState } from 'react';
2
+ import { Download, ExternalLink, File, Image } from 'lucide-react';
3
+ import { findMediaPreviews, type MediaPreviewData } from '../media';
4
+
5
+ const anonymousMediaProps = {
6
+ crossOrigin: 'anonymous' as const,
7
+ referrerPolicy: 'no-referrer' as const,
8
+ };
9
+
10
+ export function MediaPreviewList({ data }: { data: unknown }) {
11
+ const previews = findMediaPreviews(data);
12
+
13
+ if (previews.length === 0) {
14
+ return null;
15
+ }
16
+
17
+ return (
18
+ <div className="grid gap-2 sm:grid-cols-2">
19
+ {previews.map((preview, index) => (
20
+ <MediaPreviewCard key={index} preview={preview} />
21
+ ))}
22
+ </div>
23
+ );
24
+ }
25
+
26
+ function MediaPreviewCard({ preview }: { preview: MediaPreviewData }) {
27
+ const [loadRemotePreview, setLoadRemotePreview] = useState(false);
28
+ const canRender =
29
+ preview.source != null &&
30
+ (preview.kind === 'image' ||
31
+ preview.kind === 'audio' ||
32
+ preview.kind === 'video');
33
+ const shouldRender =
34
+ canRender && (preview.sourceType === 'inline' || loadRemotePreview);
35
+ const label = preview.filename ?? `${preview.kind} preview`;
36
+
37
+ return (
38
+ <div className="overflow-hidden rounded-md border border-border bg-background">
39
+ <div className="flex items-center gap-2 border-b border-border px-2.5 py-2">
40
+ {preview.kind === 'image' ? (
41
+ <Image className="size-3.5 text-info" aria-hidden="true" />
42
+ ) : (
43
+ <File className="size-3.5 text-info" aria-hidden="true" />
44
+ )}
45
+ <span className="min-w-0 flex-1 truncate text-xs font-medium">
46
+ {label}
47
+ </span>
48
+ <span className="text-[10px] font-mono text-muted-foreground">
49
+ {preview.mediaType}
50
+ </span>
51
+ </div>
52
+
53
+ {shouldRender && preview.kind === 'image' && (
54
+ <img
55
+ alt={label}
56
+ className="max-h-72 w-full bg-muted/30 object-contain"
57
+ decoding="async"
58
+ loading="lazy"
59
+ src={preview.source}
60
+ {...anonymousMediaProps}
61
+ />
62
+ )}
63
+
64
+ {shouldRender && preview.kind === 'audio' && (
65
+ // Captured media parts do not include caption tracks.
66
+ // oxlint-disable-next-line jsx-a11y/media-has-caption
67
+ <audio
68
+ className="w-full p-2"
69
+ controls
70
+ preload="none"
71
+ src={preview.source}
72
+ {...anonymousMediaProps}
73
+ />
74
+ )}
75
+
76
+ {shouldRender && preview.kind === 'video' && (
77
+ // Captured media parts do not include caption tracks.
78
+ // oxlint-disable-next-line jsx-a11y/media-has-caption
79
+ <video
80
+ className="max-h-72 w-full bg-muted/30"
81
+ controls
82
+ preload="metadata"
83
+ src={preview.source}
84
+ {...anonymousMediaProps}
85
+ />
86
+ )}
87
+
88
+ <div className="flex items-center gap-2 px-2.5 py-2">
89
+ {canRender && preview.sourceType === 'remote' && !loadRemotePreview && (
90
+ <button
91
+ className="text-xs font-medium text-info hover:underline"
92
+ onClick={() => setLoadRemotePreview(true)}
93
+ type="button"
94
+ >
95
+ Load preview
96
+ </button>
97
+ )}
98
+
99
+ {preview.source != null && (
100
+ <a
101
+ className="ml-auto inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
102
+ download={
103
+ preview.sourceType === 'inline' ? preview.filename : undefined
104
+ }
105
+ href={preview.source}
106
+ rel="noreferrer"
107
+ target="_blank"
108
+ >
109
+ {preview.sourceType === 'inline' ? (
110
+ <Download className="size-3" aria-hidden="true" />
111
+ ) : (
112
+ <ExternalLink className="size-3" aria-hidden="true" />
113
+ )}
114
+ {preview.sourceType === 'inline' ? 'Download' : 'Open'}
115
+ </a>
116
+ )}
117
+
118
+ {preview.source == null && (
119
+ <span className="text-xs text-muted-foreground">
120
+ {preview.unavailableReason ??
121
+ 'Preview unavailable; inspect the JSON metadata below.'}
122
+ </span>
123
+ )}
124
+ </div>
125
+ </div>
126
+ );
127
+ }
@@ -21,9 +21,11 @@ import {
21
21
  import {
22
22
  CollapsibleToolCall,
23
23
  CollapsibleToolResult,
24
+ MediaAwareValue,
24
25
  ReasoningBlock,
25
26
  TextBlock,
26
27
  } from './shared-components';
28
+ import { findMediaPreviews } from '../media';
27
29
 
28
30
  export function InputPanel({ input }: { input: ParsedInput | null }) {
29
31
  const messages: PromptMessage[] = input?.prompt ?? [];
@@ -128,6 +130,17 @@ function getReasoningContent(content: string | ContentPart[]): string {
128
130
  return '';
129
131
  }
130
132
 
133
+ function getDirectMediaContent(content: string | ContentPart[]): ContentPart[] {
134
+ return Array.isArray(content)
135
+ ? content.filter(
136
+ part =>
137
+ part.type !== 'tool-call' &&
138
+ part.type !== 'tool-result' &&
139
+ findMediaPreviews(part).length > 0,
140
+ )
141
+ : [];
142
+ }
143
+
131
144
  function InputMessagePreview({
132
145
  message,
133
146
  index,
@@ -148,12 +161,14 @@ function InputMessagePreview({
148
161
  const toolCalls = getToolCalls(content);
149
162
  const toolResults = getToolResults(content);
150
163
  const reasoningContent = getReasoningContent(content);
164
+ const mediaCount = getDirectMediaContent(content).length;
151
165
 
152
166
  const partCount =
153
167
  (textContent ? 1 : 0) +
154
168
  (reasoningContent ? 1 : 0) +
155
169
  toolCalls.length +
156
- toolResults.length;
170
+ toolResults.length +
171
+ mediaCount;
157
172
 
158
173
  return (
159
174
  <div className="rounded-md border border-border/50 bg-background/50 p-2.5 space-y-2">
@@ -183,6 +198,12 @@ function InputMessagePreview({
183
198
  </div>
184
199
  )}
185
200
 
201
+ {mediaCount > 0 && (
202
+ <div className="text-[11px] text-muted-foreground">
203
+ {mediaCount} media {mediaCount === 1 ? 'part' : 'parts'}
204
+ </div>
205
+ )}
206
+
186
207
  {toolCalls.length > 0 && (
187
208
  <div className="space-y-1">
188
209
  {toolCalls.slice(0, 3).map((call, i) => {
@@ -235,7 +256,8 @@ function InputMessagePreview({
235
256
  {!textContent &&
236
257
  !reasoningContent &&
237
258
  toolCalls.length === 0 &&
238
- toolResults.length === 0 && (
259
+ toolResults.length === 0 &&
260
+ mediaCount === 0 && (
239
261
  <div className="text-[11px] text-muted-foreground italic">
240
262
  Empty message
241
263
  </div>
@@ -264,12 +286,15 @@ export function MessageBubble({
264
286
  const toolCalls = getToolCalls(content);
265
287
  const toolResults = getToolResults(content);
266
288
  const reasoningContent = getReasoningContent(content);
289
+ const directMediaContent = getDirectMediaContent(content);
290
+ const mediaCount = directMediaContent.length;
267
291
 
268
292
  const partCount =
269
293
  (textContent ? 1 : 0) +
270
294
  (reasoningContent ? 1 : 0) +
271
295
  toolCalls.length +
272
- toolResults.length;
296
+ toolResults.length +
297
+ mediaCount;
273
298
 
274
299
  return (
275
300
  <div className="rounded-md border border-border/50 bg-background/50 p-3 space-y-2">
@@ -303,6 +328,10 @@ export function MessageBubble({
303
328
  />
304
329
  )}
305
330
 
331
+ {directMediaContent.length > 0 && (
332
+ <MediaAwareValue data={directMediaContent} />
333
+ )}
334
+
306
335
  {toolCalls.length > 0 && (
307
336
  <div className="space-y-2">
308
337
  {toolCalls.map((call, i) => (
@@ -332,7 +361,8 @@ export function MessageBubble({
332
361
  {!textContent &&
333
362
  !reasoningContent &&
334
363
  toolCalls.length === 0 &&
335
- toolResults.length === 0 && (
364
+ toolResults.length === 0 &&
365
+ mediaCount === 0 && (
336
366
  <div className="text-[11px] text-muted-foreground italic">
337
367
  Empty message
338
368
  </div>