Skip to content

Composer: paste images from the clipboard into the chat input #314

Description

@germanescobar

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 onSelectno 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 Filedata: 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

  • Pasting a PNG/JPEG/GIF/WebP/HEIC image from the clipboard into the chat composer adds it as an attachment with a thumbnail, identical to dragging the same file in.
  • Pasting text still works exactly as before (default browser behavior).
  • Pasting an unsupported file type shows a clear error via the existing attachmentError slot — no silent failure.
  • Pasting when the active model doesn't support images is a no-op (or shows a clear error, matching the drop behavior).
  • Pasting into the drop zone (but not directly into the textarea) also works.
  • All existing limits (5 files, 15 MB/file, 35 MB total) still apply.
  • No regression to drag-and-drop or the file-picker button.
  • A small unit test for the paste handler covers: image, text-only, unsupported type, over-limit, no-provider.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requestuiUser interface changes

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions