@@ -18,9 +18,13 @@ import { buildQueue } from '@/lib/queue';
1818import { getSandboxConfig , getSandboxManager , SandboxClient } from '@/lib/sandbox' ;
1919import { getSandboxDatabaseUrl } from '@/lib/sandbox/database' ;
2020import { MessageAttachmentPayload , uploadFile } from '@/lib/storage' ;
21+ import type { ImageUrlContent } from '@/lib/types' ;
2122import * as Sentry from '@sentry/nextjs' ;
2223import { eq } from 'drizzle-orm' ;
2324
25+ // Supported image media types for Claude multipart prompts
26+ const IMAGE_MEDIA_TYPES = [ 'image/jpeg' , 'image/png' , 'image/gif' , 'image/webp' ] as const ;
27+
2428// Schema for updating a chat session
2529const updateChatSessionSchema = z . object ( {
2630 title : z . string ( ) . min ( 1 ) . max ( 100 ) . optional ( ) ,
@@ -65,7 +69,7 @@ async function saveUploadedFile(file: File, projectId: string): Promise<MessageA
6569}
6670
6771/**
68- * Process a FormData request and extract the content and attachment
72+ * Process a FormData request and extract the content and attachments
6973 */
7074async function processFormDataRequest (
7175 req : NextRequest ,
@@ -89,6 +93,7 @@ async function processFormDataRequest(
8993 for ( let i = 0 ; i < attachmentCount ; i ++ ) {
9094 const attachmentFile = formData . get ( `attachment_${ i } ` ) as File | null ;
9195 if ( attachmentFile ) {
96+ // Upload to S3 for storage/display
9297 const attachment = await saveUploadedFile ( attachmentFile , projectId ) ;
9398 attachments . push ( attachment ) ;
9499 }
@@ -358,6 +363,7 @@ export async function POST(
358363 const contentType = req . headers . get ( 'content-type' ) || '' ;
359364 let messageContent : string ;
360365 let attachmentPayloads : MessageAttachmentPayload [ ] = [ ] ;
366+ let imageUrls : ImageUrlContent [ ] = [ ] ; // URL-based images for Claude (fetched by CLI)
361367
362368 if ( contentType . includes ( 'multipart/form-data' ) ) {
363369 // Process FormData request (for file uploads)
@@ -366,12 +372,27 @@ export async function POST(
366372 messageContent = formData . content ;
367373 attachmentPayloads = formData . attachments ;
368374
375+ // Build URL-based image objects from uploaded attachments
376+ // CLI will fetch these URLs and convert to base64 for Claude
377+ imageUrls = attachmentPayloads
378+ . filter ( a =>
379+ IMAGE_MEDIA_TYPES . includes ( a . upload . mediaType as ( typeof IMAGE_MEDIA_TYPES ) [ number ] )
380+ )
381+ . map ( a => ( {
382+ mediaType : a . upload . mediaType as ImageUrlContent [ 'mediaType' ] ,
383+ url : a . upload . fileUrl ,
384+ } ) ) ;
385+
369386 if ( attachmentPayloads . length > 0 ) {
370- console . log ( `⬆️ ${ attachmentPayloads . length } file(s) uploaded ` ) ;
387+ console . log ( `📎 ${ attachmentPayloads . length } file(s) attached ` ) ;
371388 attachmentPayloads . forEach ( ( attachment , index ) => {
372- console . log ( `⬆️ Attachment [${ index + 1 } ] uploaded : ${ attachment . upload . fileUrl } ` ) ;
389+ console . log ( `📎 Attachment [${ index + 1 } ]: ${ attachment . upload . fileUrl } ` ) ;
373390 } ) ;
374391 }
392+
393+ if ( imageUrls . length > 0 ) {
394+ console . log ( `🖼️ ${ imageUrls . length } image URL(s) prepared for Claude` ) ;
395+ }
375396 } else {
376397 // Process JSON request for text messages
377398 console . log ( 'Processing JSON request for streaming' ) ;
@@ -533,6 +554,7 @@ export async function POST(
533554 // Stream events from kosuke serve /api/plan (plan phase only)
534555 const planStream = sandboxClient . streamPlan ( messageContent , '/app/project' , {
535556 resume : claudeSessionId , // Resume previous conversation if exists
557+ ...( imageUrls . length > 0 && { images : imageUrls } ) , // Include image URLs (fetched by CLI)
536558 } ) ;
537559
538560 for await ( const event of planStream ) {
0 commit comments