Repository navigation
Cycle 37: media upload feature (byte cap + composer UI) - #6
Merged
Merged
Conversation
Operator asked for uploadable images/files in channels with client- side compression and limits looser than Discord's. Investigated the existing pipeline first: the backend is already fully built and working end-to-end (initiate/upload/serve/attach-to-message) - this is purely a frontend gap. Assigned: Codex-18 (bump the 25MB cap to 50MB, fix two hardcoded error-message strings that would otherwise drift from the constant), AG-20 (composer drag-drop/file-picker/client-side image compression), AG-21 (inline attachment rendering in messages). Also corrected AG-19's brief - there is no approve/reject route to wire against; uploads auto-approve today, so nothing is actually stuck in quarantine yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MAX_ATTACHMENT_BYTES was duplicated between app.ts (a local const) and initiateUploadRequestSchema's size .max() in protocol.ts, both hardcoded to 25MB independently - bumping only one would have left the schema silently capping uploads at the old limit regardless. Moved the constant into @cove/contracts as the single source of truth, bumped it to 50MB, and updated both call sites plus the two hardcoded "25 MB" error-message strings to derive from it instead of repeating the number. Updated the existing over-limit test's threshold and added a test for the new ceiling. 2 new tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codex-18 done directly by Claude, AG-20 (composer attachment UI) running via Grok CLI tonight, AG-21 (inline rendering) left queued for Antigravity's normal rotation, per operator's explicit 3-way split request. Also documents a real correction: the Grok CLI's default model had resolved to grok-composer-2.5-fast, which its own model registry labels as Cursor's model (agent_type: "cursor"), not xAI's. Verified via --debug-file output that -m grok-build correctly resolves to xAI's own model against cli-chat-proxy.grok.com. All invocations from here on pin -m grok-build explicitly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the existing Composer.tsx (message textarea/reply-bar/send) with attachment support: - Wires the previously-inert "Add attachment" button to a hidden file input, plus drag-and-drop over the whole composer form. - Client-side image compression: only for image/* files over 2MB, scales the longest edge to at most 2560px via canvas, re-encodes as JPEG and WebP in parallel and keeps whichever is smaller, with a graceful fallback to the original file on any error. Small images and non-image files pass through untouched. - Pending-attachment strip above the textarea (thumbnail for images, filename chip for everything else) with per-item remove. - New props: pendingAttachments, onFilesSelected, onRemoveAttachment. Composer stays presentation-only - no network calls, no permanent file-list ownership; the actual initiate/upload calls happen where this gets wired into App.tsx next. Built via a parallel Grok CLI trial run in a disposable clone (this time pinned to -m grok-build explicitly after discovering the CLI's default had resolved to Cursor's Composer model), reviewed and independently re-verified before landing. 6 new tests, including canvas/Image/URL mocking for the compression path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates the attachment upload experience end-to-end for Cycle 37 by raising the backend attachment size cap to 50MB (with a single shared constant in @cove/contracts) and adding a presentation-only attachment picker/drag-drop + client-side image compression UI to the web composer (with unit tests and minimal styling).
Changes:
- Centralize and raise the attachment size limit to 50MB across both backend enforcement and request-schema validation.
- Extend
Composerwith attachment selection (button + drag/drop), pending-attachment preview strip, and client-side image compression for large images. - Add/extend unit tests for both the backend attachment pipeline limit and the composer attachment behaviors.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| WORKLOG.md | Records Codex-18 + AG-20 landing notes and queue updates. |
| status/grok.md | Status report for AG-20 work (composer attachment UI). |
| services/core/src/app.ts | Uses shared MAX_ATTACHMENT_BYTES and updates limit error strings accordingly. |
| services/core/src/app.test.ts | Updates/extends tests for the new 50MB initiation cap. |
| packages/contracts/src/protocol.ts | Introduces MAX_ATTACHMENT_BYTES and uses it in initiateUploadRequestSchema. |
| apps/web/src/styles.css | Adds composer attachment strip/chip styling and drag-over highlight. |
| apps/web/src/Composer.tsx | Adds attachment UI hooks, compression logic, drag/drop handlers, and preview URL lifecycle management. |
| apps/web/src/Composer.test.tsx | Adds jsdom-friendly tests covering picker, drag/drop, preview strip, removal, and compression gating. |
- compressImage now keeps the original file if every re-encoded candidate (JPEG/WebP) turns out larger than the source - some source formats (e.g. certain PNGs) don't shrink well, and blindly swapping to the "smallest of the re-encodes" could increase upload size despite being on the compression path. Added a test that forces oversized re-encodes and asserts the original is kept. - Attachment chips now use a stable per-File key (a WeakMap-backed id assigned on first render) instead of the array index. Removing an item from the middle of the list shifts every subsequent index, which could cause React to reuse the wrong chip's DOM (and its <img>/blob URL) for a different file. - Removed a redundant `grid-column: 1 / -1` declaration on `.attachment-strip` - it's already covered by the combined `.reply-bar, .attachment-strip` rule a few lines down. Co-Authored-By: Claude Sonnet 5 <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.
Summary
Stacks on #5 (not yet merged). Operator asked for uploadable images/files in channels with client-side compression and limits looser than Discord's — split three ways:
MAX_ATTACHMENT_BYTESfrom 25MB to 50MB. Found and fixed a deeper issue while doing it: the limit was duplicated betweenapp.tsand an independent.max()oninitiateUploadRequestSchemainprotocol.ts— bumping only one would have left uploads silently capped at the old value via schema rejection. Moved the constant into@cove/contractsas the single source of truth for both.Composer.tsxwith drag-drop, file-picker wiring, and client-side image compression (canvas resize to a 2560px longest edge + JPEG/WebP re-encode, only for images over 2MB, non-images and small images pass through untouched). Presentation-only — no network calls; the actual upload flow wiring intoApp.tsxis a separate next step.Also fixed a real correction discovered mid-session: the Grok CLI's default model had resolved to
grok-composer-2.5-fast, which its own model registry labels"Cursor's latest coding model"(agent_type: "cursor") — not xAI's. All invocations from AG-20 onward pin-m grok-buildexplicitly, verified via--debug-fileoutput showingmodel: "grok-build"againstcli-chat-proxy.grok.com.The backend attachment pipeline itself (initiate/upload/serve/quarantine-approve/attach-to-message) was already fully built before this PR — confirmed by reading the code, not assumed.
Test plan
npm run typecheck— clean across all 5 workspacesnpm test— 278/278 passing (123 core + 117 web + 24 contracts + 14 desktop)npm run format:check— clean🤖 Generated with Claude Code