Skip to content

feat(composer): attach large text pastes as a text file - #3454

Open
jhedlund wants to merge 2 commits into
block:mainfrom
jhedlund:feat/paste-large-text-as-file
Open

jhedlund wants to merge 2 commits into
block:mainfrom
jhedlund:feat/paste-large-text-as-file

Conversation

@jhedlund

@jhedlund jhedlund commented Nov 13, 2026 •

Copy link
Copy Markdown

Why

Pasting a long log or document fills the draft, and past the 16,000-character message limit the paste is refused outright. I paste logs to agents constantly and end up saving them to a file by hand first.

What

An ordinary paste of plain text over 10,000 characters or 200 lines attaches as pasted-text-YYYY-MM-DD-HHMMSS.txt instead. The file stays on this device until the message is sent. A notice offers Paste inline instead, which inserts the text and removes the file. Plain-text paste (Mod+Shift+V, or the desktop menu) always pastes inline.

How

  • Uses the existing attachment draft, so the 10-file and size limits and the chip apply. The draft item is marked to upload on Send: the eager upload pump skips it and prepareForSend uploads it. Removing it, or choosing Paste inline instead, never reaches the media host. The chip reads "Uploads when sent".
  • If the text is too long to insert inline, the file stays and the existing too-long alert shows.
  • The notice stays until it's dismissed and shows only while its file is still in the draft, so Send or removing the file closes it. Typing closes it as well.
  • A trailing newline doesn't count as an extra line, so exactly 200 copied lines stay inline.
  • Unchanged: pastes while editing a sent message, where uploads are unavailable, and outside the message field. A refused attachment (file limit, size, drafts full) pastes the text inline without an attachment error.
  • Rich clipboard content over the threshold is saved as its plain text only. Plain-text paste is the way to keep it in the message.

Risk

Changes default paste behaviour above the threshold. The thresholds are constants in large-paste.ts and easy to tune; tell me if you'd prefer different numbers or a setting.

Testing

Unit tests for the threshold, including trailing newlines and CRLF. Composer tests cover: attach with no upload until Send, then upload and publish on Send with the notice closing; plain-text paste inline; refused attachment at the 10-file limit pasting inline with no error; paste inline instead with nothing uploaded; too long to inline; editing a sent message; the notice closing on typing. Used daily in a Windows 11 build. Not tried on macOS or Linux.

Built with help from a Claude agent.

@jhedlund
jhedlund requested a review from a team as a code owner November 13, 2026 03:24

@joahg joahg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This account isn't approving UI changes. This changes composer paste behavior, so it needs a design or core-team reviewer. It also shows mergeable: CONFLICTING, so it needs a rebase before it can land.

Findings, worst first:

  • P2: the file uploads at paste time, not on Send. The PR text says "upload-on-Send", but attachments.store.add calls pump() straight away. The new test confirms it by waiting for the upload call after the paste, before any Send. Pasted text used to stay on this device until Send. Now an accidentally pasted secret goes to the media host immediately, even if the message is never sent. Paste inline instead only aborts the upload. If it already finished, the blob stays on the server. I didn't check whether removing a draft attachment deletes the blob, or who can fetch it by URL in a private channel. The help doc says files are shared with the conversation's members. This is no worse than an image, but it's a new privacy difference for text, and the PR description should say so.
  • P3: the "refused attachment falls back to an ordinary paste" behavior is not as described and has no test. With a single file, attachments.store.add throws on a refusal (10 files already, over UPLOAD_MAX_BYTES, drafts full). attachFiles then calls refuseAttachment. The user gets a "Could not attach file" error and the text is pasted inline. The code comment says "without an attachment error". The PR description lists a refused-attachment test, but I see no such test in the diff.
  • P3: line counting is off by one for text ending in a newline. "a\n".repeat(200) splits into 201 and is treated as large.
  • P3: the only way to get inline text is the 5-second notice button. Re-pasting or using plain-text paste attaches again, and there is no setting. Rich clipboard content (HTML) over the threshold is attached as plain text only, so link labels and formatting are lost in the file.
  • P3: the notice isn't cleared on Send or when the channel changes while the draft stays empty (the effect keys on draft). A late click on Paste inline instead would then insert the text into whichever composer is current, while the file stays where it was.

