cmux NEXT composer: a pasted image is a chip only, never data: text in the prompt
What broke
Leo copied two images in Chrome and pasted them into the composer. They showed as chips and also full size in the field. The prompt then went out with  as its text: the bubble showed pages of base64, the agent read the base64 as prompt text, and the chat's compaction failed.
Cause: Chrome's Copy Image puts the image file and an <img> in text/html on the clipboard. The composer's paste listener made a chip from the file, but ProseMirror also turned the HTML's <img> into an image node. The field drew that node full size and wrote it as markdown with the data URI. The field only stepped aside for a paste of files with no text and no HTML.
Fix
- Paste (
MarkdownField.tsx,attachments.ts,attachmentIntake.tsx): a paste that carries images (files, or adata:<img>in its HTML) gives the field only its plain text. The composer attaches the images as chips, including fromdata:<img>s in PNG, JPEG, GIF or WebP when the clipboard has no file. That fallback doesn't cancel the paste, so the New Tab and shell text fields still take the text, and spacer-sized images are skipped. Rich HTML with ordinary remote images (emoji, avatars) keeps its formatting. - Send (
direct.tssend,promptBlocks): a markdown image of base64 PNG, JPEG, GIF or WebP outside code is lifted into an ACP image block (inlineImages.ts), so the sent bubble shows it as an image above the text. Adata:URI written as text (in code, a source map, a CSSurl(), an SVG) goes as written, and the rest of the prompt keeps its whitespace. - Render (
UserBubble.tsx): a bubble never draws adata:URI. Each image reads as its name in brackets, which also covers prompts already sent this way, like Leo's. Copy copies the prompt as it was sent. Edit opens it without its inline images.
Tests
webviews/test/composer-paste-images.test.tsx drives the real composer (Milkdown/ProseMirror) under JSDOM:
- Paste → chip: pasting two images (files plus
<img>HTML) makes two chips, no<img>in the field, and no text. - HTML only: an image copied as HTML with no file still becomes a chip, and its text stays text.
- Send payload: sending the pasted images gives two image blocks and no
data:in any text block. - Already in the text: a markdown
data:image in the text, including one with a title, goes as an image block. - Written as text: a
data:URI in code, a source map, a CSSurl()or an SVG goes exactly as written. - Plain text field: an image copied as HTML into a plain text field becomes a chip, the paste isn't cancelled, and a spacer GIF makes no chip.
- Bubble: a bubble whose text holds
data:URIs shows[Screenshot · 778×696]and[Image], with no base64.
On base the first 5 fail, reproducing the bug: an HTMLImageElement in the field and  in the text block. With the fix all 7 pass. Related suites (attachment thumbnails, sent-image lifecycle, edit/resend, user message actions, composer draft switch, prompt delivery) pass one file per process, as CI runs them. Typecheck has 0 errors; its one warning is base's test/sidebar-rail-css.test.tsx. Lint, formatting and the pre-push gate pass. No Swift changes.
🤖 Generated with Claude Code
Agent session: cosmic-gray
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Pasting copied images into the composer now attaches them as chips only, never as data: text in the prompt.
Chrome's Copy Image puts both the image file and an <img> in text/html on the clipboard, so ProseMirror turned the HTML into a full-size image node that went out as . The bubble showed pages of base64, the agent read it as prompt text, and the chat's compaction failed.
- A paste that carries images (files, a
data:<img>in its HTML, ormarkdown in its plain text, as a prompt copied from a bubble) gives the field only its plain text; the composer attaches the images as chips. An image copied as HTML alone is attached without cancelling the paste, so a textarea still takes its text; spacer-sized and non-agent image types are not attached. Lifted text is inserted via the field's own window. - Lifting is limited to markdown
data:images of png/jpeg/gif/webp outside code (inlineImages.ts); any otherdata:URI — a source map, a CSSurl(), an SVG — stays as written. Each lifted image goes out as an ACP image block on send and inpromptBlocks. - A bubble never draws a
data:URI: each image reads as its name in brackets, which also covers prompts already sent this way. Copy copies a prompt as sent; Edit opens it without its inline images. - Adds
composer-paste-images.test.tsxdriving the real composer under JSDOM; the scenarios fail on base and pass with the fix.