Skip to content

🤖 feat: sync a per-project VS Code .code-workspace file with worktrees - #3932

Open
ibetitsmike wants to merge 8 commits into
mainfrom
mike/fix-3722-code-workspace-sync
Open

🤖 feat: sync a per-project VS Code .code-workspace file with worktrees#3932
ibetitsmike wants to merge 8 commits into
mainfrom
mike/fix-3722-code-workspace-sync

Conversation

@ibetitsmike

Copy link
Copy Markdown
Contributor

Summary

Opt-in per-project sync of a VS Code .code-workspace file with the worktree runtime: set a workspace-file path for a project in Settings → Runtimes and xum keeps that file's folders array 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-workspace file. 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, and settings/extensions blocks are never touched (JSONC edits via jsonc-parser, atomic writes via write-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.
  • Desired set: active (non-archived) top-level worktree workspaces of the project (sub-agent children and taskIsolation: "none" excluded); multi-project workspaces contribute their per-project checkout path.
  • Wiring: WorkspaceService create/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.
  • Setting: codeWorkspaceSyncPath on ProjectConfigSchema; projects.setCodeWorkspaceSyncPath ORPC setter validates the .code-workspace extension (BAD_REQUEST with a user-visible message) and syncs immediately on save. Clearing the setting never deletes previously written files.
  • UI: "VS Code workspace file" field in Settings → Runtimes under the per-project scope, following the Instructions draft + Save pattern.
  • Docs: short section in docs/runtime/worktree.mdx.

Validation

  • 20 new tests (codeWorkspaceSync.test.ts, router setter tests), including red-green on the managed-root invariant and on the client-visible validation error.
  • Remote dogfood UAT on dev.coder.com against the pushed SHA: PASS across the create/archive/unarchive/delete lifecycle, startup drift repair, relative-path resolution, user-content preservation, and disabled-by-default inertness. UAT's one in-scope finding (validation error surfaced as "Internal server error") is fixed here and covered by a regression test.

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

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

mintlify Bot commented Aug 23, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Mux 🟢 Ready View Preview Aug 23, 2026, 9:23 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/node/services/workspaceService.ts Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/browser/contexts/ProjectContext.tsx Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
- 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)
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

All six findings addressed in 0602c0d; threads replied to and resolved. Please take another look.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/node/services/workspaceService.ts Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/common/schemas/project.ts
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: 0602c0db98

View security finding report

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.

Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
- 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
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

All five round-2 findings addressed in 1042b0d; threads replied to and resolved. Please take another look.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/node/services/workspaceService.ts Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts
Comment thread src/node/orpc/router.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: 1042b0d825

View security finding report

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.

Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
- 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
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

All four round-3 findings addressed in e22dfea; threads replied to and resolved. Please take another look.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/node/orpc/router.ts
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: e22dfea4cc

View security finding report

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.

Comment thread src/node/worktree/codeWorkspaceSync.ts
- 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
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

All six round-4 findings (including both security threads) addressed in 19a7aea; threads replied to and resolved. Also fixed the Test / Unit CI regression from the round-2 pre-removal capture. Please take another look.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/browser/features/Settings/Sections/RuntimesSection.tsx Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts Outdated
Comment thread src/node/worktree/codeWorkspaceSync.ts
@chatgpt-codex-connector

This comment has been minimized.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: 253966103b

View security finding report

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.

Comment thread src/node/worktree/codeWorkspaceSync.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +430 to +431
if (!isWorktreeRuntime(metadata.runtimeConfig)) {
return rootsByProject;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in feb51a2be: managedRootsByProject now derives devcontainer roots from the checkout's parent directory, mirroring computeManagedWorktreePaths. Unit test added (red without the fix).

Comment on lines +544 to +545
const participantCanonical = await boundedRealPath(participantFile);
return participantCanonical === canonicalTarget ? participantPath : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +284 to +287
const text = jsonc.applyEdits(
original,
jsonc.modify(original, ["folders"], newFolders, MODIFY_OPTIONS)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +5474 to +5475
for (const involvedPath of involvedPaths) {
await syncProjectCodeWorkspace(this.config, involvedPath, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +579 to +584
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" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +497 to +500
const projects = config.loadConfigOrDefault().projects;
const targetFile = resolveConfiguredCodeWorkspacePath(
normalizedProjectPath,
projects.get(normalizedProjectPath)?.codeWorkspaceSyncPath

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: 0247540cc5

View security finding report

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Integrate worktree runtime with .code-workspace file

1 participant