fix(desktop): stage dropped files into the remote session workspace

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>
This commit is contained in:
Brooklyn Nicholson
2026-06-09 16:50:08 -05:00
co-authored by LeonSGP Teknium
parent 02f878ec5a
commit 4906dcfc25
12 changed files with 584 additions and 142 deletions
+19
View File
@@ -11,6 +11,10 @@ export interface ComposerAttachment {
previewUrl?: string
path?: string
attachedSessionId?: string
/** Set while the file/image bytes are being staged into the session
* workspace (remote upload or local stage), and 'error' if that failed.
* Drives the spinner / error state on the composer attachment card. */
uploadState?: 'uploading' | 'error'
}
export const $composerDraft = atom('')
@@ -73,6 +77,21 @@ export function clearComposerAttachments() {
$composerAttachments.set([])
}
/** Update only the upload state of an existing attachment (no-op if it's gone,
* e.g. the user removed it mid-upload). Pass `undefined` to clear it. */
export function setComposerAttachmentUploadState(id: string, uploadState?: ComposerAttachment['uploadState']) {
const current = $composerAttachments.get()
const index = current.findIndex(attachment => attachment.id === id)
if (index < 0) {
return
}
const next = [...current]
next[index] = { ...next[index]!, uploadState }
$composerAttachments.set(next)
}
const TERMINAL_REF_RE = /@terminal:(`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|\S+)/g
function unquoteRefValue(raw: string) {
+10 -1
View File
@@ -6,6 +6,11 @@ import { $activeSessionId, $selectedStoredSessionId } from './session'
export interface PreviewTarget {
binary?: boolean
byteSize?: number
/** Inline image bytes (a `data:` URL) when the renderer already holds them —
* e.g. a pasted/dropped screenshot whose only on-disk copy is a transient
* path the preview can't reliably re-read. Rendered directly and NOT
* persisted to the session-preview registry (it would bloat localStorage). */
dataUrl?: string
kind: 'file' | 'url'
label: string
large?: boolean
@@ -214,7 +219,11 @@ function persistSessionPreviewRegistry(registry: SessionPreviewRegistry) {
}
try {
window.localStorage.setItem(REGISTRY_STORAGE_KEY, JSON.stringify(pruneRegistry(registry)))
// Drop the inline image bytes before persisting — a screenshot data URL is
// megabytes and would blow the localStorage quota. On reload the record
// falls back to reading its `path`/`url`.
const lean = JSON.stringify(pruneRegistry(registry), (key, value) => (key === 'dataUrl' ? undefined : value))
window.localStorage.setItem(REGISTRY_STORAGE_KEY, lean)
} catch {
// Session previews are a desktop convenience; storage failures are nonfatal.
}