Summary
Pasting an image (e.g. a screenshot from the clipboard) into the chat composer does nothing. Drag-and-drop and the file-picker button both work, but Ctrl/Cmd+V is silently ignored, even when there's a perfectly valid PNG on the clipboard. This is the standard way to share screenshots with an agent in Cursor, ChatGPT, Claude.ai, and most other chat UIs, so the missing handler is a real papercut.
After digging, this is purely a missing event handler: every other piece of the pipeline already exists.
What's already there (in client/src/pages/SessionView.tsx)
- The composer's
<textarea> only wires onChange, onKeyDown, and onSelect — no onPaste handler anywhere (confirmed by rg -n "onPaste|clipboardData|ClipboardEvent" client/src/ returning nothing relevant).
- The drop zone already calls
addComposerFiles(event.dataTransfer.files) for the onDrop path on the same wrapping <div> at line ~5895.
- The file-input button calls the same
addComposerFiles(event.target.files).
addComposerFiles (line 4019) already does the right things for an image: enforces the 5-file / 15 MB-per-file / 35 MB-total limits, validates against SUPPORTED_ATTACHMENT_TYPES (which already includes image/png, image/jpeg, image/heic, image/heif, image/gif, image/webp), generates a URL.createObjectURL(file) for PREVIEWABLE_IMAGE_TYPES so the existing AttachmentStrip thumbnail renders correctly, and sets a human-readable error via setAttachmentError(...) (e.g. "foo.png is not a supported file type" or "Attachments are larger than 35 MB total").
- The composer already renders
{attachmentError && ...} at line 6102, so the user will see any error from addComposerFiles without extra wiring.
So the only real change is one onPaste handler on the <textarea> that reads the clipboard and forwards any File items to the existing addComposerFiles.
Proposed behavior
- Image paste:
Cmd/Ctrl+V while focused in the composer (or anywhere inside the composer drop zone) reads event.clipboardData.items, filters for kind === "file", and calls addComposerFiles(files) — exactly the same code path as drop and the file-picker button.
- Text paste: unchanged. Default browser behavior inserts the text into the textarea. We must not call
preventDefault() for non-file pastes.
- Mixed clipboard (e.g. screenshot tool that also puts a text fallback on the clipboard): prefer the file. If only text is on the clipboard, fall through to default.
- Unsupported types: let the existing
SUPPORTED_ATTACHMENT_TYPES check surface a clear error via the existing attachmentError slot — no new UI needed.
- Provider/model gating: respect the existing
canAttachMore check. If the active provider/model doesn't support attachments, the paste should be a no-op (or show the same error the file-picker would show — match whichever is cheaper).
- Paste outside the textarea (e.g. user pastes onto the drop zone but not the textarea): should still work, by attaching
onPaste to the wrapping <div> the way onDrop is attached.
Why this is low-risk
- It's a single new event handler that delegates to an already-tested function (
addComposerFiles).
- The MIME allowlist, size limits, count limit, error UX, and preview thumbnail path are all unchanged.
- No backend changes required —
SessionAttachment already handles the File → data: URL conversion on the client and the server treats them identically to dropped / picked files.
Open questions
- Paste into the new-session composer before a project is selected. The current
canAttachMore check needs selectedProvider / selectedModel to be defined; if either is missing the paste is currently undefined behavior. Probably: silently no-op + toast? But that path is shared with drop, so the fix is the same for both.
- HEIC/HEIF from macOS screenshots. They're in
SUPPORTED_ATTACHMENT_TYPES, so they'll go through, but most providers don't accept them. Worth surfacing as a clearer error than "not a supported file type" when the model rejects them later? Out of scope for this issue — the existing error path is fine for v1.
- Multi-image paste (some screenshot tools put several variants on the clipboard). Should be fine —
addComposerFiles accepts a FileList and already enforces MAX_ATTACHMENT_COUNT.
Acceptance criteria
Related code
- Composer + drop zone:
client/src/pages/SessionView.tsx (the <form> and <textarea> around lines 5860–5980; the <div> with onDragOver/onDrop at 5895–5904)
addComposerFiles: client/src/pages/SessionView.tsx:4019 (already enforces all limits)
- Attachment allowlist / preview set:
SUPPORTED_ATTACHMENT_TYPES and PREVIEWABLE_IMAGE_TYPES at client/src/pages/SessionView.tsx:301–321
- Existing error display:
{attachmentError && ...} at client/src/pages/SessionView.tsx:6102
AttachmentStrip (used in the outgoing user message bubble to render the thumbnail): client/src/pages/SessionView.tsx
Summary
Pasting an image (e.g. a screenshot from the clipboard) into the chat composer does nothing. Drag-and-drop and the file-picker button both work, but
Ctrl/Cmd+Vis silently ignored, even when there's a perfectly valid PNG on the clipboard. This is the standard way to share screenshots with an agent in Cursor, ChatGPT, Claude.ai, and most other chat UIs, so the missing handler is a real papercut.After digging, this is purely a missing event handler: every other piece of the pipeline already exists.
What's already there (in
client/src/pages/SessionView.tsx)<textarea>only wiresonChange,onKeyDown, andonSelect— noonPastehandler anywhere (confirmed byrg -n "onPaste|clipboardData|ClipboardEvent" client/src/returning nothing relevant).addComposerFiles(event.dataTransfer.files)for theonDroppath on the same wrapping<div>at line ~5895.addComposerFiles(event.target.files).addComposerFiles(line 4019) already does the right things for an image: enforces the 5-file / 15 MB-per-file / 35 MB-total limits, validates againstSUPPORTED_ATTACHMENT_TYPES(which already includesimage/png,image/jpeg,image/heic,image/heif,image/gif,image/webp), generates aURL.createObjectURL(file)forPREVIEWABLE_IMAGE_TYPESso the existingAttachmentStripthumbnail renders correctly, and sets a human-readable error viasetAttachmentError(...)(e.g. "foo.png is not a supported file type" or "Attachments are larger than 35 MB total").{attachmentError && ...}at line 6102, so the user will see any error fromaddComposerFileswithout extra wiring.So the only real change is one
onPastehandler on the<textarea>that reads the clipboard and forwards anyFileitems to the existingaddComposerFiles.Proposed behavior
Cmd/Ctrl+Vwhile focused in the composer (or anywhere inside the composer drop zone) readsevent.clipboardData.items, filters forkind === "file", and callsaddComposerFiles(files)— exactly the same code path as drop and the file-picker button.preventDefault()for non-file pastes.SUPPORTED_ATTACHMENT_TYPEScheck surface a clear error via the existingattachmentErrorslot — no new UI needed.canAttachMorecheck. If the active provider/model doesn't support attachments, the paste should be a no-op (or show the same error the file-picker would show — match whichever is cheaper).onPasteto the wrapping<div>the wayonDropis attached.Why this is low-risk
addComposerFiles).SessionAttachmentalready handles theFile→data:URL conversion on the client and the server treats them identically to dropped / picked files.Open questions
canAttachMorecheck needsselectedProvider/selectedModelto be defined; if either is missing the paste is currently undefined behavior. Probably: silently no-op + toast? But that path is shared with drop, so the fix is the same for both.SUPPORTED_ATTACHMENT_TYPES, so they'll go through, but most providers don't accept them. Worth surfacing as a clearer error than "not a supported file type" when the model rejects them later? Out of scope for this issue — the existing error path is fine for v1.addComposerFilesaccepts aFileListand already enforcesMAX_ATTACHMENT_COUNT.Acceptance criteria
attachmentErrorslot — no silent failure.Related code
client/src/pages/SessionView.tsx(the<form>and<textarea>around lines 5860–5980; the<div>withonDragOver/onDropat 5895–5904)addComposerFiles:client/src/pages/SessionView.tsx:4019(already enforces all limits)SUPPORTED_ATTACHMENT_TYPESandPREVIEWABLE_IMAGE_TYPESatclient/src/pages/SessionView.tsx:301–321{attachmentError && ...}atclient/src/pages/SessionView.tsx:6102AttachmentStrip(used in the outgoing user message bubble to render the thumbnail):client/src/pages/SessionView.tsx