From b3084fd515cbc3bbc0e4e8085d7cb1714a98b34c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 01:46:24 +0000 Subject: [PATCH] fix: route clipboard image pastes to the attachment strip Pasting an image while the markdown editor was focused left it stuck on ProseMirror's inline "uploading" placeholder instead of going to the attachment strip. The capture-phase paste interceptor bailed before it could redirect the file because: - pasted images usually surface only through `clipboardData.items` (`getAsFile`), with `clipboardData.files` left empty, and - image pastes often advertise an *empty* `text/plain` entry, which the old `types.includes('text/plain')` check mistook for a text paste. Read files from `items` (falling back to `files`), and gate on the actual text payload rather than the advertised type, so real text pastes (Excel/Word renditions) still paste as text while pure image pastes become attachments. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QtNQeBpkDJPC9obiB25pV6 --- js/desktop/src/hooks/use-file-input.ts | 29 +++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/js/desktop/src/hooks/use-file-input.ts b/js/desktop/src/hooks/use-file-input.ts index 749c987..737b98c 100644 --- a/js/desktop/src/hooks/use-file-input.ts +++ b/js/desktop/src/hooks/use-file-input.ts @@ -5,6 +5,19 @@ interface UseFileInputOptions { enabled: boolean; } +/** + * Files carried by a paste. A pasted image is usually exposed only through + * `items` (`getAsFile`), with `files` left empty, so read `items` first and + * fall back to `files` for the rare clipboard that populates only the latter. + */ +function clipboardFiles(data: DataTransfer): File[] { + const fromItems = Array.from(data.items) + .filter((item) => item.kind === 'file') + .map((item) => item.getAsFile()) + .filter((file): file is File => file !== null); + return fromItems.length > 0 ? fromItems : Array.from(data.files); +} + export function useFileInput({ onFilesSelected, enabled, @@ -49,14 +62,20 @@ export function useFileInput({ if (!enabled) return; // Capture phase so file pastes always become attachments — ProseMirror - // would otherwise inline pasted images as ephemeral blob: URLs. Mixed - // clipboards (e.g. Excel/Word ship an image rendition alongside the text) - // must still paste as text, so only file-only pastes are intercepted. + // (the markdown editor) would otherwise inline a pasted image as an + // ephemeral blob: URL and get stuck on its "uploading" placeholder. const handlePaste = (e: ClipboardEvent) => { const data = e.clipboardData; if (!data) return; - const files = Array.from(data.files); - if (files.length === 0 || data.types.includes('text/plain')) return; + + // Actual text means "paste as text", even when the clipboard also ships + // an image rendition (Excel/Word, or "copy image" from a page with alt + // text). Test the payload, not the advertised types: image pastes often + // list an *empty* text/plain entry that must not block the attachment. + if (data.getData('text/plain').trim().length > 0) return; + + const files = clipboardFiles(data); + if (files.length === 0) return; e.preventDefault(); e.stopPropagation(); onFilesRef.current(files);