Skip to content

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

Closed
msegec wants to merge 66 commits into
pingdotgg:mainfrom
msegec:feat/files-rename-delete
Closed

feat(web): rename and delete files from the files view#8162
msegec wants to merge 66 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 commits on top of that upload branch.

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

Made with Claude Fable 5 using the Claude Code harness.


Note

Medium Risk
Changes direct workspace filesystem mutations and a new signed upload HTTP path; mitigated by path canonicalization, size/token checks, and broad server and client tests, but bugs could still corrupt or expose files outside the project root.

Overview
Adds rename, delete, and signed HTTP upload for project workspace files, wired through new WebSocket RPCs (projects.createUploadUrl, projects.renameEntry, projects.deleteEntry) with operate-scope auth and integration in the files browser.

On the server, WorkspaceFileSystem gains same-directory rename (no overwrite; hard-link claim with FAT/exFAT fallbacks) and file delete, serialized with writes via a mutation lock and canonical parent checks to block symlink escape. WorkspaceUpload mints expiring tokens, validates Content-Length, streams bodies into .part staging, then atomically commits (link or rename) with conflict draining and root checks; POST /api/workspace/upload/* exposes the upload endpoint.

In the web app, the file tree context menu adds rename/delete (plus RenameEntryDialog), drag/drop and picker uploads with a progress queue, and a shared WorkspaceFileDropOverlay. FileSaveCoordinator now supports suspend/resume/discard/reset so pending editor saves do not race rename, delete, or overwrite uploads; FilePreviewPanel closes or retargets open tabs, clears stale cache, refreshes image assets, and renameReviewCommentPath moves file review comments on rename. Attachment uploads share a extracted uploadXhr helper.

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

Note

Add file upload, rename, and delete to the files view (up to 100 MiB)

  • Adds upload (button + drag-and-drop with progress), rename dialog, and delete via context menu to FileBrowserPanel.
  • Server adds renameEntry and deleteEntry to WorkspaceFileSystem under a serialized mutation lock, plus a streaming upload endpoint validated by signed tokens.
  • FileSaveCoordinator gains suspend, resume, discard, and reset to hold or drop unsaved edits around mutations; FilePreviewPanel wires these callbacks to prevent save races.
  • A Zustand upload queue manages up to 3 concurrent uploads per environment, target serialization, overwrite confirmation, and retry/cancel.
  • Behavioral Change: writeFile concurrency key in projectCommands.ts changed from per-path to per-project-root (environmentId + cwd) to serialize saves, renames, and deletes.

Macroscope summarized 5556344.

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: caada5b4-5060-4cf9-8df3-5486e5d55134

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: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
Comment thread apps/server/src/workspace/WorkspaceFileSystem.ts
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 XXL production feature adds signed uploads plus destructive rename/delete operations and changes shared save coordination across the web and server. Path-based containment and rollback race windows still plausibly permit outside-workspace effects or data loss, requiring human review.

Not approved because:

  • 10 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.

Comment thread apps/web/src/components/files/FilePreviewPanel.tsx Outdated
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
Comment thread apps/web/src/lib/workspaceUploadQueue.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.
msegec added 3 commits August 25, 2026 14:32
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
Comment thread apps/web/src/components/files/fileSaveCoordinator.ts
Comment thread apps/web/src/components/files/fileSaveCoordinator.ts Outdated
msegec added 9 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
Renaming a symlink silently duplicated it on macOS: the source check used
stat, so a symlink-to-file passed as a plain file, and the hard-link claim
followed the symlink and linked its referent under the new name. The
same-inode guard then saw different inodes and returned success with the
symlink still on disk.

The source check now uses lstat, matching deleteEntry, and a symlink source
skips the hard-link claim for the O_EXCL claim plus rename path, which never
dereferences the source. A test layer that resolves the source before
linking reproduces the macOS link(2) semantics on Linux and pins the fix.

Also disables the rename dialog's submit for . and .., which are path
components the server rejects with a generic error, and notes in the files
view doc that renaming and deleting are not available on mobile.
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 WorkspaceUploadBodyError. Everything else in the new Effect surface (the WorkspaceFileSystem service additions, WorkspaceUpload module, contracts errors and predicates, catchTags/structural catchIf usage, subpath namespace imports) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/workspace/WorkspaceUpload.ts Outdated
msegec added 4 commits August 27, 2026 08:37
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.

@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 492b3d3. Configure here.

Comment thread apps/web/src/components/files/fileSaveCoordinator.ts
msegec added 3 commits August 27, 2026 18:43
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.
Two renames of the same file could run concurrently on the server, both
hard-link their targets before either removed the source, and both report
success, leaving the file under both names. Rename and delete now share one
mutation lock, so the loser re-checks a source the winner already moved and
fails.

Deleting a file re-resolved its path after the canonical parent check, so a
parent directory swapped for a symlink in that window redirected the removal
outside the workspace. The delete now re-resolves the parent immediately
before the removal and refuses when it no longer names the checked directory,
shrinking the window to the gap before the unlink syscall itself.

Renaming an open file left drafted review comments pointing at the old path,
so a submitted draft referenced a file that no longer exists. File review
comments now follow the rename.
# Conflicts:
#	apps/web/src/components/files/FileBrowserPanel.tsx
#	apps/web/src/lib/workspaceUploadQueue.ts
// An overwrite upload replaced the bytes on disk; drop the
// stale optimistic overlay and pending edits so a later save
// cannot write the pre-upload snapshot over the upload.
clearProjectFileQueryData(environmentId, cwd, path);

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:1140

Uploading over a background file leaves its cached readFile result unchanged, so reopening it within the 30-second stale window displays the pre-upload contents. onEntryUploaded only calls file.refresh() when editorShowsFile(path) is true; refresh the query for every uploaded path, not just the currently displayed file.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/files/FilePreviewPanel.tsx around line 1140:

Uploading over a background file leaves its cached `readFile` result unchanged, so reopening it within the 30-second stale window displays the pre-upload contents. `onEntryUploaded` only calls `file.refresh()` when `editorShowsFile(path)` is true; refresh the query for every uploaded path, not just the currently displayed file.

The FAT fallback rename may only replace its own exclusive claim, but a
concurrent write to the same target could land between the claim and the
rename and be lost. Writes now share the mutation lock with rename and
delete, so a mutation never overlaps a write to the same workspace.
Effect.catchTags({
PlatformError: (cause) =>
Effect.gen(function* () {
yield* fileSystem.remove(target.absolutePath, { force: true }).pipe(Effect.ignore);

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:599

A failed source removal can delete another writer's replacement at target.absolutePath, causing data loss. The rollback unconditionally removes the target after the hard-link claim, without verifying that the target still refers to this rename's linked inode; check isSameFile before removing it.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/workspace/WorkspaceFileSystem.ts around line 599:

A failed source removal can delete another writer's replacement at `target.absolutePath`, causing data loss. The rollback unconditionally removes the target after the hard-link claim, without verifying that the target still refers to this rename's linked inode; check `isSameFile` before removing it.

@t3dotgg

t3dotgg commented Aug 28, 2026

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

File rename and delete are destructive workspace mutations that need clear collision, trash, remote, and concurrent-refresh behavior. We have not selected that contract for the Files view.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotgg t3dotgg closed this Aug 28, 2026
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.

2 participants