Skip to content

fix(site/src/pages/AgentsPage): paste SVG source inline instead of as attachment#26969

Draft
jakehwll wants to merge 2 commits into
mainfrom
jakehwll/fix-svg-string-paste-inline
Draft

fix(site/src/pages/AgentsPage): paste SVG source inline instead of as attachment#26969
jakehwll wants to merge 2 commits into
mainfrom
jakehwll/fix-svg-string-paste-inline

Conversation

@jakehwll

@jakehwll jakehwll commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🤖 This PR was written by Coder Agents on behalf of Jake Howell.

Pasting SVG XML text into the /agents chat input surfaced Unsupported file type. because the large-paste heuristic wrapped it into a text/plain File and the server-side classifier in coderd/x/chatfiles/mime.go probes for an SVG root element before trusting the declared MIME type. SVG uploads are intentionally rejected because SVG can carry active script.

Detect the SVG root element on the paste path so oversized SVG sources bypass the attachment path and land inline in the Lexical editor. SVG uploads via file picker or drag-and-drop are unchanged; both paths already filter image/svg+xml before it reaches the upload pipeline.

Implementation plan

Root cause

  1. User pastes SVG XML text into the Lexical editor.
  2. PasteSanitizationPlugin in site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.tsx extracts the plain text.
  3. isLargePaste(text) (>=10 lines OR >=1000 chars) wraps the paste into a synthetic .txt File with MIME text/plain via createPasteFile().
  4. The file flows through onFilePaste -> handleFilePaste -> onAttach -> useFileAttachments.handleAttach -> startUpload -> API.experimental.uploadChatFile().
  5. Server-side, postChatFile calls chatfiles.PrepareStoredFile() -> ClassifyStoredMediaType(), which invokes HasSVGRootElement(data) first. That returns image/svg+xml regardless of the declared Content-Type. image/svg+xml is not in codersdk.AllChatAttachmentMediaTypes, so the server returns HTTP 400 with Message: "Unsupported file type.".

Why the fix inspects content

SVG source copied from a text editor arrives on the clipboard as text/plain; there is no image/svg+xml entry on the clipboard to consult, and the synthetic File we construct is text/plain by design. The server sniffs bytes and reclassifies to image/svg+xml, so the only thing that predicts the server's decision is the content itself.

Fix

Ask the browser's XML parser to classify the paste. If it parses as an SVG document with an <svg> root element, skip the large-paste-to-attachment conversion and insert the text inline instead. Kept lenient by design: a false positive just leaves an XML-ish paste inline (harmless), whereas a false negative reproduces the original bug.

Changes

  • site/src/pages/AgentsPage/components/ChatMessageInput/pasteHelpers.ts: add hasSVGRootElement(text) backed by DOMParser.parseFromString(text, "image/svg+xml"). Returns true when parsing succeeds without a parsererror and the root element is <svg>.
  • site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.tsx: gate both paste branches (native ClipboardEvent and beforeinput) on !hasSVGRootElement(text) before invoking createPasteFile(text) and calling onFilePaste.
  • site/src/pages/AgentsPage/components/ChatMessageInput/pasteHelpers.test.ts: unit tests covering bare <svg>, self-closing <svg/>, attributed <svg xmlns=...>, XML declaration prolog, DOCTYPE, comments, BOM, case-insensitivity, lookalike names, HTML, markdown, CSV fragments, whitespace-only, unterminated PIs and comments.
  • site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx: LargeSVGPasteRemainsInline story that pastes an SVG larger than the isLargePaste threshold and asserts it lands inline without calling onAttach, alongside the existing LargePasteCreatesAttachmentPreview and CtrlShiftVBypassesAttachmentCollapse stories.

Scope

  • Uploading SVG via the file picker: unchanged. chatAttachmentAcceptAttribute does not include SVG.
  • Dropping SVG files onto the editor: unchanged. AgentChatInput.handleDrop filters via isChatAttachmentFile, which excludes image/svg+xml.
  • Pasted SVG images (image on clipboard, not text): unchanged. The clipboard files array is filtered by isChatAttachmentFile in the plugin's native-paste branch.

Validation

  • pnpm --dir site exec vitest run --project=unit src/pages/AgentsPage/components/ChatMessageInput/pasteHelpers.test.ts -> 33/33 pass.
  • pnpm --dir site exec vitest run --project=unit src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.test.tsx -> 3/3 pass.
  • pnpm --dir site exec vitest run --project=storybook src/pages/AgentsPage/components/AgentChatInput.stories.tsx -> 48/48 pass in headless Chromium.
  • pnpm --dir site run lint:types -> 0 errors.
  • pnpm --dir site exec biome check on the touched files -> clean.

…as attachment

Pasting SVG XML text into the /agents chat input surfaced
"Unsupported file type." because the large-paste heuristic wrapped it
into a text/plain File. The server-side classifier in
coderd/x/chatfiles/mime.go probes for an SVG root element before
trusting the declared MIME type and rejects the resulting attachment.

Detect the SVG root element in the paste helper so oversized SVG
sources bypass the attachment path and land inline in the Lexical
editor. Uploads via file picker or drag-and-drop are unaffected.
…tection

Replace the hand-rolled XML tokenizer in hasSVGRootElement with a
DOMParser call. The browser's XML parser handles BOM, XML declarations,
comments, and DOCTYPE prologs natively, so the helper collapses to a
parse-and-inspect check that reads far more clearly.

The check remains lenient by design: a false positive leaves an XML-ish
paste inline, which is harmless, whereas a false negative reproduces
the "Unsupported file type." bug. The removed test covered a synthetic
combined prolog (BOM + XML decl + comment + DOCTYPE) that jsdom rejects
but that no real-world SVG produces.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant