Skip to content

feat(web): upload files into the project from the files view - #8151

Open
msegec wants to merge 30 commits into
pingdotgg:mainfrom
msegec:feat/files-view-upload
Open

feat(web): upload files into the project from the files view#8151
msegec wants to merge 30 commits into
pingdotgg:mainfrom
msegec:feat/files-view-upload

Conversation

@msegec

@msegec msegec commented Aug 25, 2026

Copy link
Copy Markdown

What changed

The files view can now put files into the project, not just read them.

  • Drop files anywhere on the files panel, or use the new upload button next to refresh. Each upload gets a progress row and the tree refreshes as files land.
  • The client asks the server to mint a signed upload URL over the existing WebSocket (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.
  • The server writes to a .part temp file and renames it into place, so a dropped connection never leaves a half-written file at the target path.
  • Uploading over an existing file asks for confirmation first. The existence check happens when the URL is minted, so the prompt appears before any bytes are sent.
  • Uploads cap at 100 MiB and target the project root in this first pass. Folder drops are filtered out.

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.

Before Drop overlay
Files panel before, read-only tree Drop files to upload overlay while dragging
Upload in progress After upload
Progress row for a 24 MiB upload Tree showing both uploaded files
Overwrite confirm
Replace meeting-notes.txt confirmation dialog

Video of the full flow (drop, large upload with progress, overwrite confirm): upload-flow.mp4

Verification

  • 151 focused server, web queue, and attachment tests pass (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).
  • Server and web typechecks pass.
  • Targeted lint and git diff --check pass.
  • Checked drop upload, button upload, progress, overwrite confirm, and tree refresh in an isolated real-app preview using copied project data.

Checklist

  • This PR is small and focused on one concern
  • I explained what changed and why
  • I included screenshots of the UI changes
  • I included a short video of the upload flow

Made with Claude Fable 5 using the Claude Code harness.


Note

Medium Risk
New authenticated write path into project workspaces with signed tokens and filesystem edge cases; mitigated by path canonicalization, size limits, and broad server/integration tests.

Overview
Adds workspace file uploads from the Files panel (and reuses a shared drop overlay in chat): drag-and-drop or a picker, progress/cancel/retry rows, tree refresh, and preview reload when an open file is overwritten.

The flow is mint signed URL over WebSocket (projects.createUploadUrl, operate scope) then POST bytes to /api/workspace/upload/<token>. Contracts define a 100 MiB cap and 10 minute token TTL. The server streams into a .part file, enforces claimed size and Content-Length when present, validates tokens (including kind separation from attachment uploads), blocks paths outside the workspace (including post-mint symlink cases), handles overwrite/conflict with body draining for reliable HTTP errors, and commits with rename/link plus FAT/exFAT fallbacks.

The web client adds workspaceUploadQueue (per-environment concurrency, per-path serialization, overwrite confirm on mint failure or 409) and factors uploadXhr out of attachment uploads for shared rejection handling. User docs cover the Files view upload behavior.

Reviewed by Cursor Bugbot for commit 7d81edf. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add file uploads to workspace files panel

  • Adds drag-and-drop and file picker uploads to the files panel using a new client-side queue that tracks progress, concurrency, cancellation, and retries.
  • Introduces a projects.createUploadUrl WebSocket RPC to mint signed upload URLs and a new HTTP POST route to stream request bodies directly to disk.
  • Server enforces size and path validation, supports overwrite semantics, rejects root escapes and symlinks, and cleans up temporary files on all exits.
  • Risk: The new projects.createUploadUrl RPC requires AuthOrchestrationOperateScope; upload size is capped at 100 MiB via PROJECT_UPLOAD_MAX_BYTES.

Macroscope summarized 7d81edf.

msegec added 8 commits August 25, 2026 07:12
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.
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b6a8324b-961b-4b44-ad7f-49232435099e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 25, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/files/FileBrowserPanel.tsx Outdated
Comment thread apps/web/src/components/files/FileBrowserPanel.tsx Outdated
Comment thread apps/web/src/components/files/FileBrowserPanel.tsx Outdated
Comment thread apps/web/src/components/files/FileBrowserPanel.tsx Outdated
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
Comment thread apps/web/src/lib/workspaceUploadQueue.ts
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
Comment thread apps/server/src/workspace/WorkspaceUpload.ts
Comment thread packages/contracts/src/project.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a substantial authenticated workspace-write capability, including a new upload RPC, signed HTTP route, filesystem commit logic, and client-side queue/UI behavior. Its auth-directory changes and broad production blast radius require human review despite the extensive focused test coverage.

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.
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 25, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/contracts/src/project.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/chat/WorkspaceFileDropOverlay.tsx Outdated
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.
Comment thread apps/server/src/workspace/WorkspaceUpload.ts
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
Comment thread apps/server/src/workspace/WorkspaceUpload.ts
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
msegec added 3 commits August 25, 2026 09:22
…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.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/contracts/src/project.ts Outdated
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.
@msegec
msegec force-pushed the feat/files-view-upload branch from d06e4a6 to 3ec000c Compare August 25, 2026 03:10
Comment thread apps/server/src/workspace/WorkspaceUpload.ts
Comment thread apps/web/src/lib/workspaceUploadQueue.ts Outdated
Comment thread apps/server/src/workspace/WorkspaceUpload.ts
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.
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
msegec added 2 commits August 25, 2026 12:00
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.
Comment thread apps/server/src/workspace/WorkspaceUpload.ts
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
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.
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
Comment thread apps/server/src/workspace/WorkspaceUpload.ts
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.
Comment thread apps/server/src/workspace/WorkspaceUpload.ts
msegec added 3 commits August 25, 2026 13:28
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.
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
msegec added 3 commits August 25, 2026 14:32
Workspace uploads buffered the whole body in server memory, up to
100 MiB per request with no server-side concurrency cap, so parallel
uploads could hold many full bodies at once. The body now streams
straight into the .part staging file; the claimed size is enforced
while bytes land, and oversized, truncated, and unreadable bodies
still reject with 400. Cleanup on every exit is unchanged.

The client also discarded the server's rejection detail and showed
only a generic status line, and a file over the 100 MiB limit failed
with an unhelpful mint error. Failed rows now show the server's
plain-text reason when one is present, and oversized files fail
immediately with the limit named.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding on error-cause preservation for the new workspace upload body error. Everything else in the changed Effect code (namespace subpath imports, catchTags for known tags, structural catchIf on platform error reasons, environment-acquired WorkspacePaths/FileSystem/WorkspaceEntries dependencies, exported Schema.is predicate) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/http.ts Outdated
Macroscope flagged that mapping the request stream error into a
field-less WorkspaceUploadBodyError discards the underlying platform
failure. The error now requires a cause, the wrap site threads the
stream error through, and the 400 branch logs it the same way other
degraded paths do.
@msegec

msegec commented Aug 27, 2026

Copy link
Copy Markdown
Author

Related: #8235 and #8092 add file attachments to the composer. This PR reuses the same signed-URL upload pattern but targets the project tree from the files view, so the two do not overlap in scope. Both #8092 and this branch touch apps/web/src/lib/attachmentUploadQueue.ts; happy to rebase once the attachment work lands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 20cb9c4. Configure here.

Comment thread apps/web/src/components/files/FileBrowserPanel.tsx
Comment thread apps/server/src/workspace/WorkspaceUpload.ts
Four upload defects from PR review. A non-overwrite conflict answered 409
without consuming the request body, so clients mid-send saw a connection
reset instead of the conflict message; the server now drains the body,
bounded by the claimed size, before responding. A finished upload only
refreshed the tree, leaving a replaced open preview showing stale bytes
that a later save could write back; completion now reloads the open file
when it was the target. Two queued uploads for the same target could run
concurrently, prompting twice and racing the server rename; jobs sharing
an environment, cwd, and path now run one at a time. A cancel that raced
a completed request cleared the row while the server had committed the
file; the entries refresh now still runs so the tree shows the file.
Comment thread apps/web/src/components/files/FileBrowserPanel.tsx Outdated
An upload completing between a selection commit and the passive effect
flush read the previous selection and skipped reloading the replaced
preview. The refs now update in a layout effect, which runs before the
browser can deliver the completion event.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant