diff --git a/apps/web/src/components/BranchToolbar.logic.test.ts b/apps/web/src/components/BranchToolbar.logic.test.ts index d1ca6d01e4c34..7e7e225e67c7f 100644 --- a/apps/web/src/components/BranchToolbar.logic.test.ts +++ b/apps/web/src/components/BranchToolbar.logic.test.ts @@ -8,6 +8,7 @@ import { resolveEffectiveEnvMode, resolveBranchToolbarPrBranch, resolveLocalCheckoutBranchMismatch, + resolveWorktreeBaseBranchCandidate, sanitizeNewRefName, shouldIncludeBranchPickerItem, } from "./BranchToolbar.logic"; @@ -254,3 +255,53 @@ describe("sanitizeNewRefName", () => { expect(sanitizeNewRefName("foo--bar")).toBe("foo--bar"); }); }); + +describe("resolveWorktreeBaseBranchCandidate", () => { + const ref = (name: string, overrides: Partial = {}): VcsRef => ({ + name, + current: false, + isDefault: false, + worktreePath: null, + ...overrides, + }); + + it("waits for the initial ref list", () => { + expect( + resolveWorktreeBaseBranchCandidate({ + isInitialBranchesLoadPending: true, + refs: [], + currentGitBranch: "staging", + }), + ).toBeNull(); + }); + + it("prefers the default branch", () => { + expect( + resolveWorktreeBaseBranchCandidate({ + isInitialBranchesLoadPending: false, + refs: [ref("feature", { current: true }), ref("staging", { isDefault: true })], + currentGitBranch: "feature", + }), + ).toBe("staging"); + }); + + it("falls back to the current branch when it is a listed ref", () => { + expect( + resolveWorktreeBaseBranchCandidate({ + isInitialBranchesLoadPending: false, + refs: [ref("develop", { current: true })], + currentGitBranch: "develop", + }), + ).toBe("develop"); + }); + + it("ignores an unborn HEAD while a clone is still running", () => { + expect( + resolveWorktreeBaseBranchCandidate({ + isInitialBranchesLoadPending: false, + refs: [], + currentGitBranch: "main", + }), + ).toBeNull(); + }); +}); diff --git a/apps/web/src/components/BranchToolbar.logic.ts b/apps/web/src/components/BranchToolbar.logic.ts index 01b9155617a44..560be9fe4b9ed 100644 --- a/apps/web/src/components/BranchToolbar.logic.ts +++ b/apps/web/src/components/BranchToolbar.logic.ts @@ -200,6 +200,23 @@ export function resolveBranchToolbarValue(input: { return currentGitBranch ?? activeThreadBranch; } +// A repository whose HEAD is unborn (e.g. while a clone is still running) reports its +// init.defaultBranch as the current branch even though no such ref exists yet. +export function resolveWorktreeBaseBranchCandidate(input: { + isInitialBranchesLoadPending: boolean; + refs: ReadonlyArray; + currentGitBranch: string | null; +}): string | null { + const { isInitialBranchesLoadPending, refs, currentGitBranch } = input; + if (isInitialBranchesLoadPending) return null; + const defaultBranchName = refs.find((refName) => refName.isDefault)?.name ?? null; + if (defaultBranchName) return defaultBranchName; + if (currentGitBranch && refs.some((refName) => refName.name === currentGitBranch)) { + return currentGitBranch; + } + return null; +} + export function resolveBranchTriggerLabel(input: { activeWorktreePath: string | null; effectiveEnvMode: EnvMode; diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 60eef31b1b63d..19328d3967fd6 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -53,6 +53,7 @@ import { resolveBranchToolbarValue, resolveDraftEnvModeAfterBranchChange, resolveEffectiveEnvMode, + resolveWorktreeBaseBranchCandidate, sanitizeNewRefName, shouldIncludeBranchPickerItem, } from "./BranchToolbar.logic"; @@ -252,7 +253,6 @@ export function BranchToolbarBranchSelector({ const createBranchItemValue = canCreateBranch ? `__create_new_branch__:${trimmedBranchQuery}` : null; - const defaultBranchName = refs.find((refName) => refName.isDefault)?.name ?? null; const pickerCreateBranchItemValue = hasExactBranchMatch ? null : createBranchItemValue; const branchPickerItems = [ ...(checkoutPullRequestItemValue ? [checkoutPullRequestItemValue] : []), @@ -478,9 +478,11 @@ export function BranchToolbarBranchSelector({ }); }; - const worktreeBaseBranchCandidate = isInitialBranchesLoadPending - ? null - : (defaultBranchName ?? currentGitBranch); + const worktreeBaseBranchCandidate = resolveWorktreeBaseBranchCandidate({ + isInitialBranchesLoadPending, + refs, + currentGitBranch, + }); // The server sets started threads' branches; defaulting here would detach the agent from its worktree. const threadStarted = serverSession !== null;