feat(web): rename and delete files from the files view - #8162
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.
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.
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.
|
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.
One finding: the new permanent file-delete confirmation does not use the destructive confirm variant, so its confirm button renders with the default primary treatment instead of the destructive one every other permanent-delete prompt in the app uses. Details inline.
Posted via Macroscope — UI Consistency
| // so a rename can never clobber another entry. The link and the source | ||
| // removal form one critical section: an interrupt between them would | ||
| // strand both names on disk, so the pair runs uninterruptibly. | ||
| yield* Effect.uninterruptible( |
There was a problem hiding this comment.
🟠 High workspace/WorkspaceFileSystem.ts:428
Concurrent renameEntry calls for the same source both return success and leave both target paths as hard links to the same file, so the operation does not produce a single rename and later writes through either name affect the other. Effect.uninterruptible only prevents interruption within each fiber; it does not serialize separate requests, allowing both link(source, target) calls to succeed before either remove(source.absolutePath). Serialize this critical section (at least per source or workspace) so concurrent renames cannot both publish targets.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/workspace/WorkspaceFileSystem.ts around line 428:
Concurrent `renameEntry` calls for the same source both return success and leave both target paths as hard links to the same file, so the operation does not produce a single rename and later writes through either name affect the other. `Effect.uninterruptible` only prevents interruption within each fiber; it does not serialize separate requests, allowing both `link(source, target)` calls to succeed before either `remove(source.absolutePath)`. Serialize this critical section (at least per source or workspace) so concurrent renames cannot both publish targets.
Evidence trail:
Commit dc7004a6513329593871c73a7e9157722314cf97
- apps/server/src/workspace/WorkspaceFileSystem.ts:368-388, 417-428, 430-492
- apps/server/src/workspace/WorkspaceFileSystem.ts:136-145
- apps/server/src/ws.ts:1946-1953, 2451-2457
- apps/server/src/workspace/WorkspaceFileSystem.test.ts:372-398
- https://effect.website/docs/concurrency/semaphore
- https://nodejs.org/api/fs.html (fsPromises.link, fsPromises.rename, fsPromises.writeFile)
- git diff MERGE_BASE REVIEWED_COMMIT -- apps/server/src/workspace/WorkspaceFileSystem.ts apps/server/src/ws.ts
There was a problem hiding this comment.
Accepting this edge. Neither interleaving loses data, the duplicate is a second hard link to the same inode. The workspace has no per-path locking to build on, and the rename comes from a modal dialog, so two concurrent renames of one source are not reachable from the UI. A locking model to prevent benign duplication is not worth the complexity.
There was a problem hiding this comment.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
| ); | ||
| } | ||
|
|
||
| yield* fileSystem |
There was a problem hiding this comment.
🔴 Critical workspace/WorkspaceFileSystem.ts:557
deleteEntry can remove a file outside the workspace root when another process swaps the checked parent directory for a symlink after directoryEscapesWorkspaceRoot returns. The subsequent path-based stat and remove are not protected by that check; renameEntry has the same link/remove race. Use directory-descriptor-relative, symlink-safe operations (or otherwise hold the parent validation and mutation atomically) so concurrent filesystem changes cannot bypass the root boundary.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/workspace/WorkspaceFileSystem.ts around line 557:
`deleteEntry` can remove a file outside the workspace root when another process swaps the checked parent directory for a symlink after `directoryEscapesWorkspaceRoot` returns. The subsequent path-based `stat` and `remove` are not protected by that check; `renameEntry` has the same `link`/`remove` race. Use directory-descriptor-relative, symlink-safe operations (or otherwise hold the parent validation and mutation atomically) so concurrent filesystem changes cannot bypass the root boundary.
Evidence trail:
Commit dc7004a6513329593871c73a7e9157722314cf97. Inspect `apps/server/src/workspace/WorkspaceFileSystem.ts:329-346` (canonical parent check), `:379-497` (rename validation and path-based link/remove), and `:500-560` (delete validation and path-based stat/remove). Inspect `apps/server/src/workspace/WorkspacePaths.ts:202-230` (lexical path resolution) and `apps/server/src/ws.ts:1946-1953` (RPC callers). Git command: `git show dc7004a6513329593871c73a7e9157722314cf97 -- apps/server/src/workspace/WorkspaceFileSystem.ts apps/server/src/workspace/WorkspacePaths.ts apps/server/src/ws.ts`.
There was a problem hiding this comment.
Not changing this. The endpoint requires operate scope, and operate scope already drives coding agents with full shell access in the same cwd, so anyone who can win this race can just run rm directly. The shipped writeFile path has the same check-then-act shape for the same reason. Hardening this call adds no capability boundary.
There was a problem hiding this comment.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This is a large end-to-end feature that adds authorized file uploads, renames, deletes, and cross-cutting save coordination with significant filesystem and concurrency behavior. Open review concerns include unresolved root-containment and concurrent mutation/upload races, so the changes require human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
…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.
dc7004a to
f741fda
Compare
There was a problem hiding this comment.
Reviewed the new Effect service code (WorkspaceFileSystem.renameEntry/deleteEntry, WorkspaceUpload, the new contracts errors, and the RPC/web call sites) against the Effect service conventions. Service shape, namespace imports, Foo["Service"] typing, Effect.catchTags/structural catchIf usage, and layer wiring all look correct. Two convention issues on the error modelling side are noted inline.
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.
There was a problem hiding this comment.
UI consistency review of the new files-view upload/rename/delete surfaces. The extracted WorkspaceFileDropOverlay, the Button usages (icon-xs ghost header actions, icon-micro ghost-muted row actions), and RenameEntryDialog's Dialog composition all match existing repo patterns. Two smaller consistency points below.
Posted via Macroscope — UI Consistency
f741fda to
854003f
Compare
| cwd, | ||
| relativePath, | ||
| onPendingChange, | ||
| discardSavesRef, |
There was a problem hiding this comment.
🟠 High files/FilePreviewPanel.tsx:477
Deleting or renaming the active file can still let an in-flight persist complete against the old path, recreating the deleted or pre-rename file. FileSaveCoordinator.discard() clears the timer and revision but does not cancel or invalidate the write already started by persist, which can still invoke onConfirmed; make in-flight saves abortable or ignore their completion after discard.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/files/FilePreviewPanel.tsx around line 477:
Deleting or renaming the active file can still let an in-flight `persist` complete against the old path, recreating the deleted or pre-rename file. `FileSaveCoordinator.discard()` clears the timer and revision but does not cancel or invalidate the write already started by `persist`, which can still invoke `onConfirmed`; make in-flight saves abortable or ignore their completion after discard.
Evidence trail:
f741fda: apps/web/src/components/files/fileSaveCoordinator.ts:34-40,56-81; apps/web/src/components/files/FilePreviewPanel.tsx:419-429,1102-1115; apps/web/src/components/files/FileBrowserPanel.tsx:323-331; apps/web/src/components/files/RenameEntryDialog.tsx:80-100; packages/client-runtime/src/state/projectCommands.ts:45-46,95-126; packages/client-runtime/src/state/runtime.ts:188-204. Git command: git show f741fda -- apps/web/src/components/files/fileSaveCoordinator.ts apps/web/src/components/files/FilePreviewPanel.tsx packages/client-runtime/src/state/projectCommands.ts packages/client-runtime/src/state/runtime.ts
There was a problem hiding this comment.
Leaving this. The persist is already in flight server-side while the confirm is up and the client cannot cancel it. The window is sub-second against a human dialog, and deleting again recovers.
There was a problem hiding this comment.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
| /** Drop unsaved edits without persisting; for files removed out from under the surface. */ | ||
| discard(): void { | ||
| this.disposed = true; | ||
| this.clearTimer(); |
There was a problem hiding this comment.
🟡 Medium files/fileSaveCoordinator.ts:37
discard() leaves the edited contents in optimisticFileAtom, so reopening a deleted or renamed path can display discarded data and a later edit can overwrite a recreated file with that stale content. Clear the optimistic query state when discarding, alongside resetting the coordinator revision.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/files/fileSaveCoordinator.ts around line 37:
`discard()` leaves the edited contents in `optimisticFileAtom`, so reopening a deleted or renamed path can display discarded data and a later edit can overwrite a recreated file with that stale content. Clear the optimistic query state when discarding, alongside resetting the coordinator revision.
Evidence trail:
f741fda apps/web/src/components/files/fileSaveCoordinator.ts:34-40
f741fda apps/web/src/components/files/projectFilesQueryState.ts:47-69, 100-115, 175-198
f741fda apps/web/src/components/files/FilePreviewPanel.tsx:485-486, 752-757, 1102-1115
f741fda packages/client-runtime/src/state/projectCommands.ts:47-50, 75-76
git show f741fda -- apps/web/src/components/files/fileSaveCoordinator.ts apps/web/src/components/files/projectFilesQueryState.ts apps/web/src/components/files/FilePreviewPanel.tsx
There was a problem hiding this comment.
Fixed in 2f882fd. Delete and rename now clear the file query cache, so reopening the path refetches instead of showing stale bytes.
There was a problem hiding this comment.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
| threadRef, | ||
| ).surfaces.some((surface) => surface.id === `file:${from}`); | ||
| store.closeSurface(threadRef, `file:${from}`); | ||
| if (wasOpen) store.openFile(threadRef, to); |
There was a problem hiding this comment.
🟡 Medium files/FilePreviewPanel.tsx:1114
Renaming an open file leaves its existing review comments in composerDraftTarget pointing at from, so submitting the composer attaches comments to the old, nonexistent path and the agent cannot locate them. After closeSurface/openFile, retarget those comments to to or explicitly remove/invalidate them.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/files/FilePreviewPanel.tsx around line 1114:
Renaming an open file leaves its existing review comments in `composerDraftTarget` pointing at `from`, so submitting the composer attaches comments to the old, nonexistent path and the agent cannot locate them. After `closeSurface`/`openFile`, retarget those comments to `to` or explicitly remove/invalidate them.
Evidence trail:
f741fda: apps/web/src/components/files/FilePreviewPanel.tsx:492-505, 1106-1114; apps/web/src/composerDraftStore.ts:3326-3369; apps/web/src/components/ChatView.tsx:5392-5407; apps/web/src/reviewCommentContext.ts:209-236; apps/web/src/rightPanelStore.ts:393-415, 507-537
There was a problem hiding this comment.
Leaving this. Draft comments keep the file path but carry the quoted snippet, so the context survives a rename. Diff-view drafts already have the same staleness when the agent renames files.
There was a problem hiding this comment.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
| onClose={() => setRenameTarget(null)} | ||
| onRenamed={(newRelativePath) => { | ||
| entriesQuery.refresh(); | ||
| onEntryRenamed?.(renameTarget, newRelativePath); |
There was a problem hiding this comment.
🟡 Medium files/FileBrowserPanel.tsx:643
Renaming a background file tab activates it, switching the preview away from the currently active file. onEntryRenamed is called for every successful rename, and the downstream openFile call always sets activeSurfaceId to the renamed path; preserve the active surface unless the renamed file was already active.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/files/FileBrowserPanel.tsx around line 643:
Renaming a background file tab activates it, switching the preview away from the currently active file. `onEntryRenamed` is called for every successful rename, and the downstream `openFile` call always sets `activeSurfaceId` to the renamed path; preserve the active surface unless the renamed file was already active.
Evidence trail:
Commit f741fda1: apps/web/src/components/files/FilePreviewPanel.tsx:1106-1115; apps/web/src/components/files/FileBrowserPanel.tsx:641-644; apps/web/src/rightPanelStore.ts:393-417 and 507-522.
There was a problem hiding this comment.
Fixed in 2f882fd. Renaming a background tab reopens the surface without stealing focus; the previously active surface is restored.
There was a problem hiding this comment.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
| if (queuedIndex !== -1) { | ||
| queue.splice(queuedIndex, 1); | ||
| } | ||
| job.abort?.(); |
There was a problem hiding this comment.
🟡 Medium lib/workspaceUploadQueue.ts:288
Canceling after the upload request reaches the server still allows storeWorkspaceUpload to commit the file, while cancelWorkspaceUpload clears the row as if nothing was written. This silently leaves a file on the project that only appears after a later refresh; cancellation needs a server-side cancellation/revocation check (or the client must reconcile the completed upload).
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/lib/workspaceUploadQueue.ts around line 288:
Canceling after the upload request reaches the server still allows `storeWorkspaceUpload` to commit the file, while `cancelWorkspaceUpload` clears the row as if nothing was written. This silently leaves a file on the project that only appears after a later refresh; cancellation needs a server-side cancellation/revocation check (or the client must reconcile the completed upload).
Evidence trail:
Reviewed commit 854003f. apps/web/src/lib/workspaceUploadQueue.ts:184-207, 277-290; apps/web/src/lib/uploadXhr.ts:20-33; apps/server/src/http.ts:288-334; apps/server/src/workspace/WorkspaceUpload.ts:242-268; apps/web/src/components/files/FileBrowserPanel.tsx:228-239.
There was a problem hiding this comment.
Leaving this. Cancel aborts the XHR client-side; once the bytes fully reached the server the write has committed, and reconciling would mean deleting user data. The file shows up on the next refresh.
There was a problem hiding this comment.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
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.
POSIX rename over another name of the same file is a no-op, so the same-inode rename fallback left the source entry behind when the conflict was a pre-existing hard link pair rather than a case variant. The directory listing reports exact on-disk names and tells the shapes apart: both names listed removes the source entry, only the source listed renames to apply the case change, and otherwise the target name already holds the data. The hard-link test now asserts the source entry is gone.
The hard-link-pair branch reads an identical source and target path as two listed names of one inode and removed the file's only directory entry while reporting success. The web dialog blocks unchanged names, but the RPC has no such gate. Identical resolved paths now return success before touching the filesystem, with a test pinning the file's survival.
Renaming or deleting a file from the tree only refreshed the listing. An open surface for the old path kept rendering stale contents, and its debounced saves could recreate the deleted file or the pre-rename name. Now a delete closes the file surface and a rename follows it to the new path, and the save coordinator discards unsaved edits for the removed path instead of flushing them on dispose. The delete confirm also uses the destructive dialog variant to match its menu item.
The same-file check followed symlinks, so a symlink at the target read as another name of the source's inode. The rename then removed the source and left the symlink dangling, losing the contents. The check now compares lstat identity, so a symlink is a distinct entry and the rename fails with the target-exists conflict.
The not-a-file and cross-directory stages are pure validation failures with no underlying error, so a required cause forced call sites to manufacture one. cause is now optional, matching ProjectWriteFileError, and validation stages construct without it.
Delete and rename now clear the optimistic file query so a reopened path refetches instead of showing stale bytes. The discard guard checks the file the editor shows now, not the render that created the callback, so a thread switch cannot drop the new thread's edits. Renaming a background tab no longer steals the active surface.
The symlink escape checks passed a formatted string as the error cause. Both stage unions gain an escapes-root literal with the message built in the contract, so the checks construct without a cause and clients get the same typed stage as every other failure.
…he schema RenameEntryDialog duck-typed the failure by _tag. The contracts package now exports a Schema.is predicate for ProjectRenameEntryTargetExistsError and the dialog uses it.
A case-only rename lists a single directory entry, so both names listed means the target name is genuinely occupied by another link to the same inode. Removing the source silently succeeded and dropped a real entry; report ProjectRenameEntryTargetExistsError like any other conflict.
Saves, renames, and deletes share one serial per-path command queue, so a debounced save that fires while the mutation is in flight enqueues behind it and recreates the file after a delete or at the old path after a rename. Discard the editor's pending saves when the mutation starts instead of after it succeeds, closing the queue-ordering window. The post-success discard stays as a net for a mid-flight file switch.
Rename and delete discarded pending edits up front, so a failed mutation silently lost whatever the editor still showed. The coordinator now suspends saves for the mutation and the outcome decides what follows: success discards, failure resumes and persists the held edits. Dispose while suspended skips the flush so a save cannot land behind the mutation on the shared serial queue.
The linkless rename fallback checked the target with a listing and exists(), leaving a window where a rival file created between check and rename was replaced, and a dangling symlink under other casing escaped both checks. An empty O_EXCL create now claims the target name, so the rename only ever replaces this rename's own claim; when the name is held by the source's own inode the rename is a case change and needs no claim. deleteEntry now lstats the entry, so a dangling symlink is removed instead of reading as already gone, and anything that is not a regular file or a symlink is refused, matching renameEntry's file-only contract.
Rename keyed its serial lane on the source path, so a delete of the rename's target ran on another lane and could remove the freshly renamed file. writeFile, renameEntry, and deleteEntry now share one serial lane per project, which also keeps saves ordered with mutations of the same file. An overwrite upload onto the open file left the pre-upload contents in the preview, and a later debounced save wrote that stale snapshot over the upload. The upload now clears the optimistic overlay, resets pending edits, and refetches the file. A discarded coordinator also ignores late editor changes, so the cache-key rotation after a delete can no longer revive the revision and recreate the file.
…ename Two rename gaps. After the hard link landed, the source removal ran unconditionally, so a concurrent writer's new file under the source name was deleted; the removal now runs only while the source still names the linked inode. And a stat failure between the fallback claim and the inode capture stranded the empty claim, making every retry read the name as taken; the claim is now reclaimed before the error surfaces. Tests cover both: a link-rival layer that replaces the source after the link, and a path-scoped stat failure hook.
Three save-coordination gaps around overwrite uploads. The overwrite hold now starts when the conflict is discovered, before the confirm dialog opens, so a debounced save cannot land while the dialog is up. The coordinator counts overlapping holds instead of a boolean, so a rename finishing early cannot release an upload's hold. And resume compares against a persisted-revision watermark, so a snapshot that already saved is not written again over what the upload put on disk. Settle callbacks pair with the hold: only jobs that entered the overwrite phase fire onSettled.
04ab2f9 to
c0ff793
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c0ff793. Configure here.
Creating the rival after deleting the claim lets ext4 recycle the claim inode and model a state that the real staged rename cannot produce.
Keep the original lstat cause inside Effect tryPromise’s tagged unknown error so ENOENT remains recoverable without an untagged error channel.
# Conflicts: # apps/web/src/lib/workspaceUploadQueue.ts

What changed
Files in the files view can now be renamed and deleted from their context menu.
projects.renameEntry,projects.deleteEntry), gated by the same operate scope asprojects.writeFile.This applies to the web app and the desktop wrapper, over local, relay, and tunnel connections alike. The mobile files view is unchanged.
Note: this branch is stacked on #8151, so the diff includes those upload commits until it merges. The rename and delete work is the six commits from
06d471f11up.Why
The files tab can read files and, with #8151, put them there, but renaming or deleting one still means the shell or the composer. On a remote environment driven from app.t3.codes or the tunnel that detour is the whole task. If the files tab is already open, the context menu is where these belong.
UI changes
Video of the full flow (context menu, rename, conflict, delete): rename-delete-flow.mp4
Verification
vp test run apps/server/src/workspace/WorkspaceFileSystem.test.ts), 11 of them new for rename and delete, covering conflicts, races, symlink escapes, and the self-rename no-op.git diff --checkpass.Checklist
Note
High Risk
Direct filesystem writes with complex rename/upload concurrency and path-escape logic; mistakes could corrupt or delete workspace files or allow writes outside the project root.
Overview
Adds workspace file management end to end: signed upload URLs over WebSocket plus a new
POST /api/workspace/uploadroute, andrenameEntry/deleteEntryonWorkspaceFileSystemwith same-directory-only renames, symlink/root guards, and atomic rename via hard-link claiming (with linkless-volume fallbacks).The files view gets drag-and-drop and picker uploads (with overwrite confirmation, progress, cancel/retry), context-menu Rename and Delete, and shared
WorkspaceFileDropOverlaystyling.workspaceUploadQueuehandles minting, 409 races, and per-environment concurrency;uploadXhris shared with attachment uploads.FileSaveCoordinatornow supports suspend / resume / discard / reset so pending editor saves do not race rename, delete, or overwrite uploads;FilePreviewPanelsuspends or discards saves, refreshes or closes surfaces, and reopens renamed paths while preserving reveal line and panel focus.Reviewed by Cursor Bugbot for commit d1d7ad0. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add file rename, delete, and upload to the web files view
projectsCreateUploadUrl,projectsRenameEntry, andprojectsDeleteEntryWebSocket RPCs with typed contracts in rpc.ts and project.ts; server implements signed upload URLs (WorkspaceUpload.ts), atomic same-directory rename, and tolerant delete in WorkspaceFileSystem.ts.uploadXhrhelper.FileSaveCoordinatorgainssuspend,resume,discard, andresetAPIs in fileSaveCoordinator.ts; FilePreviewPanel.tsx wires these to suspend or discard saves during rename/delete/overwrite and swaps the active surface to the new path on rename.writeFilecommand concurrency is now serialized per(environmentId, cwd)instead of per path in projectCommands.ts, so concurrent writes to different files in the same directory queue together. Uploads are capped at 100 MiB with a 10-minute URL TTL.FileSaveCoordinator.disposeonly persists when there are edits beyond the confirmed revision.Macroscope summarized d1d7ad0.