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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtNQeBpkDJPC9obiB25pV6
This commit is contained in:
Claude
2026-06-21 01:46:24 +00:00
parent bfe53b46cf
commit b3084fd515
+24 -5
View File
@@ -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);