Skip to content

Group threads by worktree#3898

Open
jakeleventhal wants to merge 33 commits into
pingdotgg:mainfrom
jakeleventhal:t3code/group-threads-worktrees
Open

Group threads by worktree#3898
jakeleventhal wants to merge 33 commits into
pingdotgg:mainfrom
jakeleventhal:t3code/group-threads-worktrees

Conversation

@jakeleventhal

@jakeleventhal jakeleventhal commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Solves #3697

Reasoning/Justification

https://cap.so/s/dvaz19nmrwjh2ja

Verification

Screen.Recording.2026-07-11.at.5.22.24.PM.mov

Summary

  • add a Group threads setting with Group by worktree as the default and Keep separate for the previous behavior
  • add workspace-aware thread creation with Main checkout, New worktree, and existing-worktree choices
  • make Cmd+N follow the configured default and add Cmd+T for creating a thread in the active worktree
  • group sidebar threads under their checkout/worktree and keep labels consistent across the picker and sidebar
  • fall back to a local base when a branch has no remote-tracking commit

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 check
  • vp run typecheck
  • focused web tests: 83 passed
  • desktop dev-app smoke testing for grouping, workspace selection, worktree creation, and provider submission

Note

Group sidebar threads by worktree with a new mod+t same-worktree shortcut

  • Adds a sidebarThreadGroupingMode client setting ('worktree' default, 'separate' opt-out) that groups sidebar threads under labeled worktree headers, with thread counts and dynamic workspace identity resolution from VCS refs.
  • Introduces groupSidebarThreadsByWorktree and orderSidebarThreadsByWorktree in Sidebar.logic.ts to compute groups; handles Windows path case-insensitivity via normalizeWorkspacePath.
  • Adds deriveWorkspaceOptions, resolveWorkspaceSelection, and related helpers in BranchToolbar.logic.ts; the env-mode selector and mobile selector now list Main checkout and existing worktrees as discrete choices.
  • Adds a chat.newSameWorktree command (default keybinding mod+t) via startNewThreadInSameWorktreeFromContext; new-thread creation is centralized through startNewThreadInProjectFromContext with per-project env-mode and start-from-origin defaults.
  • Adds mainCheckoutPath to VcsListRefsResult and a RemoteTrackingRefNotFoundError to GitVcsDriver; when starting from origin with a missing remote tracking branch, the server now falls back to the local base branch instead of failing.
  • Behavioral Change: resolveEnvModeLabel now returns 'Main checkout' instead of 'Local'; resolveSidebarNewThreadSeedContext is removed and callers must use the new handler pipeline.

Macroscope summarized 4ade26b.


Note

Medium Risk
Touches git bootstrap, worktree metadata, and default sidebar grouping behavior; incorrect checkout resolution could start threads in the wrong path, though fallbacks and tests limit blast radius.

Overview
Adds worktree-aware workspaces end to end: Git ref listing now exposes mainCheckoutPath, the branch toolbar can pick Main checkout, New worktree, or an existing worktree, and thread creation separates Cmd+N (project default mode + resolved main checkout) from Cmd+T (chat.newSameWorktree, same checkout as the active thread).

The sidebar gains a Group threads client setting (worktree default, separate for the old flat list). In worktree mode, threads render under checkout labels using VCS ref data and normalized paths (including Windows case rules).

Bootstrap worktree prep no longer hard-fails when startFromOrigin cannot resolve a remote-tracking branch: missing refs yield RemoteTrackingRefNotFoundError and the server logs and falls back to the local base branch; other git errors still abort.

Copy and settings rename Local / Current checkout to Main checkout where relevant; pending worktree selection is honored before server metadata round-trips on empty server threads.

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

@coderabbitai

coderabbitai Bot commented Jul 11, 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

Run ID: 70b063f0-3465-41c1-b935-3b169a667f7c

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 11, 2026
Comment thread apps/web/src/components/BranchToolbar.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/BranchToolbar.logic.ts Outdated
Comment thread apps/web/src/lib/chatThreadActions.ts
Comment thread apps/web/src/hooks/useHandleNewThread.ts Outdated
@jakeleventhal
jakeleventhal marked this pull request as ready for review July 11, 2026 22:10
Comment thread apps/web/src/hooks/useHandleNewThread.ts
Comment thread apps/web/src/hooks/useHandleNewThread.ts Outdated
Comment thread apps/web/src/components/BranchToolbar.logic.ts Outdated
Comment thread apps/web/src/components/BranchToolbarEnvModeSelector.tsx Outdated
Comment thread apps/web/src/components/Sidebar.logic.ts
@macroscopeapp

