diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 75d241b..3da3714 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -30,13 +30,14 @@ export async function POST(request: NextRequest) { // Save user message — persist file metadata so attachments survive page reload let savedContent = content; + let fileMeta: Array<{ id: string; name: string; type: string; size: number; filePath: string }> | undefined; if (files && files.length > 0) { const workDir = session.working_directory || process.cwd(); const uploadDir = path.join(workDir, '.codepilot-uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } - const fileMeta = files.map((f) => { + fileMeta = files.map((f) => { const safeName = path.basename(f.name).replace(/[^a-zA-Z0-9._-]/g, '_'); const filePath = path.join(uploadDir, `${Date.now()}-${safeName}`); const buffer = Buffer.from(f.data, 'base64'); @@ -81,15 +82,21 @@ export async function POST(request: NextRequest) { abortController.abort(); }); - // Convert file attachments to the format expected by streamClaude + // Convert file attachments to the format expected by streamClaude. + // Include filePath from the already-saved files so claude-client can + // reference the on-disk copies instead of writing them again. const fileAttachments: FileAttachment[] | undefined = files && files.length > 0 - ? files.map((f, i) => ({ - id: f.id || `file-${Date.now()}-${i}`, - name: f.name, - type: f.type, - size: f.size, - data: f.data, - })) + ? files.map((f, i) => { + const meta = fileMeta?.find((m: { id: string }) => m.id === f.id); + return { + id: f.id || `file-${Date.now()}-${i}`, + name: f.name, + type: f.type, + size: f.size, + data: f.data, + filePath: meta?.filePath, + }; + }) : undefined; // Stream Claude response, using SDK session ID for resume if available diff --git a/src/lib/claude-client.ts b/src/lib/claude-client.ts index d39c719..deae504 100644 --- a/src/lib/claude-client.ts +++ b/src/lib/claude-client.ts @@ -135,26 +135,32 @@ function extractTokenUsage(msg: SDKResultMessage): TokenUsage | null { * Returns a ReadableStream of SSE-formatted strings. */ /** - * Save non-image file attachments to a temporary upload directory - * and return the file paths. The files are placed in .codepilot-uploads/ - * under the working directory so Claude's Read tool can access them. + * Get file paths for non-image attachments. If the file already has a + * persisted filePath (written by the uploads route), reuse it. Otherwise + * fall back to writing the file to .codepilot-uploads/. */ -function saveUploadedFiles(files: FileAttachment[], workDir: string): string[] { - const uploadDir = path.join(workDir, '.codepilot-uploads'); - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); - } - const savedPaths: string[] = []; +function getUploadedFilePaths(files: FileAttachment[], workDir: string): string[] { + const paths: string[] = []; + let uploadDir: string | undefined; for (const file of files) { - // Sanitize filename to prevent directory traversal - const safeName = path.basename(file.name).replace(/[^a-zA-Z0-9._-]/g, '_'); - const timestamp = Date.now(); - const filePath = path.join(uploadDir, `${timestamp}-${safeName}`); - const buffer = Buffer.from(file.data, 'base64'); - fs.writeFileSync(filePath, buffer); - savedPaths.push(filePath); + if (file.filePath) { + paths.push(file.filePath); + } else { + // Fallback: write file to disk (should not happen in normal flow) + if (!uploadDir) { + uploadDir = path.join(workDir, '.codepilot-uploads'); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + } + const safeName = path.basename(file.name).replace(/[^a-zA-Z0-9._-]/g, '_'); + const filePath = path.join(uploadDir, `${Date.now()}-${safeName}`); + const buffer = Buffer.from(file.data, 'base64'); + fs.writeFileSync(filePath, buffer); + paths.push(filePath); + } } - return savedPaths; + return paths; } export function streamClaude(options: ClaudeStreamOptions): ReadableStream { @@ -409,7 +415,7 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream 0) { const workDir = workingDirectory || process.cwd(); - const savedPaths = saveUploadedFiles(nonImageFiles, workDir); + const savedPaths = getUploadedFilePaths(nonImageFiles, workDir); const fileReferences = savedPaths .map((p, i) => `[User attached file: ${p} (${nonImageFiles[i].name})]`) .join('\n');