feat: add chat attachments (opaque upload + prompt references) - #23
Open
alexanderkreidich wants to merge 7 commits into
Open
feat: add chat attachments (opaque upload + prompt references)#23alexanderkreidich wants to merge 7 commits into
alexanderkreidich wants to merge 7 commits into
Conversation
Users can attach files in the chat UI; the client ships them as opaque base64 bytes, agent-server stores them in the project workspace at attachments/<id>/<filename>, and prompts referencing attachment ids get the workspace paths appended so the agent can read the files with its own tools when needed.
- agent-server: POST /v1/projects/{id}/attachments, attachment storage module, prompt composition, tests
- agent-protocol: regenerated contract (UploadAttachmentRequest, AttachmentInfo, PromptRequest.attachments)
- agent-client: AgentClient.uploadAttachment, sendPrompt attachments param, ChatPanel attach button + chips
alexanderkreidich
marked this pull request as ready for review
August 26, 2026 15:45
The attach button and pending-attachment chips had no CSS rules, so they rendered with browser-default styling (white button on the dark theme). Style them with the existing --ac-* tokens to match the composer.
Author
|
human reviewed |
Server: - Enforce the ~25 MB upload cap as an HTTP body limit, so an oversized request is refused while streaming instead of after being fully read and JSON-parsed (previously one request could OOM the process serving every project). - Validate base64 strictly. `Buffer.from(s, "base64")` never throws and silently drops a trailing partial group, so a truncated payload was stored as a corrupt file with a `size` that didn't match the upload. - Accept zero-byte files (base64 of an empty file is the empty string). - Truncate filenames by UTF-8 byte length, keeping the extension. POSIX caps a path segment at 255 bytes, so a long CJK/emoji name passed the 255-character check and then failed with an uncaught ENAMETOOLONG 500. - Add a 200 MB per-project attachment quota, reported as 413. - Shape validation failures on the upload route as the documented ErrorResponse instead of a raw zod issue tree. Client: - Restore the prompt text and attachment ids when a send fails. They were cleared before the request, so a rejected prompt lost the typed text and the only handle on the already-uploaded files. - Cap a file selection at the server's 20-attachment-per-prompt limit. - Reset pending attachments when the active session changes; ChatPanel is not remounted, so chips followed the user into the next session. - Hide the internal attachment note from the rendered user message and show the attached filenames as chips instead. The note is now wrapped in `<attached-files>` delimiters that the reducer strips. Deploy: - Exclude `.pi` and `attachments` from app image builds. The build context is the project root, so every upload was copied into the app image on each rebuild. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`sessionLabel` reads `firstMessage`, which is the stored prompt — so it carried the `<attached-files>` note that the transcript already strips. The sidebar showed "What is in these files? <att…". Found by driving the live UI; the transcript and history paths were already covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The textarea, the attach button, and Send were three flex siblings each carrying the same border and background at three different heights (40.2 / 36 / 35.5px), bottom-aligned. So the paperclip read as a second, shorter input field beside the real one, and the tops stepped. Wrap them in `.agent-chat-composer`, which now owns the border, background and radius; the textarea is transparent and borderless, the attach button is a ghost icon button, and every control is sized off one `--ac-control-h` so the row cannot drift again. Attach stays left (an input affordance) and Send right (the commit action), matching the composer convention. The textarea also grows with its content up to `max-height: 40vh` instead of scrolling a one-line window, which is what makes bottom-aligned buttons meaningful — they stay beside the last line typed. Height is synced from `scrollHeight` on the `input` value, so programmatic clears (send, session switch) shrink it too. Also adds focus styling: `:focus-within` on the shell (the textarea sets `outline: none` and previously had no focus indication at all) and `:focus-visible` rings on both buttons. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hidden file input behind the paperclip used `display: none`. WebKit
refuses to open a file chooser for a programmatic `.click()` on an input
with no rendered box — it does nothing, silently — so the attach button was
dead in Safari and any WebKit shell. Chromium happens to allow it, which is
why this went unnoticed.
Clip the input instead of removing it from layout (a 1x1 box with
`clip-path: inset(50%)` and `pointer-events: none`), which every engine
accepts. It also gets `tabIndex={-1}` and `aria-hidden`, since the paperclip
button is already the accessible control and a rendered input would
otherwise add a duplicate tab stop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds chat attachments: users can attach files in the chat UI; the client ships them as opaque base64 bytes, agent-server stores them in the project workspace, and the agent reads them with its own file tools when a prompt references them.
POST /v1/projects/{id}/attachments— JSON base64 upload (~25 MB decoded cap), stored atattachments/<uuid>/<sanitized-filename>inside the project dirPOST …/sessions/{id}/promptaccepts optionalattachments: string[]; the server resolves ids to workspace-relative paths and appends a note to the prompt text; unknown ids → 400runtime/attachments.ts(sanitization, id validation, prompt composition) +http/attachmentsRoutes.tsUploadAttachmentRequest,AttachmentInfo,PromptRequest.attachments,uploadAttachmentoperation)AgentClient.uploadAttachment(projectId, filename, bytes),sendPrompt(…, attachments?), ChatPanel attach button (📎) + pending-attachment chips + upload error surfaceDesign notes
QA / smoke test
Tested revision:
5c380ae(this PR's head; the container image was built from exactly this code — the only host-side working-tree deltas were files not part of the image build context).Environment: local outer builder container (
container/Dockerfile+run-outer.sh, Docker/OrbStack on macOS), agent modelopenai/gpt-5.6-lunaserved by an OpenOrange (LiteLLM) instance via the existingLITELLM_*provider envs.Scenarios & results (all pass):
POST /v1/projects/{id}/attachmentswith base64launch-codes.txt→ 200, file verified on disk atattachments/<id>/launch-codes.txtinside the project workspace volume.POST …/promptwith the attachment id → user message carries the appended attachment note; the agent autonomously called itsreadtool on the workspace path and answered the file's passphrase verbatim.npm run check(biome),npm run typecheck,npm test— 170 agent-server tests (7 new), 93 agent-client tests (2 new), 2 protocol tests.Out of scope
Container runner env pass-throughs (
run-outer.sh) for the OpenOrange/LiteLLM wiring are deliberately not in this PR and will be proposed separately.