From 4906dcfc256b70e8753881bf25fdb80e9312f73c Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 9 Jun 2026 16:50:08 -0500 Subject: [PATCH] fix(desktop): stage dropped files into the remote session workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finder/OS drops became `@file:/Users/...` refs that only resolve when the gateway shares the local disk, so on a remote gateway non-image files (PDF/CSV/Markdown/...) never reached the agent. Route OS drops through the file.attach / image.attach_bytes upload pipeline — in-app project-tree and gutter drags stay inline workspace-relative refs — across every drop surface: the conversation area, the composer form, the contenteditable input, and the message-edit composer (which still reproduced the bug). Also: - upload dropped files eagerly when a session exists, so the card shows a spinner instead of stalling the send (images stay submit-time to avoid racing their thumbnail write); - round the attachment card and drop the monospace detail; - render image previews from the bytes we already hold, so a pasted/dropped screenshot shows its thumbnail and previews even when its only on-disk copy is a transient path (the data URL is not persisted to localStorage). Supersedes #38615, #41203. Co-authored-by: LeonSGP <154585401+LeonSGP43@users.noreply.github.com> Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> --- .../src/app/chat/composer/attachments.tsx | 64 +++-- apps/desktop/src/app/chat/composer/index.tsx | 61 +++-- .../src/app/chat/composer/inline-refs.ts | 6 + .../chat/hooks/use-composer-actions.test.ts | 57 +++++ .../app/chat/hooks/use-composer-actions.ts | 31 ++- apps/desktop/src/app/chat/index.tsx | 22 +- .../src/app/chat/right-rail/preview-file.tsx | 6 +- .../session/hooks/use-prompt-actions.test.tsx | 133 ++++++++++- .../app/session/hooks/use-prompt-actions.ts | 223 ++++++++++++------ .../src/components/assistant-ui/thread.tsx | 93 +++++++- apps/desktop/src/store/composer.ts | 19 ++ apps/desktop/src/store/preview.ts | 11 +- 12 files changed, 584 insertions(+), 142 deletions(-) create mode 100644 apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts diff --git a/apps/desktop/src/app/chat/composer/attachments.tsx b/apps/desktop/src/app/chat/composer/attachments.tsx index 0c154a8a4b1..6229c9da8bd 100644 --- a/apps/desktop/src/app/chat/composer/attachments.tsx +++ b/apps/desktop/src/app/chat/composer/attachments.tsx @@ -3,8 +3,9 @@ import { useStore } from '@nanostores/react' import { Codicon } from '@/components/ui/codicon' import { Tip } from '@/components/ui/tooltip' import { useI18n } from '@/i18n' -import { FileText, FolderOpen, ImageIcon, Link, Terminal } from '@/lib/icons' +import { AlertCircle, FileText, FolderOpen, ImageIcon, Link, Loader2, Terminal } from '@/lib/icons' import { normalizeOrLocalPreviewTarget } from '@/lib/local-preview' +import { cn } from '@/lib/utils' import type { ComposerAttachment } from '@/store/composer' import { notifyError } from '@/store/notifications' import { setCurrentSessionPreviewTarget } from '@/store/preview' @@ -31,7 +32,9 @@ function AttachmentPill({ attachment, onRemove }: { attachment: ComposerAttachme const c = t.composer const Icon = { folder: FolderOpen, url: Link, image: ImageIcon, file: FileText, terminal: Terminal }[attachment.kind] const cwd = useStore($currentCwd) - const canPreview = attachment.kind !== 'folder' && attachment.kind !== 'terminal' + const isUploading = attachment.uploadState === 'uploading' + const hasUploadError = attachment.uploadState === 'error' + const canPreview = attachment.kind !== 'folder' && attachment.kind !== 'terminal' && !isUploading const detail = attachment.detail && attachment.detail !== attachment.label ? attachment.detail : undefined async function openPreview() { @@ -59,7 +62,15 @@ function AttachmentPill({ attachment, onRemove }: { attachment: ComposerAttachme throw new Error(c.couldNotPreview(attachment.label)) } - setCurrentSessionPreviewTarget(preview, 'manual', target) + // We already hold the image bytes (the card thumbnail) — render those + // directly so a screenshot/clipboard image previews even when its only + // on-disk copy is a transient path the renderer can't re-read. + const withBytes = + attachment.kind === 'image' && attachment.previewUrl + ? { ...preview, dataUrl: attachment.previewUrl, previewKind: 'image' as const } + : preview + + setCurrentSessionPreviewTarget(withBytes, 'manual', target) } catch (error) { notifyError(error, c.previewUnavailable) } @@ -69,30 +80,51 @@ function AttachmentPill({ attachment, onRemove }: { attachment: ComposerAttachme