feat(webchat): add file attach button and drag-and-drop for image uploads#34082
feat(webchat): add file attach button and drag-and-drop for image uploads#34082jaylane wants to merge 12 commits intoopenclaw:mainfrom
Conversation
…oads Add a paperclip (📎) button to the webchat compose area that opens the native file picker for image attachments (jpg, png, gif, webp). Also adds drag-and-drop support on the compose area. This complements the existing clipboard paste support by providing two additional attachment methods that users expect from a modern chat UI. Changes: - Add hidden file input + paperclip attach button to compose row - Add drag-and-drop handlers (dragover/dragleave/drop) on compose area - Extract shared readFileAsAttachment() helper (also used by paste) - Add CSS for attach button, drag-over visual state, and mobile layout - Reuse existing ChatAttachment type and preview/remove UI Closes openclaw#4846, closes openclaw#18454 AI-assisted: yes (Hex/Claude Opus 4.6, fully tested locally)
Greptile SummaryThis PR adds two new image-attachment entry points to the webchat compose area — a paperclip attach button (file picker) and drag-and-drop — alongside a shared Two remaining issues:
Confidence Score: 3/5
Last reviewed commit: 0a5746e |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a296bae82
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rop guard - Fix multi-file race condition: batch all FileReader reads with Promise.all before calling onAttachmentsChange once, preventing stale snapshot overwrites when selecting/dropping multiple images. - Fix dragleave flicker: check compose.contains(e.relatedTarget) before removing dragover class, so moving between child elements (e.g. textarea) doesn't flash the indicator. - Guard handleDrop against disconnected state to match attach button. - Refactor: extract readFileAsDataUrl() returning Promise<ChatAttachment> and addAttachments() helper used by all three entry points (file picker, drag-and-drop, paste).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 55d7fc30d3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…atch reads - Add error event listener to FileReader so promise rejects on failure - Switch from Promise.all to Promise.allSettled so one bad file doesn't stall the entire batch — successful reads still attach
Only preventDefault for file drags — text and URL drops fall through to the textarea's default behavior. Check dataTransfer.types for Files in dragover, and files.length > 0 in drop.
- Add 10MB file size limit in addAttachments to prevent memory pressure from large images - Pass props to handleDragOver and early-return when disconnected, matching handleDrop's pattern - Prevents misleading drag-over visual feedback when user is not connected Addresses review feedback from greptile-apps
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab3dd0e9ce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const current = props.attachments ?? []; | ||
| props.onAttachmentsChange?.([...current, ...newAttachments]); |
There was a problem hiding this comment.
Append async file results to current attachment state
addAttachments snapshots props.attachments before Promise.allSettled resolves, then writes onAttachmentsChange([...current, ...newAttachments]) from that stale value. In the new file-picker/drag-drop flows, users can trigger overlapping attachment reads (or clear/send attachments) before the first read finishes; when each promise settles, later completions can overwrite earlier ones or reintroduce attachments that were already cleared. This should merge against the latest attachment state at completion time rather than the render-time snapshot.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Technically valid but practically a non-issue here. FileReader.readAsDataURL for local image files resolves in sub-millisecond — the async API shape is misleading. For the stale snapshot to matter, a user would need to trigger two independent attachment actions (e.g. drag-drop AND paste) within the same microtask window, which is physically impossible with human input.
The real guard is that all three entry points (paste, file picker, drop) funnel through the same addAttachments call, and Promise.allSettled batches all files from a single action into one onAttachmentsChange call. There's no interleaving within a single user gesture.
If we ever add network-fetched attachments (URL paste, etc.) where read latency is real, we'd want a callback-style updater (onAttachmentsChange(prev => [...prev, ...new])), but that's a different PR.
There was a problem hiding this comment.
To use Codex here, create an environment for this repo.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a299347be2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ui/src/ui/views/chat.ts
Outdated
| const MAX_FILE_BYTES = 10 * 1024 * 1024; // 10 MB | ||
|
|
||
| function addAttachments(files: File[], props: ChatProps) { | ||
| const imageFiles = files.filter((f) => f.type.startsWith("image/") && f.size <= MAX_FILE_BYTES); |
There was a problem hiding this comment.
Align webchat attachment size cap with gateway limit
The new attachment gate allows files up to 10 MB (MAX_FILE_BYTES), but chat.send currently enforces a 5,000,000-byte maximum when parsing attachments (parseMessageWithAttachments(..., { maxBytes: 5_000_000 }) in src/gateway/server-methods/chat.ts). In practice, users can now pick/drop/paste 5–10 MB images, see them accepted in compose, and then hit INVALID_REQUEST on send; this is a new mismatch in the upload UX introduced with the attach/drag-drop flow.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — the client cap was 10 MB but the gateway enforces 5 MB (parseMessageWithAttachments maxBytes: 5_000_000). Fixed in ad79dec — aligned MAX_FILE_BYTES to 5_000_000.
Also added 8 tests for the attachment flow while I was in there: attach button state, file input wiring, size rejection, non-image rejection, drag-drop connected guard, dragover class gating, and multi-file batching.
|
Re: Likely needs a |
MAX_FILE_BYTES was 10 MB but the gateway enforces 5,000,000 bytes in parseMessageWithAttachments. Users could select/drop 5-10 MB images, see them accepted in compose, then hit INVALID_REQUEST on send. Also adds attachment test coverage: - attach button renders and respects connected state - file input wiring (click propagation) - valid image files trigger onAttachmentsChange - files > 5 MB are silently rejected - non-image files are rejected - drop events ignored when disconnected - dragover class gated on connected state - multiple files batched into single callback
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad79dec794
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const current = props.attachments ?? []; | ||
| props.onAttachmentsChange?.([...current, ...newAttachments]); |
There was a problem hiding this comment.
Use latest attachment state when async reads finish
addAttachments appends newAttachments to props.attachments captured from the render that started Promise.allSettled, so if the user removes/clears attachments or triggers another attach flow before file reads resolve, this callback can reintroduce removed items or overwrite newer state. Fresh evidence: FileReader.readAsDataURL for 5 MB images takes tens to hundreds of milliseconds in current Chromium/Firefox/WebKit, so this race is realistic in normal UI interactions.
Useful? React with 👍 / 👎.
|
Closing — this feature was implemented upstream. The attach button is now live in the webchat UI. 🎉 |
demo.mov
Summary
Add a 📎 paperclip button to the webchat compose area that opens the native file picker for image attachments (jpg, png, gif, webp). Also adds drag-and-drop support on the compose area.
Currently the webchat only supports clipboard paste for image attachments. This PR adds the two other attachment methods users expect:
Changes
ui/src/ui/views/chat.ts<input type="file">and paperclip attach button to compose rowhandleDragOver/handleDragLeave/handleDrophandlers on compose areareadFileAsAttachment()helper (refactored out ofhandlePaste)image/jpeg,image/png,image/gif,image/webpui/src/styles/chat/layout.cssDesign Decisions
ChatAttachmenttype which usesdataUrl+mimeType. Generic file support would need a different transport mechanism.icons.paperclipSVG.Testing
Related Issues
Closes #4846, closes #18454
🤖 AI-assisted: Built with Hex (Claude Opus 4.6). Fully tested locally. The code was reviewed and understood before submission.