diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index a7fa35af..dc335f24 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -934,6 +934,15 @@ export function setupIpcHandlers() { } return { path: result.filePaths[0] ?? null }; }, + 'dialog:openFiles': async (event, args) => { + const win = BrowserWindow.fromWebContents(event.sender); + const result = await dialog.showOpenDialog(win!, { + title: args.title ?? 'Attach files', + ...(args.defaultPath ? { defaultPath: resolveShellPath(args.defaultPath) } : {}), + properties: ['openFile', 'multiSelections'], + }); + return { paths: result.canceled ? [] : result.filePaths }; + }, // Knowledge version history handlers 'knowledge:history': async (_event, args) => { const commits = await versionHistory.getFileHistory(args.path); diff --git a/apps/x/apps/renderer/src/components/code/code-chat.tsx b/apps/x/apps/renderer/src/components/code/code-chat.tsx index 8e9a9567..6f400deb 100644 --- a/apps/x/apps/renderer/src/components/code/code-chat.tsx +++ b/apps/x/apps/renderer/src/components/code/code-chat.tsx @@ -1,10 +1,11 @@ import { useEffect, useRef, useState } from 'react' -import { ArrowUp, Loader2, Square, Terminal } from 'lucide-react' +import { ArrowUp, FileText, Loader2, Plus, Square, Terminal, X } from 'lucide-react' import type { CodeSession, CodeSessionStatus } from '@x/shared/src/code-sessions.js' import { cn } from '@/lib/utils' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { Conversation, ConversationContent, ConversationScrollButton } from '@/components/ai-elements/conversation' import { MessageResponse } from '@/components/ai-elements/message' import { Shimmer } from '@/components/ai-elements/shimmer' @@ -110,9 +111,13 @@ export function CodeChat({ const textareaRef = useRef(null) const busy = isProcessing || status === 'working' || status === 'needs-you' + // Attached file PATHS — like dragging a file into the Claude Code CLI, the + // agent receives paths and reads the files itself with its own tools. + const [attachments, setAttachments] = useState([]) useEffect(() => { setDraft('') + setAttachments([]) setStopping(false) textareaRef.current?.focus() }, [session.id]) @@ -121,14 +126,47 @@ export function CodeChat({ if (!busy) setStopping(false) }, [busy]) + const addAttachments = (paths: string[]) => { + const cleaned = paths.filter(Boolean) + if (cleaned.length === 0) return + setAttachments((prev) => [...prev, ...cleaned.filter((p) => !prev.includes(p))]) + } + + const handlePickFiles = async () => { + const res = await window.ipc.invoke('dialog:openFiles', { + title: 'Attach files', + defaultPath: session.cwd, + }) + addAttachments(res.paths) + textareaRef.current?.focus() + } + + const handleDrop = (e: React.DragEvent) => { + if (!e.dataTransfer?.files?.length) return + e.preventDefault() + const paths = Array.from(e.dataTransfer.files) + .map((file) => window.electronUtils?.getPathForFile(file)) + .filter(Boolean) as string[] + addAttachments(paths) + } + + const canSend = (Boolean(draft.trim()) || attachments.length > 0) && !busy + const handleSend = async () => { + if (!canSend) return const text = draft.trim() - if (!text || busy) return + const files = attachments + // The agent gets paths, CLI-style; it reads them from disk on its own. + const message = files.length > 0 + ? `${text || 'Look at the attached files.'}\n\nAttached files (read them from disk):\n${files.map((p) => `- ${p}`).join('\n')}` + : text setDraft('') - const result = await send(text) + setAttachments([]) + const result = await send(message) if (!result.ok && result.error) { toast.error(result.error) setDraft(text) + setAttachments(files) } } @@ -137,8 +175,14 @@ export function CodeChat({ await stop() } + const basename = (p: string) => p.split(/[\\/]/).pop() || p + return ( -
+
{ if (e.dataTransfer?.types?.includes('Files')) e.preventDefault() }} + onDrop={handleDrop} + > {/* Slim header — session controls live in the Code view's middle header */}
@@ -207,6 +251,28 @@ export function CodeChat({ borderless textarea, round primary send / destructive stop). */}
+ {attachments.length > 0 && ( +
+ {attachments.map((p) => ( + + + {basename(p)} + + + ))} +
+ )}