Skip to content

feat: send text files from the composer to the agent - #469

Merged
iamnbutler merged 4 commits into
mainfrom
file-uploads
Oct 16, 2026
Merged

iamnbutler merged 4 commits into
mainfrom
file-uploads

Conversation

@iamnbutler

Copy link
Copy Markdown
Contributor

Part of #26. Related: #105, #269.

A non-image file in the composer (dropped, picked, or pasted) no longer fails with "Only images can be attached". If it decodes as UTF-8, it is sent with the message, and every model in the chat reads it.

  • Protocol: TextFile = { name, text }, carried as files? on say, ask, the message event, and QueuedMessage.
  • Storage: no new store and no migration. Each file is its own text block after the message in pi's user message, <attachment name="...">\n…\n</attachment>. The first text block is still author: text, so input()/human() read existing transcripts unchanged and parse files back from later blocks. The size budget is shared: checkAttachments replaces checkImages, and images plus file text (UTF-8 bytes) must fit under the existing 1.5 MB limit (the Durable Object row cap), with at most 10 files. Names must be one line.
  • Prompt: one added line in the channel section says attachments are inline, not on disk. Without it, Haiku searched the filesystem for the file before answering.
  • App: images.ts became attachments.ts. It reads text files first, then shrinks images to fit the remaining budget. A binary file is refused with "X isn't a text file; only images and text files can be attached". Sent files appear in the timeline as the existing file chunk/FileView block, and by name in the queue. A withdrawn message brings its files back into the composer as chips.
  • UI package: Attachment gets an optional file, which useAttachments sets, so contents without a preview can be read. The chip UI is unchanged.

Product calls:

  • Files go to the agent as text in the transcript, not into the workspace. This matches how images already work, and it works the same for hosted and compute channels and for teammates on other machines.
  • Binary files (PDF, zip, …) are refused for now. They would need workspace upload or provider document blocks.
  • Older clients and hosts reading a new ask message see the file text joined onto the message text. Nothing is lost.

Remaining for #26: binary/non-text files (into the lane or workspace, or as documents for providers that accept them), and opening an attached file's contents from the timeline.

Smoke-tested with a source host on isolated ACE_HOME/ACE_CONFIG_HOME and real anthropic/claude-haiku-4-5 runs:

  • say and ask with files: the model answered from the attachment, a name with quotes and contents containing </attachment> round-tripped, and replayed message events carried files.
  • The channel refused a multi-line name, 11 files, and 1.6 MB of text.
  • In the served app, file blocks render in user messages.
  • The app's attachments() in Bun accepted a .md file and refused random bytes.
  • I didn't drive the native file picker end to end; macOS's open panel wasn't observable to the desktop tools.

Built in Ace

Built with Claude Code outside Ace, in a session already running there; no Ace gap involved.

A non-image attachment that decodes as UTF-8 is sent as `files` on say and
ask. The channel stores each as its own <attachment name="..."> text block
after the message in the pi transcript, so every model reads it and the
message's first text block stays the message. Messages, the queue, and the
timeline show attached files by name; withdrawn messages return them to the
composer. Binary files are refused with a clear error.

Why: packages/channel/src/room.ts#function content | the first text block of a human message is always `author: text`; files follow as <attachment> blocks that input() and human() parse back, so old transcripts read unchanged
Why: packages/channel/src/room.ts#function checkAttachments | images and file text share one MAX_BYTES budget under the Durable Object 2 MB row cap
@iamnbutler
iamnbutler merged commit 2383e14 into main Oct 16, 2026
1 check passed
iamnbutler added a commit that referenced this pull request Oct 16, 2026
…imeline (#479)

* feat(app): send PDFs as their text and open attached files from the timeline

A PDF dropped, picked, or pasted into the composer is read with pdf.js in the
app and sent as a TextFile, so it is stored and reaches every model exactly
like a text file from #469: no protocol, storage, or migration change.
Scanned PDFs (no text layer) and unreadable PDFs are refused. A sent file's
timeline block now shows its line count and opens its text in the shared
FileView in a dialog.

Why: apps/app/src/attachments.ts#function text | pdf.js page text must be read through the stream's reader; WebKit's ReadableStream isn't async-iterable, so getTextContent throws in the desktop webview
Why: apps/app/src/attachments.ts#function read | detect PDFs by MIME type only; a withdrawn PDF returns to the composer as its extracted text typed text/plain under its .pdf name
Why: packages/ui/src/components/timeline/layout.ts | a file block's cache key includes its text, since the block opens that text and names can repeat across messages

* lane: sync file-uploads-2 memory

* lane: sync file-uploads-2 memory
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