macroscopeapp Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces significant new sidebar grouping and workspace selection features with substantial cross-cutting changes. Multiple unresolved review comments identify high/medium severity issues including race conditions, pagination bugs, and path normalization problems that could cause threads to execute in the wrong workspace.

You can customize Macroscope's approvability policy. Learn more.

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

Comment on lines +293 to +294
return (
serverConfigs.get(newThreadProjectRef.environmentId)?.settings ?? DEFAULT_SERVER_SETTINGS

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

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

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

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.

Regression coverage added in fd4f7526 to ensure failed target discovery never reuses another project's checkout.

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment on lines +984 to +985
const workspaceEnvironmentId = renderedThreads[0]?.environmentId ?? null;
const workspaceProjectId = renderedThreads[0]?.projectId ?? 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 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 👍 / 👎.

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.

Regression coverage added in 0e60ec29 for grouped rows containing multiple project checkouts.

? renderedThreads.map(renderThread)
: null}
{shouldShowThreadPanel && threadGroupingMode === "worktree"
? renderedThreadGroups.flatMap((group) => [

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

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 b4fa9a38: navigation, jump labels, and range selection now use grouped visual order.

Comment on lines +253 to +256
const defaultRef = activeProjectBranches.data.refs.find(
(ref) => !ref.isRemote && ref.isDefault,
);
return defaultRef ? { branch: defaultRef.name, path: null } : undefined;

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

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 69592d82: local drafts are seeded from the branch actually checked out in the project checkout.

Comment on lines +4877 to +4879
setPendingServerThreadEnvMode("worktree");
setPendingServerThreadBranch(worktree.branch);
void updateThreadMetadata({

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

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 9145cbb1: pending existing-worktree selections are honored immediately by the send path.

projectRef.environmentId === newThreadProjectRef.environmentId &&
projectRef.projectId === newThreadProjectRef.projectId
) {
return activeProjectMainCheckout;

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

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.

Regression coverage added in 67955356 for starting a draft while active ref discovery is still pending.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread apps/web/src/components/BranchToolbar.logic.ts Outdated
Comment thread apps/web/src/components/BranchToolbar.tsx Outdated
Comment thread apps/web/src/components/BranchToolbar.tsx
Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/BranchToolbar.tsx 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 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".

Comment on lines +268 to +271
const activeWorktreePath =
activeWorktreePathOverride !== undefined
? activeWorktreePathOverride
: (serverThread?.worktreePath ?? draftThread?.worktreePath ?? 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 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 👍 / 👎.

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 5e3872d3: the branch selector now uses the pending worktree path immediately.

Comment on lines +35 to +36
function normalizeWorkspacePath(path: string): string {
return path.trim().replaceAll("\\", "/").replace(/\/+$/, "");

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

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 8b6e2257: Windows drive and UNC workspace keys are normalized case-insensitively, with regression coverage.

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment on lines +991 to +997
threadGroupingMode === "worktree"
? projectMembers.map((member) => ({
member,
atom: vcsEnvironment.listRefs({
environmentId: member.environmentId,
input: { cwd: member.workspaceRoot, limit: 100 },
}),

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

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 9d2ed11c: hidden project rows no longer subscribe to ref scans.

Comment on lines +288 to +291
const options = deriveWorkspaceOptions(
branchState.data?.refs ?? [],
activeProject.workspaceRoot,
branchState.data?.mainCheckoutPath,

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

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 9644cd87: the workspace picker now loads all ref pages before deriving its complete worktree list.

Comment thread apps/server/src/ws.ts Outdated
})
.pipe(
Effect.matchEffect({
onFailure: (error) =>

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

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 770796f4: only a specifically classified missing remote ref falls back locally; unexpected lookup failures propagate, with regression coverage.

Comment thread apps/web/src/state/queries.ts
Comment thread apps/web/src/state/queries.ts
Comment thread apps/web/src/state/queries.ts

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

Effect service conventions: one issue found. See inline comment.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/vcs/GitVcsDriver.ts Outdated

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

Effect Service Conventions: one finding on the new resolveRemoteTrackingCommit failure handling. See inline comment.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/vcs/GitVcsDriver.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: 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 &&

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

Comment thread apps/web/src/components/ChatView.tsx Outdated
effectiveEnvModeOverride: envMode,
activeWorktreePathOverride: activeWorktreePath,
activeThreadBranchOverride: activeThreadBranch,
onActiveThreadBranchOverrideChange: setPendingServerThreadBranch,

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

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

new Promise<undefined>((resolve) => {
timeout = setTimeout(resolve, MAIN_CHECKOUT_RESOLUTION_TIMEOUT_MS);
}),
])) ??

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3f42da8. Configure here.

@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: 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 },

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

Comment on lines +152 to +155
await context.handleNewThread(projectRef, {
branch: mainCheckout?.branch ?? null,
worktreePath: threadEnvMode === "local" ? (mainCheckout?.path ?? null) : null,
envMode: threadEnvMode,

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 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" },

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

Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/server/src/vcs/GitVcsDriverCore.ts
Comment thread apps/web/src/components/Sidebar.tsx
Comment thread apps/web/src/hooks/useHandleNewThread.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

await handleNewThread(scopeProjectRef(project.environmentId, project.id));

P2 Badge Apply project defaults when opening empty projects

When the project-search action falls through to creating a draft because the selected project has no latest thread, this still calls the raw handleNewThread instead of startNewThreadInProjectFromContext. That bypasses the newly added target defaults/main-checkout resolver used by the new-thread-in action below, so opening an empty project from the palette can create a draft with branch/worktreePath left null; in environments defaulting to New worktree the first send is blocked for a missing base branch, and linked-checkout projects defaulting to Main checkout won't switch to the actual main checkout.

ℹ️ 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 +111 to +115
: refs.find(
(ref) =>
ref.isDefault &&
ref.worktreePath !== null &&
!workspacePathsEqual(ref.worktreePath, projectWorkspaceRoot),

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 Require an explicit main checkout path

When mainCheckoutPath is unavailable, for example from an older environment server that still returns per-ref worktreePath, this promotes whichever default-branch worktree is not the registered project to “Main checkout”. In a repo where main is checked out in a secondary worktree while the actual primary checkout is elsewhere or on another branch, the workspace picker and local-draft seeding will use that secondary worktree path/branch, so the agent can run in the wrong checkout.

Useful? React with 👍 / 👎.

Comment on lines +86 to +90
(await Promise.race([
context.resolveDefaultMainCheckout(projectRef),
new Promise<undefined>((resolve) => {
timeout = setTimeout(resolve, MAIN_CHECKOUT_RESOLUTION_TIMEOUT_MS);
}),

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 Wait for checkout discovery before falling back

Fresh evidence beyond the loading-window finding: this races main-checkout discovery against a fixed 500 ms timer, and the production callers do not provide defaultMainCheckout. In remote environments or large repos where the first listRefs call takes just over 500 ms, local/default thread creation falls back to null branch/path even though discovery would have resolved shortly after, so linked-checkout drafts run in the registered checkout instead of the main checkout and default worktree drafts can still be created without a base branch.

Useful? React with 👍 / 👎.

jakeleventhal and others added 26 commits July 18, 2026 10:02
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>
@jakeleventhal
jakeleventhal force-pushed the t3code/group-threads-worktrees branch from 98ba360 to 4ade26b Compare July 18, 2026 14:17
const options = data
? deriveWorkspaceOptions(data.refs, member.workspaceRoot, data.mainCheckoutPath)
: null;
const normalizedProjectCwd = member.workspaceRoot.replaceAll("\\", "/").replace(/\/+$/, "");

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 components/Sidebar.tsx:1022

projectCheckoutRef compares normalized paths case-sensitively, so on Windows a ref whose worktreePath differs only in drive or directory casing from member.workspaceRoot (e.g. C:\Repo vs c:\repo) is never matched. The project-checkout group then falls back to the thread branch or "Project checkout" instead of the actual ref name. The groupSidebarThreadsByWorktree logic already uses normalizeWorkspacePath for case-insensitive comparison — use the same helper here instead of the inline replaceAll/replace normalization.

Also found in 1 other location(s)

apps/web/src/components/BranchToolbar.logic.ts:42

workspacePathKey only recognizes drive-letter Windows paths, so normalized UNC paths such as //server/share/Repo are compared case-sensitively. Windows/WSL UNC paths can arrive with casing differences (for example the project root versus Git ref metadata), causing the same checkout to be treated as distinct: the picker can add duplicate fallback worktrees, fail to select the active checkout, or resolve the wrong Main checkout state. Detect normalized paths beginning with // as Windows paths too.

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

`projectCheckoutRef` compares normalized paths case-sensitively, so on Windows a ref whose `worktreePath` differs only in drive or directory casing from `member.workspaceRoot` (e.g. `C:\Repo` vs `c:\repo`) is never matched. The project-checkout group then falls back to the thread branch or `"Project checkout"` instead of the actual ref name. The `groupSidebarThreadsByWorktree` logic already uses `normalizeWorkspacePath` for case-insensitive comparison — use the same helper here instead of the inline `replaceAll`/`replace` normalization.

Also found in 1 other location(s):
- apps/web/src/components/BranchToolbar.logic.ts:42 -- `workspacePathKey` only recognizes drive-letter Windows paths, so normalized UNC paths such as `//server/share/Repo` are compared case-sensitively. Windows/WSL UNC paths can arrive with casing differences (for example the project root versus Git ref metadata), causing the same checkout to be treated as distinct: the picker can add duplicate fallback worktrees, fail to select the active checkout, or resolve the wrong Main checkout state. Detect normalized paths beginning with `//` as Windows paths too.

@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 3 potential issues.

There are 4 total unresolved issues (including 1 from previous review).

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 4ade26b. Configure here.

branch: worktree.branch,
worktreePath: worktree.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.

Workspace metadata races picker

High Severity

Selecting an existing worktree on an empty server thread immediately calls updateThreadMetadata. This prematurely updates the thread's worktreePath, causing canOverrideServerThreadEnvMode to become false. As a result, pending environment overrides are cleared, the workspace picker locks, and the first turn may execute in the project checkout instead of the selected worktree.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4ade26b. Configure here.

atom: vcsEnvironment.listRefs({
environmentId: member.environmentId,
input: { cwd: member.workspaceRoot, limit: 100 },
}),

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.

Sidebar refs stop at page

Medium Severity

The sidebar's worktree grouping and new thread main-checkout resolution (via Cmd+N) use a single listRefs call with a 100-ref limit. This can lead to incomplete worktree identities, mislabeled groups, and incorrect default branches for new threads in large repositories, where other UI components correctly paginate all refs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4ade26b. Configure here.

const normalized = path.trim().replaceAll("\\", "/").replace(/\/+$/, "");
return /^[A-Za-z]:\//.test(normalized) || normalized.startsWith("//")
? normalized.toLowerCase()
: normalized;

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.

Duplicate path normalization logic

Low Severity

Worktree path normalization for grouping lives in a private normalizeWorkspacePath in Sidebar.logic.ts, while the workspace picker uses a separate normalizeWorkspacePath and workspacePathsEqual in BranchToolbar.logic.ts. The sidebar version lowercases UNC (//) paths; the toolbar version only lowercases drive-letter paths, so the same checkout can be treated as different workspaces in the sidebar versus the picker on UNC paths.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4ade26b. Configure here.

@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: 4ade26bc2b

ℹ️ 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 +87 to +88
if (mainCheckout && value === `main:${mainCheckout.path}`) {
onExistingWorktreeChange(mainCheckout);

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 Keep Main checkout distinct from worktrees

When the selected project is a linked checkout and the real main checkout is currently on a non-default branch, routing the Main checkout option through the existing-worktree handler records only activeWorktreePath and loses that it is the main checkout. If the user then chooses the default branch in the branch picker, resolveBranchSelectionTarget treats activeWorktreePath + refName.isDefault as a request to clear worktreePath, so the checkout/sent thread moves to the registered project checkout instead of switching the real main checkout. Preserve a distinct main-checkout selection state or pass that state into branch selection so default-branch changes still target the main checkout path.

Useful? React with 👍 / 👎.

Comment on lines +5051 to +5054
...(draftThread?.worktreePath
? mode === "local"
? { branch: null, worktreePath: null }
: { worktreePath: 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 checkout branch when returning to Main checkout

When a draft is currently pointed at an existing worktree, choosing Main checkout in a project without a separate mainCheckoutPath reaches this mode === "local" branch and clears both worktreePath and branch. The UI can still display the project checkout's current branch from git status, but the draft metadata remains null, so the first send creates the server thread with branch: null while running in the project checkout, breaking sidebar/PR status and future worktree bases for that thread.

Useful? React with 👍 / 👎.

return false;
}

await context.handleNewThread(projectRef, buildContextualThreadOptions(context));

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 Include pending workspace when duplicating a thread

When an empty server thread has just selected an existing worktree, ChatView only has that path in its pending local override until the metadata update round-trips; this global chat.newSameWorktree path still copies only the persisted activeThread.worktreePath from the shared context. Pressing the new same-worktree shortcut in that window therefore creates the duplicate draft with worktreePath: null/local checkout instead of the selected checkout, so pass the effective pending branch/worktree into this action or wait for metadata before duplicating.

Useful? React with 👍 / 👎.

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant