feat(web): attach files of any type from the composer, beside images - #949
Conversation
The composer's Add menu offered an image picker only. It now has a File row too, for every model: a text file is inlined into the prompt, and a PDF, an archive or a spreadsheet is saved where the agent's Read tool can open it. A drop or a paste sorts its files the same way — images the image way, everything else as a file. The mechanism is the image one, generalised. The bytes go over the socket (`file.attach`), the agent's new `attach_file` control copies them under their own name into the session's readable artifact directory (the same place accepted image originals go) and answers with a number, and a `[File #N]` chip lands in the draft beside a file card (name, extension, size). The draft is the truth: a chip deleted at submit un-attaches, as it does for images. At submit `_drain_pending_files` appends one block per file after the prompt — a header the clients recognise, then the contents of a text file or a Read-tool hint for a binary one, classified the way an `@path` mention is (`read_file_attachment`, shared with that pipeline). Text over 256 KB is pointed at rather than inlined; files are capped at 10 MB, refused before the upload on the client and before the decode on the gateway; eight pending files at most, as for images. A reopened conversation turns the blocks back into file cards and keeps the inlined contents out of the caption. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Follow-up noted during the e2e run (pre-existing, not changed here): |
Test Results 5 files 1 021 suites 30m 51s ⏱️ For more details on these failures, see this check. Results for commit 0fd555b. ♻️ This comment has been updated with latest results. |
…ed file's Review round. The end-anchored "+500k" turn-budget shorthand and the UserPromptSubmit hooks read the prompt through _extract_prompt_text, which joins every text block with no separator — so the file block the drain appends welded "+500k" to "[File #1: …" and silently no-op'd the budget, and hooks were handed up to 256 KB of file contents as "the prompt" (the resized-image metadata block had the same latent effect). They now read the first text block, which is what the user typed. Also: Windows reserved device names and Unicode format characters are neutralised in stored names; a large text file is pointed at without being read whole; a literal closing tag inside inlined contents can no longer end the envelope early; a file dropped at submit, on /clear or on resume takes its persisted copy with it, and losing the pending-cap race removes the whole copy folder; a relative path resolves against the session directory; the composer shows the name the backend kept; a dropped folder is skipped with a notice instead of being uploaded as an empty file; a paste with files on the clipboard attaches them uniformly; the stored header regex tolerates any name and path shape short of a newline. The text fixture is written as bytes so its size holds on Windows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… off the loop Follow-ups from the approving review: a session's shutdown discards the copies of files attached for a draft that will never be sent, as /clear and resume already did; the file drain's reads and decodes run in a thread so a slow disk cannot stall the other sessions on a multi-session transport; stored names also lose C1 controls and line separators; any spelling of the closing reminder tag is neutralised; an upload that lands after more typing inserts its chip into the current draft, not the one it started from; a dropped folder is told apart by its entry (Linux gives a folder the inode's size) and the notice says what is skipped; a user's own first block can never be read as a file block on reopen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Deferred follow-ups from the two review rounds (none loses an attachment, misroutes a prompt, or leaves files behind):
|
Why
The web composer's upload menu had an image option only. deepseek-harness offers generic file attachments beside images; this adds the same to ClawCodex.
What changed
Composer (ui-web)
[File #N]chip in the draft and a card under the text (type glyph, name, extension · size, remove ×,#Ntag). Same contract as images: the draft is the truth, deleting the chip un-attaches, the strip shows only what the draft claims.Backend
file.attachgateway RPC (twin ofimage.attach): base64 in, size refused before decode, temp file, agent control, temp removed.attach_filecontrol: copies the upload under its own (sanitized) name into the session's readable artifact directory (…/tool-results/attachments/file-<rand>/<name>, the place accepted image originals go, readable by the Read tool), queues it (cap 8), answers{id, name, path, size}._drain_pending_filesat submit: for each file whose chip survives, appends a block after the prompt —[File #N: name] saved at <path> (<size>), then the text contents (≤ 256 KB; larger files are pointed at without being read) or a Read-tool hint for binaries/PDFs/images, via a newread_file_attachmentclassifier that shares the@pathmention rules. A literal</system-reminder>inside inlined contents is neutralised so a file cannot end the envelope early./clear, resume and a deleted chip drop pending files and remove their persisted copies.+500kturn-budget shorthand, UserPromptSubmit hooks, the blocked-prompt echo) now read the first text block — the user's own words — instead of every text block joined; a trailing file block (or a resized image's metadata block) used to silently defeat the budget and hand hooks the file contents.Verification
pytest:
tests/server/test_file_attach_control.py(25 new: turn budget and hooks survive a file drain, ephemeral turns leave files queued, image → prompt → file block order, resume/clear/dropped-chip cleanup, failed copy and cap race leave nothing, persistence under the artifact dir, drain inlining, chip deletion drops, binary hint without mojibake, large-text pointer, size/pending caps, missing path,/clear, classifier, leaf sanitizing, gateway round trip / data URL / empty file / oversize / bad base64 / refused control).vitest: 718 green (new: chip kinds and labels, menu row order and vision gating, composer picker + card + remove, drop sorting, transcript hydration, message cards, captions, actions upload/refusal/limit).
Browser e2e against
clawcodex servewith the live model: attached a text file, asked for its content, got the code word back; after a reload the user row shows the file card and none of the inlined contents.Two critic review rounds: the first (REQUEST CHANGES) is answered by the second commit; the second (APPROVE with follow-ups) by the third — shutdown discards unsent copies, the file drain runs in a thread, stricter name filtering, any spelling of the closing reminder tag neutralised, uploads landing after more typing insert into the current draft, entry-based folder detection, and a user's own first block is never read as a file block.
vitest: 719 green; pytest: 10,347 green locally; Windows CI fails only the four pre-existing tests plus one wall-clock-ordering memory test unrelated to this change.
🤖 Generated with Claude Code