Skip to content

feat(web): rename and delete files from the files view - #8162

Open
msegec wants to merge 55 commits into
pingdotgg:mainfrom
msegec:feat/files-rename-delete
Open

feat(web): rename and delete files from the files view#8162
msegec wants to merge 55 commits into
pingdotgg:mainfrom
msegec:feat/files-rename-delete

Conversation

@msegec

@msegec msegec commented Aug 25, 2026

Copy link
Copy Markdown

What changed

Files in the files view can now be renamed and deleted from their context menu.

  • Right-clicking a file adds Rename and Delete below the existing Copy mention and Add to chat. Folders keep the read-only menu for now.
  • Rename opens a dialog prefilled with the current name, stem selected so you can type straight over it. It renames in place; the file stays in its folder.
  • Renaming onto a name that already exists fails with an inline error and touches nothing. The server creates a hard link at the target and then removes the source, so a rename can never clobber another entry, even when two clients race. Renaming a file onto its own path is a no-op.
  • Delete asks for confirmation first and then permanently removes the file.
  • Both actions are new RPCs over the existing WebSocket (projects.renameEntry, projects.deleteEntry), gated by the same operate scope as projects.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 06d471f11 up.

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

Context menu Rename dialog
File context menu with Rename and Delete entries Rename dialog prefilled with meeting-notes.txt, stem selected
Name conflict Delete confirm
Inline error when renaming onto an existing name Delete confirmation dialog for agenda.txt
Tree after rename and delete
Tree showing the renamed file and the deleted one gone

Video of the full flow (context menu, rename, conflict, delete): rename-delete-flow.mp4

Verification

  • 20 focused WorkspaceFileSystem tests pass (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.
  • Contracts, server, and web typechecks pass.
  • Targeted lint and git diff --check pass.
  • Checked the context menu, rename, conflict error, and delete confirm 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 rename and delete flow

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/upload route, and renameEntry / deleteEntry on WorkspaceFileSystem with 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 WorkspaceFileDropOverlay styling. workspaceUploadQueue handles minting, 409 races, and per-environment concurrency; uploadXhr is shared with attachment uploads.

FileSaveCoordinator now supports suspend / resume / discard / reset so pending editor saves do not race rename, delete, or overwrite uploads; FilePreviewPanel suspends 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

  • Adds projectsCreateUploadUrl, projectsRenameEntry, and projectsDeleteEntry WebSocket 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.
  • The web files view gains an upload button with drag-and-drop, a per-upload progress/failure row with cancel/retry/dismiss, a rename dialog, and a delete-with-confirmation flow in FileBrowserPanel.tsx and RenameEntryDialog.tsx.
  • A zustand-backed upload queue in workspaceUploadQueue.ts enforces per-environment concurrency, handles 409 overwrite confirmations, and uses the shared uploadXhr helper.
  • FileSaveCoordinator gains suspend, resume, discard, and reset APIs 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.
  • Behavioral Change: writeFile command 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.dispose only persists when there are edits beyond the confirmed revision.

Macroscope summarized d1d7ad0.

msegec added 13 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.
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.
@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: 4e38b58c-458d-40b6-bf3c-ae02c1ee998b

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

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.

❤️ Share

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:XXL 1,000+ 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 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

Comment thread apps/web/src/components/files/FileBrowserPanel.tsx
Comment thread apps/web/src/components/files/FileBrowserPanel.tsx
Comment thread packages/contracts/src/project.ts Outdated
Comment thread apps/web/src/components/files/FileBrowserPanel.tsx
// 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(

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.

🟠 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/web/src/lib/workspaceUploadQueue.ts Outdated
);
}

yield* fileSystem

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.

🔴 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`.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/server/src/workspace/WorkspaceFileSystem.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 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:

  • 11 blocking correctness issues found at or above your repo's Minimum Blocking Severity

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.
@msegec
msegec force-pushed the feat/files-rename-delete branch from dc7004a to f741fda Compare August 25, 2026 03:04

@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 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

Comment thread apps/server/src/workspace/WorkspaceFileSystem.ts Outdated
Comment thread apps/web/src/lib/workspaceUploadQueue.ts Outdated
Comment thread packages/contracts/src/project.ts Outdated
Comment thread apps/web/src/components/files/RenameEntryDialog.tsx Outdated
Comment thread packages/contracts/src/project.ts Outdated
Comment thread apps/web/src/components/files/FilePreviewPanel.tsx
Comment thread apps/web/src/components/files/fileSaveCoordinator.ts
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.

@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.

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

Comment thread apps/web/src/components/files/FileBrowserPanel.tsx Outdated
Comment thread apps/web/src/lib/workspaceUploadQueue.ts Outdated
@msegec
msegec force-pushed the feat/files-rename-delete branch from f741fda to 854003f Compare August 25, 2026 03:13
Comment thread apps/web/src/lib/workspaceUploadQueue.ts Outdated
cwd,
relativePath,
onPendingChange,
discardSavesRef,

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.

🟠 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/web/src/components/files/FileBrowserPanel.tsx
/** Drop unsaved edits without persisting; for files removed out from under the surface. */
discard(): void {
this.disposed = true;
this.clearTimer();

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.

🟡 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 2f882fd. Delete and rename now clear the file query cache, so reopening the path refetches instead of showing stale bytes.

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.

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);

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.

🟡 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/server/src/workspace/WorkspaceUpload.ts
onClose={() => setRenameTarget(null)}
onRenamed={(newRelativePath) => {
entriesQuery.refresh();
onEntryRenamed?.(renameTarget, newRelativePath);

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.

🟡 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 2f882fd. Renaming a background tab reopens the surface without stealing focus; the previously active surface is restored.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
Comment thread apps/web/src/components/files/FilePreviewPanel.tsx Outdated
if (queuedIndex !== -1) {
queue.splice(queuedIndex, 1);
}
job.abort?.();

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.

🟡 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

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.

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.
msegec added 20 commits August 25, 2026 14:32
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.
@msegec
msegec force-pushed the feat/files-rename-delete branch from 04ab2f9 to c0ff793 Compare August 25, 2026 06:32

@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 1 potential issue.

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 c0ff793. Configure here.

Comment thread apps/web/src/components/files/fileSaveCoordinator.ts
Comment thread apps/web/src/components/files/fileSaveCoordinator.ts Outdated
msegec added 6 commits August 25, 2026 14:56
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
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