feat(web): upload files into the project from the files view - #8151
feat(web): upload files into the project from the files view#8151msegec wants to merge 25 commits into
Conversation
Adds the createUploadUrl command atom and a client-side upload queue for workspace files: FIFO pump capped at 3 concurrent uploads per environment, XHR-based byte upload with progress, an overwrite confirm flow for ProjectUploadTargetExistsError, and retry/cancel/dismiss for failed rows.
Floor the workspace upload body limit at 1 byte so a 0-byte upload token can't disable NodeStream's max-body check for a chunked request with no Content-Length. Route the overwrite confirm dialog through readLocalApi() like every other caller instead of calling requestConfirmDialog directly. Extract the duplicated XHR upload helper (attachments, workspace) into apps/web/src/lib/uploadXhr.ts. Raise the workspace upload timeout to 10 minutes to match the 100 MiB max and the upload token TTL. Scope the files view upload docs to web and desktop.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Two Effect convention issues in apps/server/src/workspace/WorkspaceUpload.ts. Everything else (namespace imports for effect/* and local service modules, dependency acquisition via yield* Foo.Foo, Effect.catch over the fully handled channel, no runtime boundaries in service code) looks consistent with the conventions.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
Reviewed the new files-view upload UI for consistency with the shared component system and existing upload surfaces. Four findings, all in apps/web/src/components/files/FileBrowserPanel.tsx: an unbounded upload list that can collapse the file tree, row action buttons that don't follow the established compact-row control contract, a divergent progress format, and a drop overlay duplicated verbatim from ChatView.
Posted via Macroscope — UI Consistency
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This adds a substantial end-to-end file-upload capability spanning authorization, RPC contracts, server-side filesystem writes, and client queue/UI behavior. The new authenticated endpoint writes up to 100 MiB into workspaces and changes an auth-package file, so the runtime and security surface warrant human review. You can add or adjust custom eligibility rules. Learn more. |
Store the non-overwrite upload with an atomic hard link so a concurrent upload gets a 409 instead of silently replacing the file, and ignore a second retry click while the retried job is already uploading. Share one drop-overlay component between the chat and files views, reuse the attachment progress formatter, cap the uploads strip height, size the row buttons to the compact-row contract, and name the mint target in the resolve error message.
There was a problem hiding this comment.
One convention finding on the new ProjectCreateUploadUrlError declaration. The previously flagged Effect.catchTag("WorkspacePathOutsideRootError", ...) in apps/server/src/workspace/WorkspaceUpload.ts (line 181) is still present — Effect.catchTags({ ... }) is the convention even for a single tag.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
One finding on the new shared drop-overlay component; the four items from the previous run (upload strip max-height/scroll, micro row-action contract, shared progress formatter, extracted overlay) all look addressed.
Posted via Macroscope — UI Consistency
The lexical resolve cannot see symlinked directory components, so a signed claim for a path under an in-workspace symlink could write outside the project. Canonicalize the workspace root and the target directory before any bytes land and reject with 400, the same guard AssetAccess applies to signed reads.
…ages Check the deepest existing ancestor against canonical paths before recursive mkdir so a symlinked component cannot create directories outside the workspace, derive ProjectCreateUploadUrlError messages from a stage discriminator like the sibling file errors, and merge consumer classNames into the shared drop overlay instead of letting them replace the treatment.
The repo's Effect conventions check requires catchTags for statically known tagged failures even with a single tag.
…rupt cleanup The part file now uses a fixed-length UUID name beside the target, so a long target basename cannot exceed the 255-byte filename component limit. The canonical containment check now rejects only a real parent traversal, so in-root directories like '..config' upload fine. A part file left by fiber interruption is reclaimed with an ensuring finalizer, since Effect.catch does not run on interrupts.
…resh callback throws The success path cleared the job map and upload state before invoking onUploaded, so a throwing callback fell into the failure handler and recreated the entry as failed with no job left to retry. The callback now runs in its own guard and only logs.
There was a problem hiding this comment.
Reviewed the new Effect-side code (apps/server/src/workspace/WorkspaceUpload.ts, the HTTP route, ws handler, and contracts) against the service conventions. The upload module mirrors AttachmentUpload.ts (namespace subpath imports, dependencies acquired via yield* Foo.Foo, catchTags for known tags, catchIf only for a platform reason._tag), and ProjectCreateUploadUrlError now derives its message from a structural stage with a required cause. One error-modeling issue remains in the newly added rename/delete contracts.
Posted via Macroscope — Effect Service Conventions
The signed token base64url-encodes the workspace cwd, so a long but valid cwd could push the relative url past the 4096 bound and fail result encoding. 8192 clears a PATH_MAX cwd plus the longest relative path after encoding overhead.
d06e4a6 to
3ec000c
Compare
Directory targets are rejected at mint with a target-not-file stage and at store with a 409, so an overwrite can no longer end in a generic 500 while renaming the part file over a folder. The ancestor walk stops at the filesystem root. The replace confirm renders as destructive, the retry button uses the retry icon, and the target-exists check derives from the contracts schema. cause is optional on ProjectCreateUploadUrlError so validation stages construct without one.
Non-overwrite uploads landed via link plus rename, and link fails with EPERM on FAT and exFAT volumes, turning every such upload into a 500. Write the target directly with O_EXCL instead; the exclusive create is just as atomic against a concurrent file at the target and works on filesystems without hard links.
A failed fallback write removed the target before surfacing, which could delete a rival's file created after the exists check. The fallback now claims the name with an empty O_EXCL create and renames the part onto its own claim, so the failed create removes nothing and only a failed rename reclaims the name, by which point the name holds this upload's claim. The staged part is also reclaimed before the entries refresh, so the rebuilt index never lists a phantom part entry.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b630758. Configure here.
An interrupt between the fallback's claim and rename stranded a permanent empty file at the target, and a failed rename removed the target unconditionally, which in a double race could delete a rival's confirmed overwrite. The claim and rename now run uninterruptibly, and the reclaim only removes the target while it still holds the empty claim. Upload jobs now trim the file name at creation. The RPC schema trims the path on encode, so an untrimmed name stored the file under a different path than the one the progress row and overwrite dialog reported. Empty names fail up front.
A rival's confirmed overwrite can legitimately be zero bytes, so the failed-rename reclaim could not tell it from the empty claim and deleted it. The reclaim now also compares the inode captured at claim time and falls back to the size check only where the platform reports no inode. The rival simulation in the tests now replaces the inode like a real rename, and a zero-byte rival test pins the survival.
A stat failure between the wx claim and the inode capture surfaced the error while leaving the empty claim at the target, so every later upload to that path read the name as taken. The claim is now force-removed before the error surfaces.

What changed
The files view can now put files into the project, not just read them.
projects.createUploadUrl), then POSTs the raw bytes to/api/workspace/upload/<token>. The token carries the target path, size, and a 10 minute expiry, so file bytes never travel over the WebSocket..parttemp file and renames it into place, so a dropped connection never leaves a half-written file at the target path.This applies to the web app and the desktop wrapper, over local, relay, and tunnel connections alike. The mobile files view is unchanged.
Why
Getting a file into the workspace currently means the shell or composer attachments, and attachments only feed the agent's turn, they never land in the project. For anyone driving a remote environment from app.t3.codes or the tunnel there is no path at all. Reusing the signed URL pattern the asset routes already use keeps auth in one place and keeps the WebSocket free of file payloads.
UI changes
The panel gains one upload button beside refresh; everything else appears only while an upload is in flight.
Video of the full flow (drop, large upload with progress, overwrite confirm): upload-flow.mp4
Verification
vp test run apps/server/src/workspace/WorkspaceUpload.test.ts apps/server/src/server.test.ts apps/web/src/lib/workspaceUploadQueue.test.ts apps/web/src/lib/attachmentUploadQueue.test.ts).git diff --checkpass.Checklist
Note
Medium Risk
New authenticated HTTP endpoint writes arbitrary paths inside the workspace with complex concurrency and symlink handling; mistakes could cause path escape or data loss, though the PR adds extensive guards and tests.
Overview
Adds workspace file upload from the files panel: drag-and-drop, a picker button, per-file progress, cancel/retry/dismiss, and a shared drop overlay with chat.
The client mints a signed URL over WebSocket (
projects.createUploadUrl), then POSTs bytes to/api/workspace/upload/<token>(up to 100 MiB, 10-minute TTL). File payloads stay off the socket. Overwrites prompt when minting fails with “exists” or the server returns 409.On the server,
WorkspaceUploadissues and validatesworkspace-uploadtokens (same signing key as assets/attachments), writes via a.partstaging file with path/symlink checks and atomic commit (hard link or rename fallback), refreshes the workspace index, and enforces Content-Length / body limits (including 0-byte uploads).projects.createUploadUrlrequires orchestration operate scope.Contracts and client-runtime expose the new RPC; attachment uploads now share
uploadXhr. User docs describe the files-view upload flow (root-only in v1; folder drops ignored).Reviewed by Cursor Bugbot for commit 90e9527. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add file upload to Files view with signed URLs, queue, and progress UI
projects.createUploadUrlWS-RPC mints time-limited signed URLs, a new POST route atWORKSPACE_UPLOAD_ROUTE_PREFIXvalidates tokens and stores bytes with path-escape, symlink, size, and overwrite safety, and a linkless-volume fallback handles filesystems without hard links.MAX_UPLOADS_PER_ENVIRONMENT), plus cancel, retry, dismiss, and single-overwrite-retry on mint-time or commit-time 409 conflicts.WorkspaceFileDropOverlayshows during drag. Upload rows show coarse progress, failure reasons, and retry/dismiss actions.UploadRejectedErrorfrom the old inlineuploadBytes.AuthOrchestrationOperateScopein RpcAuthorization.ts and documents the feature in files.md.MaxBodySizeto 1 byte for zero-byte claims so empty uploads succeed; chunked uploads that exceed the claimed size return 400 without creating the file.Macroscope summarized 90e9527.