diff --git a/apps/web/src/components/BranchToolbar.logic.test.ts b/apps/web/src/components/BranchToolbar.logic.test.ts index 492f080beaaa29e2ec7f5dbbbd601286ea4ded29..e940fe2c3c597f8d490e64d422e0c4335a7bf3ac 100644 --- a/apps/web/src/components/BranchToolbar.logic.test.ts +++ b/apps/web/src/components/BranchToolbar.logic.test.ts @@ -4,6 +4,7 @@ import { filterBranchPickerItems, dedupeRemoteBranchesWithLocalMatches, deriveLocalBranchNameFromRemoteRef, + resolveBranchSelectionTarget, resolveDraftEnvModeAfterBranchChange, resolveBranchToolbarValue, } from "./BranchToolbar.logic"; @@ -223,3 +224,73 @@ describe("filterBranchPickerItems", () => { ).toEqual(["feature/488", "__create_new_branch__:48"]); }); }); + +describe("resolveBranchSelectionTarget", () => { + it("reuses an existing secondary worktree for the selected branch", () => { + expect( + resolveBranchSelectionTarget({ + activeProjectCwd: "/repo", + activeWorktreePath: "/repo/.t3/worktrees/feature-a", + branch: { + isDefault: false, + worktreePath: "/repo/.t3/worktrees/feature-b", + }, + }), + ).toEqual({ + checkoutCwd: "/repo/.t3/worktrees/feature-b", + nextWorktreePath: "/repo/.t3/worktrees/feature-b", + reuseExistingWorktree: true, + }); + }); + + it("switches back to the main repo when the branch already lives there", () => { + expect( + resolveBranchSelectionTarget({ + activeProjectCwd: "/repo", + activeWorktreePath: "/repo/.t3/worktrees/feature-a", + branch: { + isDefault: true, + worktreePath: "/repo", + }, + }), + ).toEqual({ + checkoutCwd: "/repo", + nextWorktreePath: null, + reuseExistingWorktree: true, + }); + }); + + it("checks out the default branch in the main repo when leaving a secondary worktree", () => { + expect( + resolveBranchSelectionTarget({ + activeProjectCwd: "/repo", + activeWorktreePath: "/repo/.t3/worktrees/feature-a", + branch: { + isDefault: true, + worktreePath: null, + }, + }), + ).toEqual({ + checkoutCwd: "/repo", + nextWorktreePath: null, + reuseExistingWorktree: false, + }); + }); + + it("keeps checkout in the current worktree for non-default branches", () => { + expect( + resolveBranchSelectionTarget({ + activeProjectCwd: "/repo", + activeWorktreePath: "/repo/.t3/worktrees/feature-a", + branch: { + isDefault: false, + worktreePath: null, + }, + }), + ).toEqual({ + checkoutCwd: "/repo/.t3/worktrees/feature-a", + nextWorktreePath: "/repo/.t3/worktrees/feature-a", + reuseExistingWorktree: false, + }); + }); +}); diff --git a/apps/web/src/components/BranchToolbar.logic.ts b/apps/web/src/components/BranchToolbar.logic.ts index 3db270eeb2852354f38924e5cbad963d36b659ee..79acb487c27517d680e34d84d332fbfa3a7cad1d 100644 --- a/apps/web/src/components/BranchToolbar.logic.ts +++ b/apps/web/src/components/BranchToolbar.logic.ts @@ -112,3 +112,32 @@ export function filterBranchPickerItems(input: { return itemValue.toLowerCase().includes(normalizedQuery); }); } + +export function resolveBranchSelectionTarget(input: { + activeProjectCwd: string; + activeWorktreePath: string | null; + branch: Pick; +}): { + checkoutCwd: string; + nextWorktreePath: string | null; + reuseExistingWorktree: boolean; +} { + const { activeProjectCwd, activeWorktreePath, branch } = input; + + if (branch.worktreePath) { + return { + checkoutCwd: branch.worktreePath, + nextWorktreePath: branch.worktreePath === activeProjectCwd ? null : branch.worktreePath, + reuseExistingWorktree: true, + }; + } + + const nextWorktreePath = + activeWorktreePath !== null && branch.isDefault ? null : activeWorktreePath; + + return { + checkoutCwd: nextWorktreePath ?? activeProjectCwd, + nextWorktreePath, + reuseExistingWorktree: false, + }; +} diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 3869ec234a4c49b8cc6ff5c2a178b88ecb6d2120..766b284816df429dc0c46c47fd584af6f38bb206 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -27,6 +27,7 @@ import { deriveLocalBranchNameFromRemoteRef, EnvMode, filterBranchPickerItems, + resolveBranchSelectionTarget, resolveBranchToolbarValue, } from "./BranchToolbar.logic"; import { Button } from "./ui/button"; @@ -175,10 +176,15 @@ export function BranchToolbarBranchSelector({ return; } + const selectionTarget = resolveBranchSelectionTarget({ + activeProjectCwd, + activeWorktreePath, + branch, + }); + // If the branch already lives in a worktree, point the thread there. - if (branch.worktreePath) { - const isMainWorktree = branch.worktreePath === activeProjectCwd; - onSetThreadBranch(branch.name, isMainWorktree ? null : branch.worktreePath); + if (selectionTarget.reuseExistingWorktree) { + onSetThreadBranch(branch.name, selectionTarget.nextWorktreePath); setIsBranchMenuOpen(false); onComposerFocusRequest?.(); return; @@ -194,7 +200,7 @@ export function BranchToolbarBranchSelector({ runBranchAction(async () => { setOptimisticBranch(selectedBranchName); try { - await api.git.checkout({ cwd: branchCwd, branch: branch.name }); + await api.git.checkout({ cwd: selectionTarget.checkoutCwd, branch: branch.name }); await invalidateGitQueries(queryClient); } catch (error) { toastManager.add({ @@ -214,7 +220,7 @@ export function BranchToolbarBranchSelector({ } setOptimisticBranch(nextBranchName); - onSetThreadBranch(nextBranchName, activeWorktreePath); + onSetThreadBranch(nextBranchName, selectionTarget.nextWorktreePath); }); };