mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
add file attachments
This commit is contained in:
parent
0ace16ede6
commit
398efd4f80
3 changed files with 103 additions and 6 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<HTMLTextAreaElement>(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<string[]>([])
|
||||
|
||||
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 (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
onDragOver={(e) => { if (e.dataTransfer?.types?.includes('Files')) e.preventDefault() }}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* Slim header — session controls live in the Code view's middle header */}
|
||||
<div className="flex items-center gap-2 border-b px-4 py-2">
|
||||
<Terminal className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
|
|
@ -207,6 +251,28 @@ export function CodeChat({
|
|||
borderless textarea, round primary send / destructive stop). */}
|
||||
<div className="p-3">
|
||||
<div className="rowboat-chat-input mx-auto w-full max-w-3xl rounded-lg border border-border bg-background shadow-none">
|
||||
{attachments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 px-4 pb-1 pt-3">
|
||||
{attachments.map((p) => (
|
||||
<span
|
||||
key={p}
|
||||
title={p}
|
||||
className="group inline-flex max-w-[260px] items-center gap-1.5 rounded-xl border border-border/50 bg-muted/80 px-2.5 py-1.5 text-xs"
|
||||
>
|
||||
<FileText className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="min-w-0 truncate">{basename(p)}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAttachments((prev) => prev.filter((x) => x !== p))}
|
||||
aria-label="Remove attachment"
|
||||
className="flex size-4 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="px-4 pb-2 pt-4">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
|
|
@ -224,6 +290,19 @@ export function CodeChat({
|
|||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 pb-3">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handlePickFiles()}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
aria-label="Attach files"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Attach files — the agent reads them from disk (or drag & drop)</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Terminal className="size-3.5 shrink-0" />
|
||||
<span className="truncate">Direct — straight to {AGENT_LABEL[session.agent]}</span>
|
||||
|
|
@ -247,11 +326,11 @@ export function CodeChat({
|
|||
<Button
|
||||
size="icon"
|
||||
onClick={() => void handleSend()}
|
||||
disabled={!draft.trim()}
|
||||
disabled={!canSend}
|
||||
title="Send"
|
||||
className={cn(
|
||||
'h-7 w-7 shrink-0 rounded-full transition-all',
|
||||
draft.trim()
|
||||
canSend
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
: 'bg-muted text-muted-foreground',
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -802,6 +802,15 @@ const ipcSchemas = {
|
|||
path: z.string().nullable(),
|
||||
}),
|
||||
},
|
||||
'dialog:openFiles': {
|
||||
req: z.object({
|
||||
defaultPath: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
}),
|
||||
res: z.object({
|
||||
paths: z.array(z.string()),
|
||||
}),
|
||||
},
|
||||
// Knowledge version history channels
|
||||
'knowledge:history': {
|
||||
req: z.object({ path: RelPath }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue