🤖 feat: sync a per-project VS Code .code-workspace file with worktrees - #3932
🤖 feat: sync a per-project VS Code .code-workspace file with worktrees#3932ibetitsmike wants to merge 8 commits into
Conversation
…rktrees Opt-in per project: set a .code-workspace path in Settings -> Runtimes and xum reconciles the file's folders with the project's active worktree workspaces on create/rename/fork/archive/unarchive/delete and at startup. Only entries under the project's managed worktree root are ever touched; user folders, comments, and settings blocks are preserved via jsonc edits. Fixes #3722
UAT found the oRPC handler's plain Error mapped to a generic "Internal server error" toast; throw ORPCError(BAD_REQUEST) so the extension hint reaches the user.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae0d83e53f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- derive managed roots from each workspace's runtimeConfig.srcBaseDir so custom/legacy roots (e.g. pre-rename ~/.mux/src) stay synced - derive per-project checkout paths for multi-project workspaces (primary included; namedWorkspacePath is the _workspaces symlink container) - serialize read-modify-write per canonical file path (two projects can share one file) - write through symlinked workspace files instead of replacing the link - bound startup reconciliation with raceWithAbortAndTimeout (10s) - inline the context method instead of manual useCallback (React Compiler)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0602c0db98
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 0602c0db98
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
- bound every sync internally (raceWithAbortAndTimeout, 10s) so lifecycle operations cannot hang on a stalled filesystem; startup keeps its outer cap - capture a deleted workspace's managed roots before config removal so custom/legacy-root entries are still cleaned up - sanitize non-string codeWorkspaceSyncPath at config load (matches customInstructions handling) so projects.list cannot be bricked - reconcile all projects targeting the same file together so same-basename projects sharing a managed root cannot erase each other's entries - cap file size (1 MiB) before the synchronous JSONC parse
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1042b0d825
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 1042b0d825
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
- scope deletion-time extra roots per involved project (managedRootsByProject) so one project's file never gains removal rights under another's root - group shared-file participants by canonical (realpath) target so symlink aliases of one file reconcile together - treat a matching subProjectPath as project membership and sync the sub-project's file on lifecycle changes of workspaces assigned to it - surface sync failures from the explicit settings save (ORPCError) and roll the setting back so a broken integration is not silently retried
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e22dfea4cc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: e22dfea4cc
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
- one-pass folders reconcile (single jsonc edit) so repo-controlled files with thousands of entries cannot block the main thread quadratically - reject non-regular targets and read via one descriptor with a byte cap (a symlink to /dev/zero reported size 0 but never reached EOF) - derive desired state inside the per-file critical section so an older lifecycle snapshot cannot overwrite a newer write - bound every canonicalization realpath (1s) so another project's dead mount cannot stall an unrelated sync - guard the setter rollback so a concurrent newer save is never discarded - include devcontainer workspaces (host worktrees under the default root) - capture removed-workspace roots best-effort so removal never fails on it
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 19a7aeac06
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
This comment has been minimized.
This comment has been minimized.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 253966103b
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 253966103b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!isWorktreeRuntime(metadata.runtimeConfig)) { | ||
| return rootsByProject; |
There was a problem hiding this comment.
Retain devcontainer roots during cleanup
Fresh evidence beyond the resolved devcontainer inclusion fix: when the last devcontainer workspace assigned to a sub-project is removed or reassigned, this guard returns no cleanup root. After the metadata disappears or loses its old assignment, reconciliation only knows the sub-project's default root rather than the actual parent-project checkout root, so the old .code-workspace entry remains permanently; derive the devcontainer root from namedWorkspacePath here as computeManagedWorktreePaths already does.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in feb51a2be: managedRootsByProject now derives devcontainer roots from the checkout's parent directory, mirroring computeManagedWorktreePaths. Unit test added (red without the fix).
| const participantCanonical = await boundedRealPath(participantFile); | ||
| return participantCanonical === canonicalTarget ? participantPath : null; |
There was a problem hiding this comment.
Preserve participants when canonicalization times out
Fresh evidence beyond the resolved alias-grouping and canonicalization-timeout fixes: if two same-basename projects share this target through different aliases and one alias times out transiently, this drops that participant from desired while the surviving participant still contributes their overlapping managed root. The ensuing reconciliation therefore deletes the timed-out project's valid folders from the shared file; an unresolved candidate must make removal conservative rather than being treated as a confirmed non-participant.
Useful? React with 👍 / 👎.
| const text = jsonc.applyEdits( | ||
| original, | ||
| jsonc.modify(original, ["folders"], newFolders, MODIFY_OPTIONS) | ||
| ); |
There was a problem hiding this comment.
Preserve comments inside the folders array
Whenever any folder must be added or removed, replacing folders from the parsed value reserializes the entire array and discards comments within it, including comments attached to user-owned entries. This contradicts the documented promise in docs/runtime/worktree.mdx that comments are left untouched and causes user-authored content to disappear on an ordinary lifecycle sync; preserve the array's syntax trivia or narrow the advertised guarantee.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in feb51a2be by narrowing the advertised guarantee, as the finding suggests: the docs now state that comments inside the folders array are not preserved when entries change (everything outside it is). The single-array-edit approach is deliberate after the earlier quadratic-reparse finding; preserving intra-array trivia would reintroduce per-entry edit complexity.
| for (const involvedPath of involvedPaths) { | ||
| await syncProjectCodeWorkspace(this.config, involvedPath, { |
There was a problem hiding this comment.
Bound the full multi-project reconciliation
Fresh evidence beyond the resolved per-sync lifecycle timeout: this loop awaits each involved project's independently bounded sync in sequence, so a multi-project workspace with N distinct configured files on slow mounts can still block create, remove, rename, archive, or unarchive for roughly N × 10 seconds. Apply an aggregate bound or reconcile independent targets concurrently so the lifecycle operation remains responsive as the project count grows.
AGENTS.md reference: AGENTS.md:L10-L10
Useful? React with 👍 / 👎.
| original, | ||
| jsonc.modify(original, ["folders"], newFolders, MODIFY_OPTIONS) | ||
| ); | ||
| await writeFileAtomic(targetPath, text); |
There was a problem hiding this comment.
Detect external edits before replacing the workspace file
The in-process queue only serializes Xum callers: if VS Code or the user saves this file after original is read but before this atomic rename, the generated text is still based on the stale snapshot and overwrites the newer settings, extensions, or folder edits. Check that the target still matches the read version and retry reconciliation before replacing it so normal concurrent editor saves cannot lose user changes.
Useful? React with 👍 / 👎.
| const outcome = await raceWithAbortAndTimeout(work, { timeoutMs: SYNC_TIMEOUT_MS }); | ||
| if (outcome.kind !== "ok") { | ||
| log.warn("Timed out syncing .code-workspace file; continuing in background", { | ||
| projectPath, | ||
| }); | ||
| return { ok: false, error: "Timed out accessing the workspace file" }; |
There was a problem hiding this comment.
Cancel timed-out explicit saves before they can write
Fresh evidence beyond the resolved save rollback: a timeout returns failure while work explicitly continues in the background, so the settings handler rolls the path back and tells the user the save failed, but a stalled filesystem operation that later resumes can still create or rewrite the selected file from the pre-rollback snapshot. Propagate cancellation or revalidate the setting immediately before writing so a failed explicit save cannot mutate the file afterward.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0247540cc5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const projects = config.loadConfigOrDefault().projects; | ||
| const targetFile = resolveConfiguredCodeWorkspacePath( | ||
| normalizedProjectPath, | ||
| projects.get(normalizedProjectPath)?.codeWorkspaceSyncPath |
There was a problem hiding this comment.
Reconcile a sub-project before unregistering it
When projects.remove unregisters a sub-project that has assigned workspaces, the sub-project path is cleared and its project config is deleted without synchronizing its configured workspace file. This code derives the target exclusively from the current projects map, so after deletion neither startup nor later lifecycle reconciliation can find the old file, leaving Xum-managed worktree entries there permanently. Capture the setting and contributed roots and reconcile the file as part of sub-project removal.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 0247540cc5
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
| const newFolders = [...kept, ...additions.map((folderPath) => ({ path: folderPath }))]; | ||
| const text = jsonc.applyEdits( | ||
| original, | ||
| jsonc.modify(original, ["folders"], newFolders, MODIFY_OPTIONS) |
There was a problem hiding this comment.
Security: Cap synchronous folder-array serialization work
Requires the operator to enable sync for a repository-controlled file. Fresh evidence after the one-pass fix: with pinned jsonc-parser 3.3.1, a valid 90,016-byte folders array of 30,001 {} entries plus one desired addition spends 14.65s here; 180,016 bytes did not finish in 60s. Both pass the 1 MiB cap. Because jsonc.modify is synchronous, neither 10s race can fire, freezing Electron/server startup or lifecycle RPC; the path is persisted before initial sync, so killing/restarting can repeat. Enforce an entry/work budget or move formatting to an interruptible worker.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in feb51a2be: files with more than MAX_CODE_WORKSPACE_FOLDERS (2,000) folder entries are refused before any jsonc edit, bounding the synchronous serialization work (at the cap the measured cost is milliseconds, given the superlinear scaling you quantified). Regression test added (red without the guard).
Summary
Opt-in per-project sync of a VS Code
.code-workspacefile with the worktree runtime: set a workspace-file path for a project in Settings → Runtimes and xum keeps that file'sfoldersarray in sync with the project's active worktree workspaces, so all worktrees are browsable from one VS Code / code-server multi-root window.Fixes #3722
Background
Worktrees live under
~/.xum/src/<project>/<workspace>, invisible from an editor window opened on the main checkout. The issue (approved) asks xum to manage worktree paths in a.code-workspacefile. The feature is opt-in per project because most users won't want xum writing editor files unprompted.Implementation
src/node/worktree/codeWorkspaceSync.ts: reconcile module with a managed-entry invariant: xum only adds/removes folder entries whose resolved path is under the project's managed worktree root (<srcDir>/<projectName>/). User-added folders, the seeded project-root entry, comments, andsettings/extensionsblocks are never touched (JSONC edits viajsonc-parser, atomic writes viawrite-file-atomic). Malformed files are skipped, never clobbered; no-op syncs don't rewrite the file. Sync never throws, so lifecycle operations and startup cannot fail because of it.taskIsolation: "none"excluded); multi-project workspaces contribute their per-project checkout path.WorkspaceServicecreate/createMultiProject/fork/rename/remove/archive/unarchive, plus startup reconciliation to repair drift from lifecycle changes while the app was closed. Rename is currently API-only (the UI rename action is intentionally omitted upstream until a proper modal exists); the sync wiring covers it for when that lands.codeWorkspaceSyncPathonProjectConfigSchema;projects.setCodeWorkspaceSyncPathORPC setter validates the.code-workspaceextension (BAD_REQUEST with a user-visible message) and syncs immediately on save. Clearing the setting never deletes previously written files.docs/runtime/worktree.mdx.Validation
codeWorkspaceSync.test.ts, router setter tests), including red-green on the managed-root invariant and on the client-visible validation error.Risks
Low. The feature is inert unless a project opts in. The write path is scoped by the managed-root invariant, skips malformed files, and all sync failures degrade to a log line rather than failing workspace operations.
Generated with
xum• Model:anthropic:claude-fable-5• Thinking:xhigh