Group threads by worktree#3898
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review This PR introduces a new feature for grouping threads by worktree, including new UI, keybindings, and thread creation behavior changes. Additionally, 15 unresolved review comments identify potential bugs around race conditions, path handling, and defaults that warrant human review. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9b96569856
ℹ️ 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".
| return ( | ||
| serverConfigs.get(newThreadProjectRef.environmentId)?.settings ?? DEFAULT_SERVER_SETTINGS |
There was a problem hiding this comment.
Use target environment defaults for explicit project starts
When starting a thread from the sidebar or the command palette's "New thread in..." flow, startNewThreadInProjectFromContext is passed the selected projectRef, but the default mode/start-from-origin values here are resolved from newThreadProjectRef (the active/default project) instead. In a multi-environment setup where the selected project lives on an environment with different defaultThreadEnvMode or newWorktreesStartFromOrigin, the new draft is created with the wrong workspace mode for that target project.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in afe1ae1e: wire target-scoped defaults through sidebar, command-palette, and shortcut entry points.
| environmentId: projectRef.environmentId, | ||
| input: { cwd: project.workspaceRoot, limit: 100 }, | ||
| }); | ||
| if (result._tag === "Failure") return activeProjectMainCheckout; |
There was a problem hiding this comment.
Avoid reusing the active checkout for another project
If listRefs fails while resolving the main checkout for a project other than the active one, this falls back to activeProjectMainCheckout. In the "New thread in..." or sidebar project button flows, that can seed a draft for project B with project A's branch/worktree path, so the first send can run in the wrong checkout instead of the selected project's checkout.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Regression coverage added in fd4f7526 to ensure failed target discovery never reuses another project's checkout.
| const workspaceEnvironmentId = renderedThreads[0]?.environmentId ?? null; | ||
| const workspaceProjectId = renderedThreads[0]?.projectId ?? null; |
There was a problem hiding this comment.
Build worktree identity per project in grouped rows
With the default repository project grouping, renderedThreads can include threads from several project ids/checkouts in the same repo, but this derives a single workspace identity from only the first thread. Threads from the other grouped checkouts that have worktreePath: null will not have their project checkout path substituted, so they are split into project-id buckets and mislabeled instead of being grouped with their actual worktree.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Regression coverage added in 0e60ec29 for grouped rows containing multiple project checkouts.
| ? renderedThreads.map(renderThread) | ||
| : null} | ||
| {shouldShowThreadPanel && threadGroupingMode === "worktree" | ||
| ? renderedThreadGroups.flatMap((group) => [ |
There was a problem hiding this comment.
Keep keyboard order aligned with grouped rows
In worktree grouping mode, this renders each group's threads together, so a sorted list like A/worktree1, B/worktree2, C/worktree1 appears as A, C, B. The jump labels, next/previous navigation, and shift-selection ranges are still computed from the ungrouped thread order, so those shortcuts no longer match the visual row order whenever groups are interleaved.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in b4fa9a38: navigation, jump labels, and range selection now use grouped visual order.
| const defaultRef = activeProjectBranches.data.refs.find( | ||
| (ref) => !ref.isRemote && ref.isDefault, | ||
| ); | ||
| return defaultRef ? { branch: defaultRef.name, path: null } : undefined; |
There was a problem hiding this comment.
Use the current checkout branch for local drafts
When a normal project checkout is currently on a feature branch while the default branch is main, this fallback returns main with path: null. Local-mode drafts do not run a checkout before the first send, so the agent still runs in the current cwd on the feature branch while the thread metadata/bootstrap records main, making sidebar/status data and later worktree bases point at the wrong branch.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 69592d82: local drafts are seeded from the branch actually checked out in the project checkout.
| setPendingServerThreadEnvMode("worktree"); | ||
| setPendingServerThreadBranch(worktree.branch); | ||
| void updateThreadMetadata({ |
There was a problem hiding this comment.
Preserve the selected worktree for fast sends
For an empty server thread, selecting an existing worktree only stores the env mode and branch as pending while the worktreePath update is sent asynchronously. Until that metadata update round-trips, the send path still sees activeThread.worktreePath === null and treats worktree mode as a request to create a brand-new worktree, so pressing Enter immediately after selecting an existing worktree can run in a different checkout than the one selected.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 9145cbb1: pending existing-worktree selections are honored immediately by the send path.
| projectRef.environmentId === newThreadProjectRef.environmentId && | ||
| projectRef.projectId === newThreadProjectRef.projectId | ||
| ) { | ||
| return activeProjectMainCheckout; |
There was a problem hiding this comment.
Wait for active ref discovery before seeding drafts
For the active project, this returns the memoized activeProjectMainCheckout even while the branch query is still loading or has failed, instead of running the resolver path below. Creating a default worktree draft in that window seeds branch: null, so the first send is blocked by "Select a base branch" despite the project having a current branch; local/main-checkout starts can similarly miss the discovered main checkout until the query happens to finish.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Regression coverage added in 67955356 for starting a draft while active ref discovery is still pending.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Drive-letter paths can differ only by casing across refs and thread metadata, so workspace matching must ignore case for those paths. Co-authored-by: Cursor <cursoragent@cursor.com>
withActiveWorkspaceFallback should run even when listRefs returns data, since the first page can omit the thread's current worktree. Co-authored-by: Cursor <cursoragent@cursor.com>
Pass the effective checkout path into BranchToolbar so empty server threads show the selected worktree before metadata round-trips. Co-authored-by: Cursor <cursoragent@cursor.com>
Sidebar workspace identity should only treat an explicit main checkout path as main, not fall back to the project workspace root. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3abce121a5
ℹ️ 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 activeWorktreePath = | ||
| activeWorktreePathOverride !== undefined | ||
| ? activeWorktreePathOverride | ||
| : (serverThread?.worktreePath ?? draftThread?.worktreePath ?? null); |
There was a problem hiding this comment.
Forward pending worktree paths to the branch selector
When an empty server thread has a pending existing-worktree selection, this override makes the workspace picker show the selected checkout immediately, but BranchToolbarBranchSelector still reads serverThread.worktreePath/draft state and sees null until updateMetadata returns. If the user changes the branch before that round-trip, the branch picker treats the thread as “New worktree” from the project cwd and can write worktreePath: null instead of switching/reusing the selected checkout.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 5e3872d3: the branch selector now uses the pending worktree path immediately.
| function normalizeWorkspacePath(path: string): string { | ||
| return path.trim().replaceAll("\\", "/").replace(/\/+$/, ""); |
There was a problem hiding this comment.
Normalize Windows workspace keys case-insensitively
For Windows-backed environments, the same checkout can be reported with different drive-letter/path casing by thread metadata vs. git worktree list; this normalizer only fixes separators/trailing slashes, so C:/repo and c:/repo become different sidebar group keys and fail the main-checkout comparison. In that case threads from the same worktree are split or the main checkout label is missed even though the paths refer to the same checkout.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 8b6e2257: Windows drive and UNC workspace keys are normalized case-insensitively, with regression coverage.
| threadGroupingMode === "worktree" | ||
| ? projectMembers.map((member) => ({ | ||
| member, | ||
| atom: vcsEnvironment.listRefs({ | ||
| environmentId: member.environmentId, | ||
| input: { cwd: member.workspaceRoot, limit: 100 }, | ||
| }), |
There was a problem hiding this comment.
Avoid loading refs for hidden project rows
Because SidebarProjectThreadList is mounted for every project row, default worktree grouping now builds and reads a listRefs atom for every member project even when shouldShowThreadPanel is false and no grouped rows will be rendered. In sidebars with many collapsed projects or repository-grouped members, this issues unnecessary git ref/worktree scans for hidden rows; gate these targets on the panel actually being shown.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 9d2ed11c: hidden project rows no longer subscribe to ref scans.
| const options = deriveWorkspaceOptions( | ||
| branchState.data?.refs ?? [], | ||
| activeProject.workspaceRoot, | ||
| branchState.data?.mainCheckoutPath, |
There was a problem hiding this comment.
Include worktrees outside the first ref page
When a repo has more than the first 100 refs returned by useBranches, any existing worktree whose branch sorts after that page never reaches deriveWorkspaceOptions, so it is absent from the workspace picker unless it is the active fallback. The same partial page can also fabricate the main checkout as HEAD when its path is known but its branch ref was not loaded, so users with large repos cannot reliably choose existing checkouts.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 9644cd87: the workspace picker now loads all ref pages before deriving its complete worktree list.
| }) | ||
| .pipe( | ||
| Effect.matchEffect({ | ||
| onFailure: (error) => |
There was a problem hiding this comment.
Propagate unexpected origin-base resolution failures
When startFromOrigin is requested, this handler catches every resolveRemoteTrackingCommit failure and creates the worktree from the local branch instead. That fallback is appropriate when the remote-tracking ref is absent, but if the lookup fails for another reason after fetchRemote succeeds (for example a transient Git/config/refs error), the thread silently starts from a potentially stale local base despite the user asking to start from origin.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 770796f4: only a specifically classified missing remote ref falls back locally; unexpected lookup failures propagate, with regression coverage.
There was a problem hiding this comment.
Effect service conventions: one issue found. See inline comment.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
Effect Service Conventions: one finding on the new resolveRemoteTrackingCommit failure handling. See inline comment.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ffbe06f2e
ℹ️ 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".
| isServerThread && | ||
| activeThread && | ||
| activeThread.messages.length === 0 && | ||
| activeThread.worktreePath === null && |
There was a problem hiding this comment.
Allow empty worktree threads to change workspace
Once an existing-worktree selection for an empty server thread round-trips, activeThread.worktreePath becomes non-null while the thread still has no messages/session. This condition then disables the override path, and BranchToolbar also treats the persisted worktree path as locked, so a user who picked the wrong existing worktree cannot switch back to Main checkout or another worktree before the first send without abandoning the thread.
Useful? React with 👍 / 👎.
| effectiveEnvModeOverride: envMode, | ||
| activeWorktreePathOverride: activeWorktreePath, | ||
| activeThreadBranchOverride: activeThreadBranch, | ||
| onActiveThreadBranchOverrideChange: setPendingServerThreadBranch, |
There was a problem hiding this comment.
Track pending worktree paths from branch picks
For an empty server thread, choosing a branch that is already checked out in another worktree through the branch dropdown updates server metadata asynchronously with that worktree path, but the parent only records the pending branch override here. If the user presses Enter before the metadata update round-trips, activeWorktreePath is still null, so the send path treats the thread as the main checkout instead of the selected existing worktree.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| function looksLikeWindowsPath(path: string): boolean { | ||
| return /^[a-zA-Z]:\//.test(path); |
There was a problem hiding this comment.
Normalize UNC workspace paths case-insensitively
Fresh evidence beyond the sidebar-path finding: the branch toolbar uses this separate normalizer, and it only lowercases drive-letter Windows paths. In Windows-backed environments where the same UNC checkout is reported as \\Server\Share\Repo by thread metadata and //server/share/repo by Git, workspacePathsEqual returns false, so the workspace picker can fail to recognize the selected main/existing checkout and show duplicate or wrong workspace choices.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3f42da8. Configure here.
| new Promise<undefined>((resolve) => { | ||
| timeout = setTimeout(resolve, MAIN_CHECKOUT_RESOLUTION_TIMEOUT_MS); | ||
| }), | ||
| ])) ?? |
There was a problem hiding this comment.
Late race rejection unhandled
Low Severity
After the 500ms timeout wins Promise.race in resolveMainCheckout, a later rejection from resolveDefaultMainCheckout is no longer observed by the surrounding try/catch, so it can surface as an unhandled promise rejection instead of falling back to defaultMainCheckout.
Reviewed by Cursor Bugbot for commit 3f42da8. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f42da85e8
ℹ️ 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".
| loadRefs: async () => { | ||
| const result = await listRefs({ | ||
| environmentId: projectRef.environmentId, | ||
| input: { cwd: project.workspaceRoot, limit: 100 }, |
There was a problem hiding this comment.
Resolve all ref pages before seeding main checkout
When the selected project is a linked checkout and its real main checkout is on a non-default branch that falls outside the first 100 refs, this resolver never fetches the page containing the ref whose worktreePath matches mainCheckoutPath. resolveMainCheckoutTarget then falls back to branch: "HEAD" for that main checkout, so new local/default drafts are created with bogus branch metadata while running in the actual checkout; later sidebar/status data and new-worktree bases can point at HEAD instead of the checked-out branch. Load all ref pages here before resolving the main checkout, like the toolbar path does.
Useful? React with 👍 / 👎.
| await context.handleNewThread(projectRef, { | ||
| branch: mainCheckout?.branch ?? null, | ||
| worktreePath: threadEnvMode === "local" ? (mainCheckout?.path ?? null) : null, | ||
| envMode: threadEnvMode, |
There was a problem hiding this comment.
Preserve defaults when reusing grouped-project drafts
When this path starts a draft for a selected grouped project, these options are lost if useNewThreadHandler reuses an existing stored draft for the same logical repo but remaps it to a different member project/environment: that branch calls setDraftThreadContext first, then setLogicalProjectDraftThreadId(..., { threadId }), and the remap clears branch/worktree/env on project changes. In that scenario, clicking New thread for project B while an unused draft from project A exists silently drops the target main-checkout/default-mode values and creates a local/null draft for B instead; forward the same options through the remap call as the active-draft path does.
Useful? React with 👍 / 👎.
| { key: "mod+0", command: "preview.resetZoom", when: "previewFocus" }, | ||
| { key: "mod+k", command: "commandPalette.toggle", when: "!terminalFocus" }, | ||
| { key: "mod+n", command: "chat.new", when: "!terminalFocus" }, | ||
| { key: "mod+t", command: "chat.newSameWorktree", when: "!terminalFocus" }, |
There was a problem hiding this comment.
Avoid binding same-worktree to New Tab
In the browser build, mod+t is the standard new-tab shortcut (Cmd+T/Ctrl+T), but this default binding now resolves to chat.newSameWorktree; ChatRouteGlobalShortcuts then calls preventDefault() for that command in chat views whenever the terminal is not focused. As a result, users typing in the composer or navigating the chat cannot open a browser tab with the normal shortcut and instead create a T3 thread, so choose a non-reserved default or gate this binding to the desktop app.
Useful? React with 👍 / 👎.


Solves #3697
Verification
Screen.Recording.2026-07-11.at.5.22.24.PM.mov
Summary
Why
Thread creation previously conflated the registered project checkout, the true main checkout, and the active worktree. That made new drafts inherit whichever worktree happened to be active, produced misleading picker selections, and could make Cmd+N silently do nothing while resolving the main checkout.
The new flow resolves the primary checkout from Git worktree/ref data, keeps Cmd+N and Cmd+T semantics separate, and ensures checkout discovery failures cannot swallow thread creation.
User impact
Users can keep related threads grouped by worktree, deliberately create a thread in an existing worktree, and rely on Cmd+N to honor the configured Main checkout/New worktree default. Cmd+T always retains the active worktree.
Validation
vp checkvp run typecheckNote
Group sidebar threads by worktree and add
chat.newSameWorktreeshortcutsidebarThreadGroupingModeclient setting ('worktree'default,'separate'legacy) that groups sidebar threads under labeled headers per worktree path, including a 'Main checkout' label for the primary checkout.groupSidebarThreadsByWorktreeandorderSidebarThreadsByWorktreein Sidebar.logic.ts with cross-platform path normalization (case-insensitive on Windows/UNC paths).BranchToolbarEnvModeSelectorandMobileRunContextSelectorto display and select main checkout and existing worktrees with encodedmain:<path>/existing:<path>values.deriveWorkspaceOptionsandresolveWorkspaceSelectionin BranchToolbar.logic.ts to build deduplicated worktree option lists and resolve selection labels.startNewThreadInSameWorktreeFromContextand wires a newmod+tkeybinding (chat.newSameWorktree) to start a thread in the currently active worktree.resolveRemoteTrackingCommitnow returns a structuredRemoteTrackingRefNotFoundError; the server bootstrap falls back to the local base ref instead of failing when the remote tracking branch is missing.sidebarThreadGroupingModedefaults to'worktree', so existing users will see threads grouped by worktree immediately after update.Macroscope summarized 3f42da8.
Note
Medium Risk
Touches thread bootstrap, worktree metadata, and default new-thread semantics across web/desktop; behavior changes on upgrade (default sidebar grouping) and git edge cases need regression testing.
Overview
Adds worktree-aware workspace UX across the sidebar, branch toolbar, and new-thread flows, with a server-side bootstrap fix when remote bases are missing.
Sidebar & settings: New
sidebarThreadGroupingModeclient setting defaults to Group by worktree; threads render under checkout/worktree headers (with Keep separate for the old flat list). Labels distinguish main checkout vs linked worktrees usingmainCheckoutPathfromlistRefs.Workspace picker: Branch toolbar workspace controls now offer Main checkout, New worktree, and existing worktrees (with path normalization and fallbacks when refs are partial). Chat can apply pending worktree/branch overrides before the first message on empty server threads.
Thread creation: Cmd+N (
chat.new) uses per-project default env mode and resolved main-checkout branch/path instead of inheriting the active worktree. Cmd+T (chat.newSameWorktree) keeps the prior “same checkout” behavior. Cmd+Shift+N / local flows target the main checkout explicitly.Git/server:
resolveRemoteTrackingCommitsurfacesRemoteTrackingRefNotFoundError; thread bootstrap withstartFromOriginlogs and falls back to the local base branch instead of failing whenorigin/<branch>is absent.Reviewed by Cursor Bugbot for commit 3f42da8. Bugbot is set up for automated code reviews on this repo. Configure here.