Checked and fine:

  • The text is never dropped. The draft is untouched on attach, and inline falls back when attaching fails.
  • If inline would exceed the limit, the file stays and the existing alert shows.
  • Editing a sent message, composers without uploads, and pastes outside the message field behave as before.
  • The file is UTF-8 and keeps the clipboard's line endings (CRLF on Windows). The name is local time to the second. The 10-file and size limits come from the existing store.
  • None of the edited files is marked FOUNDATION.

Not reviewed: behavior on macOS or Linux, and the server-side handling of uploaded blobs.

editing.target
)
return;
const [id] = attachFiles([largePasteFile(text, new Date())]) ?? [];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 When store.add refuses a single file (10 files already, too large, drafts full) it throws, so attachFiles shows "Could not attach file" and this then pastes the text inline anyway. That contradicts the comment above ("without an attachment error") and the PR description, and no test covers it. Either skip the error for this path or fix the wording and add a test.

Comment thread src/features/messages/large-paste.ts Outdated
export function isLargeTextPaste(text: string): boolean {
return (
text.length > LARGE_PASTE_MAX_CHARS ||
text.split("\n").length > LARGE_PASTE_MAX_LINES

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 split("\n") on text ending in a newline counts an extra empty line, so a paste of exactly 200 full lines (the usual terminal copy) is treated as large. Count a trailing newline as no extra line, or drop it first.

key={largePaste.id}
title="Pasted as a text file"
tone="info"
timeout={5000}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This 5-second auto-dismissing notice is the only way to get the text inline: plain-text paste and re-pasting both attach again, and there is no setting. A keyboard user may not reach the button in time. It also stays up after Send when the draft is empty. Worth a design decision on a persistent opt-out and on the timeout.

jhedlund and others added 2 commits November 13, 2026 15:14
A plain-text paste over 10,000 characters or 200 lines used to fill the
draft, or was refused outright past the 16,000-character message limit.
It now attaches as `pasted-text-YYYY-MM-DD-HHMMSS.txt` through the
existing attachment path, so the file limits, the chip and upload on Send
all apply. A notice offers "Paste inline instead", which inserts the text
and removes the file; text too long to insert keeps the file and shows
the existing too-long alert. Typing or dismissing closes the notice.

Pastes while editing a sent message, where uploads are unavailable, or
outside the message field are unchanged, and a refused attachment falls
back to an ordinary paste.

Co-authored-by: Engineer <f79d52bb0d4ab12c7affaf2235c113a7fc797a9da8f9707757123b67cfafa6c6@buzz.matrixsoftwareinc.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jeff Hedlund <jhedlund@gmail.com>
Review follow-up for the large-paste attachment:

- The pasted-text file is held on this device and uploads only when the
  message is sent (`onSend` draft items are skipped by the eager upload
  pump; `prepareForSend` uploads them). Removing it, or choosing
  "Paste inline instead", never reaches the media host. The chip reads
  "Uploads when sent".
- A refused attachment (file limit, size, drafts full) now pastes the text
  inline without a "Could not attach file" error, as documented.
- A trailing newline no longer counts as an extra line, so exactly 200
  copied lines stay inline.
- The notice stays until dismissed (no 5-second timeout) and shows only
  while its file is still in the draft, so it leaves on Send.
- Plain-text paste (Mod+Shift+V or the desktop menu) always pastes inline.

Co-authored-by: Engineer <f79d52bb0d4ab12c7affaf2235c113a7fc797a9da8f9707757123b67cfafa6c6@buzz.matrixsoftwareinc.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jeff Hedlund <jhedlund@gmail.com>
@jhedlund
jhedlund force-pushed the feat/paste-large-text-as-file branch from 060bf5a to b1c5468 Compare November 13, 2026 20:42

This branch has not been deployed

No deployments
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.

2 participants