From f2593309b1563b9681a86e2fe521bc5209c0301f Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Tue, 3 Nov 2026 18:46:54 +0800 Subject: [PATCH 1/4] fix(web,server): picking the previous branch after an agent's checkout switches back Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 27 +++++++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 48 +++++++++++++++++-- .../BranchToolbarBranchSelector.tsx | 37 ++++++++++---- 3 files changed, 99 insertions(+), 13 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 53d031a7413e8..2515f29a7f105 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -1189,6 +1189,33 @@ it.effect("invalidates a ref snapshot when a mutation fails after changing Git", ), ); +it.effect("follows a checkout made outside the driver after refs were cached", () => + Effect.gen(function* () { + const driver = yield* GitVcsDriver.GitVcsDriver; + const cwd = yield* makeTmpDir(); + const { initialBranch } = yield* initRepoWithCommit(cwd); + yield* git(cwd, ["branch", "feature/agent"]); + yield* driver.listRefs({ cwd }); + + // A provider's own `git switch` bypasses the driver's cache invalidation. + yield* git(cwd, ["switch", "feature/agent"]); + const refs = yield* driver.listRefs({ cwd }); + const initialRef = refs.refs.find((ref) => ref.name === initialBranch); + assert.isNull(initialRef?.worktreePath); + assert.isFalse(initialRef?.current); + assert.isTrue(refs.refs.find((ref) => ref.name === "feature/agent")?.current); + + yield* driver.switchRef({ cwd, refName: initialBranch }); + assert.equal(yield* git(cwd, ["branch", "--show-current"]), initialBranch); + assert.isTrue( + (yield* driver.listRefs({ cwd })).refs.find((ref) => ref.name === initialBranch)?.current, + ); + + yield* git(cwd, ["switch", "--detach", "HEAD"]); + assert.isEmpty((yield* driver.listRefs({ cwd })).refs.filter((ref) => ref.current)); + }).pipe(Effect.provide(layerTest)), +); + it.effect("marks the current branch when worktree metadata is unavailable", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 4ace91f8017d2..8af08a5b6fe6e 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -4088,15 +4088,34 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* yield* Cache.invalidate(repositoryPathsCache, repositoryPathsCacheKey); }); + // Undefined when HEAD cannot be read; null when detached. + const readHeadBranch = (cwd: string) => + executeGit("GitVcsDriver.listRefs.headBranch", cwd, ["symbolic-ref", "--quiet", "HEAD"], { + timeoutMs: 5_000, + allowNonZeroExit: true, + }).pipe( + Effect.map((result) => + result.exitCode === 0 + ? branchNameFromHeadRef(result.stdout) + : result.exitCode === 1 + ? null + : undefined, + ), + Effect.orElseSucceed(() => undefined), + ); + const listRefs: GitVcsDriver.GitVcsDriver["Service"]["listRefs"] = Effect.fn("listRefs")( function* (input) { - const repositoryPaths = yield* resolveRepositoryPaths(input.cwd, input.refresh === true).pipe( + const cachedRepositoryPaths = yield* resolveRepositoryPaths( + input.cwd, + input.refresh === true, + ).pipe( Effect.catchTags({ GitCommandError: (error) => isMissingGitCwdError(error) ? Effect.succeed(null) : Effect.fail(error), }), ); - if (repositoryPaths === null) { + if (cachedRepositoryPaths === null) { return { refs: [], isRepo: false, @@ -4106,10 +4125,29 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }; } - const snapshot = yield* resolveListRefsSnapshot( - repositoryPaths.gitCommonDir, - input.refresh === true, + // Providers run checkouts that skip invalidation, so check the cached HEAD before + // the picker treats a branch as already checked out here. + const [cachedSnapshot, headBranch] = yield* Effect.all( + [ + resolveListRefsSnapshot(cachedRepositoryPaths.gitCommonDir, input.refresh === true), + readHeadBranch(input.cwd), + ], + { concurrency: 2 }, ); + const cachedHeadBranch = + cachedSnapshot.localBranches.find( + (ref) => ref.worktreePath === cachedRepositoryPaths.worktreeRoot, + )?.name ?? cachedRepositoryPaths.currentBranch; + const isCachedHeadStale = headBranch !== undefined && headBranch !== cachedHeadBranch; + if (isCachedHeadStale) { + yield* invalidateListRefsSnapshot(input.cwd, cachedRepositoryPaths.gitCommonDir); + } + const repositoryPaths = isCachedHeadStale + ? { ...cachedRepositoryPaths, currentBranch: headBranch } + : cachedRepositoryPaths; + const snapshot = isCachedHeadStale + ? yield* resolveListRefsSnapshot(repositoryPaths.gitCommonDir, false) + : cachedSnapshot; const hasCurrentWorktreeBranch = repositoryPaths.worktreeRoot !== null && snapshot.localBranches.some((ref) => ref.worktreePath === repositoryPaths.worktreeRoot); diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index fab250f6adaf8..c51fe17d8255a 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -125,6 +125,9 @@ export function BranchToolbarBranchSelector({ const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false, }); + const readRefs = useAtomCommand(vcsEnvironment.readRefs, { + reportFailure: false, + }); const refreshStatus = useAtomCommand(vcsEnvironment.refreshStatus, { reportFailure: false, }); @@ -377,16 +380,39 @@ export function BranchToolbarBranchSelector({ }); }; - const selectBranch = (refName: VcsRef) => { + // The picker's list can predate a checkout an agent ran itself, so confirm a worktree + // claim before skipping the switch. Unconfirmed claims fall back to a real checkout, + // which Git refuses if the branch is checked out elsewhere. + const readSelectedRef = async (cwd: string, listedRef: VcsRef): Promise => { + const result = await readRefs({ + environmentId, + input: { cwd, query: listedRef.name, limit: 100, refresh: true }, + }); + const freshRef = + result._tag === "Success" + ? result.value.refs.find( + (ref) => ref.name === listedRef.name && ref.isRemote === listedRef.isRemote, + ) + : undefined; + return freshRef ?? { ...listedRef, worktreePath: null }; + }; + + const selectBranch = async (listedRef: VcsRef) => { if (!canUpdateThreadBranch || !branchCwd || !activeProjectCwd || isBranchActionPending) return; if (isSelectingWorktreeBase) { - setThreadBranch(refName.name, null); + setThreadBranch(listedRef.name, null); setIsBranchMenuOpen(false); onComposerFocusRequest?.(); return; } + setIsBranchMenuOpen(false); + onComposerFocusRequest?.(); + + const refName = listedRef.worktreePath + ? await readSelectedRef(branchCwd, listedRef) + : listedRef; const selectionTarget = resolveBranchSelectionTarget({ activeProjectCwd, activeWorktreePath, @@ -395,8 +421,6 @@ export function BranchToolbarBranchSelector({ if (selectionTarget.reuseExistingWorktree) { setThreadBranch(refName.name, selectionTarget.nextWorktreePath); - setIsBranchMenuOpen(false); - onComposerFocusRequest?.(); return; } @@ -404,9 +428,6 @@ export function BranchToolbarBranchSelector({ ? deriveLocalBranchNameFromRemoteRef(refName.name) : refName.name; - setIsBranchMenuOpen(false); - onComposerFocusRequest?.(); - runBranchAction(async () => { const previousBranch = resolvedActiveBranch; setOptimisticBranch(selectedBranchName); @@ -575,7 +596,7 @@ export function BranchToolbarBranchSelector({ createRef(trimmedBranchQuery); } else { const refName = branchByName.get(itemValue); - if (refName) selectBranch(refName); + if (refName) void selectBranch(refName); } } From 2eb0b71bf3a15ff8a0ec13e1c498fc97b4bf8c4d Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Tue, 3 Nov 2026 19:22:24 +0800 Subject: [PATCH 2/4] fix(web,server): keep ref caching and worktree claims correct in edge cases listRefs no longer matches a null worktree root against branches without a worktree, which made every call reread refs when worktree metadata is missing. The picker keeps its worktree claim when its confirming read was truncated. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 14 +++++++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 8 ++++--- .../components/BranchToolbar.logic.test.ts | 24 +++++++++++++++++++ .../web/src/components/BranchToolbar.logic.ts | 14 +++++++++++ .../BranchToolbarBranchSelector.tsx | 9 ++----- 5 files changed, 59 insertions(+), 10 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 2515f29a7f105..1f515f0c9b382 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -1220,6 +1220,7 @@ it.effect("marks the current branch when worktree metadata is unavailable", () = Effect.scoped( Effect.gen(function* () { const delegate = yield* ChildProcessSpawner.ChildProcessSpawner; + let snapshotReads = 0; const incompleteMetadataSpawner = ChildProcessSpawner.make((command) => Effect.gen(function* () { if (!ChildProcess.isStandardCommand(command)) { @@ -1232,6 +1233,9 @@ it.effect("marks the current branch when worktree metadata is unavailable", () = if (isWorktreeRoot || isWorktreeList) { return makeNonRepositoryHandle(); } + if (command.args.includes("for-each-ref") && command.args.includes("refs/remotes")) { + snapshotReads++; + } return yield* delegate.spawn(command); }), ); @@ -1243,10 +1247,20 @@ it.effect("marks the current branch when worktree metadata is unavailable", () = Effect.provideService(GitVcsDriver.GitVcsDriver, driver), ); + // Sorts before the current branch, so a worktree lookup keyed on a null root would pick it. + yield* git(cwd, ["branch", "aaa-other"]).pipe( + Effect.provideService(GitVcsDriver.GitVcsDriver, driver), + ); + const refs = yield* driver.listRefs({ cwd, refresh: true }); assert.isTrue(refs.isRepo); assert.isTrue(refs.refs.find((ref) => ref.name === initialBranch)?.current); + + const readsAfterRefresh = snapshotReads; + yield* driver.listRefs({ cwd }); + yield* driver.listRefs({ cwd }); + assert.equal(snapshotReads, readsAfterRefresh); }), ).pipe( Effect.provide( diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 8af08a5b6fe6e..567f144f98d18 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -4134,10 +4134,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* ], { concurrency: 2 }, ); + const cachedWorktreeRoot = cachedRepositoryPaths.worktreeRoot; const cachedHeadBranch = - cachedSnapshot.localBranches.find( - (ref) => ref.worktreePath === cachedRepositoryPaths.worktreeRoot, - )?.name ?? cachedRepositoryPaths.currentBranch; + (cachedWorktreeRoot === null + ? undefined + : cachedSnapshot.localBranches.find((ref) => ref.worktreePath === cachedWorktreeRoot) + ?.name) ?? cachedRepositoryPaths.currentBranch; const isCachedHeadStale = headBranch !== undefined && headBranch !== cachedHeadBranch; if (isCachedHeadStale) { yield* invalidateListRefsSnapshot(input.cwd, cachedRepositoryPaths.gitCommonDir); diff --git a/apps/web/src/components/BranchToolbar.logic.test.ts b/apps/web/src/components/BranchToolbar.logic.test.ts index 08e6f7633dee3..cf3f3c3f27e97 100644 --- a/apps/web/src/components/BranchToolbar.logic.test.ts +++ b/apps/web/src/components/BranchToolbar.logic.test.ts @@ -7,6 +7,7 @@ import { resolveDraftEnvModeAfterBranchChange, resolveEffectiveEnvMode, resolveBranchToolbarPrBranch, + resolveConfirmedSelectedRef, resolveLocalCheckoutBranchMismatch, sanitizeNewRefName, shouldIncludeBranchPickerItem, @@ -254,3 +255,26 @@ describe("sanitizeNewRefName", () => { expect(sanitizeNewRefName("foo--bar")).toBe("foo--bar"); }); }); + +describe("resolveConfirmedSelectedRef", () => { + const listed: VcsRef = { + name: "main", + isRemote: false, + current: false, + isDefault: true, + worktreePath: "/repo", + }; + + it.each([ + ["a complete read without the ref drops the claim", { refs: [], nextCursor: null }, null], + ["a truncated read without the ref keeps the claim", { refs: [], nextCursor: 100 }, "/repo"], + ["a failed read drops the claim", null, null], + [ + "a read with the ref uses its fresh worktree", + { refs: [{ ...listed, worktreePath: "/other" }], nextCursor: 100 }, + "/other", + ], + ] as const)("%s", (_label, freshRead, expected) => { + expect(resolveConfirmedSelectedRef(listed, freshRead).worktreePath).toBe(expected); + }); +}); diff --git a/apps/web/src/components/BranchToolbar.logic.ts b/apps/web/src/components/BranchToolbar.logic.ts index e22dcfaddedeb..2640aea7e8e68 100644 --- a/apps/web/src/components/BranchToolbar.logic.ts +++ b/apps/web/src/components/BranchToolbar.logic.ts @@ -245,6 +245,20 @@ export function resolveLocalCheckoutBranchMismatch(input: { return { threadBranch: activeThreadBranch, currentBranch: currentGitBranch }; } +/** Confirms a picked ref's worktree claim against a fresh read, or `null` when the read failed. */ +export function resolveConfirmedSelectedRef( + listedRef: VcsRef, + freshRead: { readonly refs: ReadonlyArray; readonly nextCursor: number | null } | null, +): VcsRef { + if (freshRead === null) return { ...listedRef, worktreePath: null }; + const freshRef = freshRead.refs.find( + (ref) => ref.name === listedRef.name && ref.isRemote === listedRef.isRemote, + ); + if (freshRef) return freshRef; + // A truncated read can miss the ref without disproving its worktree, so keep the listed claim. + return freshRead.nextCursor === null ? { ...listedRef, worktreePath: null } : listedRef; +} + export function resolveBranchSelectionTarget(input: { activeProjectCwd: string; activeWorktreePath: string | null; diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index c51fe17d8255a..d0c75e9ce9c91 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -50,6 +50,7 @@ import { resolveBranchToolbarPrBranch, resolveBranchSelectionTarget, resolveBranchToolbarValue, + resolveConfirmedSelectedRef, resolveDraftEnvModeAfterBranchChange, resolveEffectiveEnvMode, sanitizeNewRefName, @@ -388,13 +389,7 @@ export function BranchToolbarBranchSelector({ environmentId, input: { cwd, query: listedRef.name, limit: 100, refresh: true }, }); - const freshRef = - result._tag === "Success" - ? result.value.refs.find( - (ref) => ref.name === listedRef.name && ref.isRemote === listedRef.isRemote, - ) - : undefined; - return freshRef ?? { ...listedRef, worktreePath: null }; + return resolveConfirmedSelectedRef(listedRef, result._tag === "Success" ? result.value : null); }; const selectBranch = async (listedRef: VcsRef) => { From 7fb8a04c663051275cdb193f1f0f23931e3a5b1a Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Tue, 3 Nov 2026 19:38:11 +0800 Subject: [PATCH 3/4] fix(web): the branch picker keeps the latest pick and confirms past one page Co-Authored-By: Claude Opus 5.5 --- .../BranchToolbarBranchSelector.tsx | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index d0c75e9ce9c91..65a03397cb7f4 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -21,6 +21,7 @@ import { useEffect, useImperativeHandle, useOptimistic, + useRef, useState, useTransition, type MouseEvent as ReactMouseEvent, @@ -69,6 +70,7 @@ import { MiddleTruncate } from "./ui/middle-truncate"; import { BranchPicker, BranchPickerRefItem } from "./BranchPicker"; import { stackedThreadToast, toastManager } from "./ui/toast"; +const MAX_SELECTED_REF_CONFIRM_PAGES = 10; export interface BranchToolbarBranchSelectorHandle { open: () => void; } @@ -384,16 +386,33 @@ export function BranchToolbarBranchSelector({ // The picker's list can predate a checkout an agent ran itself, so confirm a worktree // claim before skipping the switch. Unconfirmed claims fall back to a real checkout, // which Git refuses if the branch is checked out elsewhere. + const latestSelectionIdRef = useRef(0); const readSelectedRef = async (cwd: string, listedRef: VcsRef): Promise => { - const result = await readRefs({ - environmentId, - input: { cwd, query: listedRef.name, limit: 100, refresh: true }, - }); - return resolveConfirmedSelectedRef(listedRef, result._tag === "Success" ? result.value : null); + let cursor: number | undefined; + // Name matches aren't ranked, so page until the ref shows up or the list ends. + for (let page = 0; page < MAX_SELECTED_REF_CONFIRM_PAGES; page++) { + const result = await readRefs({ + environmentId, + input: { + cwd, + query: listedRef.name, + refKind: listedRef.isRemote ? "remote" : "local", + limit: 100, + ...(cursor === undefined ? { refresh: true } : { cursor }), + }, + }); + if (result._tag !== "Success") return resolveConfirmedSelectedRef(listedRef, null); + const confirmed = resolveConfirmedSelectedRef(listedRef, result.value); + // It returns `listedRef` itself only when this page was truncated without the ref. + if (confirmed !== listedRef || result.value.nextCursor === null) return confirmed; + cursor = result.value.nextCursor; + } + return listedRef; }; const selectBranch = async (listedRef: VcsRef) => { if (!canUpdateThreadBranch || !branchCwd || !activeProjectCwd || isBranchActionPending) return; + const selectionId = ++latestSelectionIdRef.current; if (isSelectingWorktreeBase) { setThreadBranch(listedRef.name, null); @@ -408,6 +427,8 @@ export function BranchToolbarBranchSelector({ const refName = listedRef.worktreePath ? await readSelectedRef(branchCwd, listedRef) : listedRef; + // A later pick made while this one was confirming its worktree wins. + if (selectionId !== latestSelectionIdRef.current) return; const selectionTarget = resolveBranchSelectionTarget({ activeProjectCwd, activeWorktreePath, From 153f872740eb27d0a2b39d3e79eea502a3f5c8b4 Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Tue, 3 Nov 2026 19:59:32 +0800 Subject: [PATCH 4/4] fix(web): an unconfirmed worktree claim no longer skips the checkout Co-Authored-By: Claude Opus 5.5 --- .../components/BranchToolbar.logic.test.ts | 35 +++++++++++++++++++ .../web/src/components/BranchToolbar.logic.ts | 20 +++++++++++ .../BranchToolbarBranchSelector.tsx | 20 +++-------- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/apps/web/src/components/BranchToolbar.logic.test.ts b/apps/web/src/components/BranchToolbar.logic.test.ts index cf3f3c3f27e97..b35136483474e 100644 --- a/apps/web/src/components/BranchToolbar.logic.test.ts +++ b/apps/web/src/components/BranchToolbar.logic.test.ts @@ -7,6 +7,8 @@ import { resolveDraftEnvModeAfterBranchChange, resolveEffectiveEnvMode, resolveBranchToolbarPrBranch, + confirmSelectedRef, + MAX_SELECTED_REF_CONFIRM_PAGES, resolveConfirmedSelectedRef, resolveLocalCheckoutBranchMismatch, sanitizeNewRefName, @@ -278,3 +280,36 @@ describe("resolveConfirmedSelectedRef", () => { expect(resolveConfirmedSelectedRef(listed, freshRead).worktreePath).toBe(expected); }); }); + +describe("confirmSelectedRef", () => { + const listed: VcsRef = { + name: "main", + isRemote: false, + current: false, + isDefault: true, + worktreePath: "/repo", + }; + const pagesUntil = (foundOnPage: number | null) => { + const cursors: Array = []; + const readPage = async (cursor: number | undefined) => { + cursors.push(cursor); + const page = cursors.length - 1; + return page === foundOnPage + ? { refs: [{ ...listed, worktreePath: "/other" }], nextCursor: (page + 1) * 100 } + : { refs: [], nextCursor: (page + 1) * 100 }; + }; + return { cursors, readPage }; + }; + + it("follows pages until it finds the ref", async () => { + const { cursors, readPage } = pagesUntil(2); + expect((await confirmSelectedRef(listed, readPage)).worktreePath).toBe("/other"); + expect(cursors).toEqual([undefined, 100, 200]); + }); + + it("drops the claim when the page limit runs out without the ref", async () => { + const { cursors, readPage } = pagesUntil(null); + expect((await confirmSelectedRef(listed, readPage)).worktreePath).toBeNull(); + expect(cursors).toHaveLength(MAX_SELECTED_REF_CONFIRM_PAGES); + }); +}); diff --git a/apps/web/src/components/BranchToolbar.logic.ts b/apps/web/src/components/BranchToolbar.logic.ts index 2640aea7e8e68..a4b3415ac2ae5 100644 --- a/apps/web/src/components/BranchToolbar.logic.ts +++ b/apps/web/src/components/BranchToolbar.logic.ts @@ -259,6 +259,26 @@ export function resolveConfirmedSelectedRef( return freshRead.nextCursor === null ? { ...listedRef, worktreePath: null } : listedRef; } +export const MAX_SELECTED_REF_CONFIRM_PAGES = 10; + +/** Pages through a fresh ref read until the picked ref is confirmed or ruled out. */ +export async function confirmSelectedRef( + listedRef: VcsRef, + readPage: ( + cursor: number | undefined, + ) => Promise<{ readonly refs: ReadonlyArray; readonly nextCursor: number | null } | null>, +): Promise { + let cursor: number | undefined; + for (let page = 0; page < MAX_SELECTED_REF_CONFIRM_PAGES; page++) { + const freshRead = await readPage(cursor); + const confirmed = resolveConfirmedSelectedRef(listedRef, freshRead); + // It returns `listedRef` itself only when this page was truncated without the ref. + if (confirmed !== listedRef || freshRead?.nextCursor == null) return confirmed; + cursor = freshRead.nextCursor; + } + return { ...listedRef, worktreePath: null }; +} + export function resolveBranchSelectionTarget(input: { activeProjectCwd: string; activeWorktreePath: string | null; diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 65a03397cb7f4..9dfd8c573edde 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -51,7 +51,7 @@ import { resolveBranchToolbarPrBranch, resolveBranchSelectionTarget, resolveBranchToolbarValue, - resolveConfirmedSelectedRef, + confirmSelectedRef, resolveDraftEnvModeAfterBranchChange, resolveEffectiveEnvMode, sanitizeNewRefName, @@ -69,8 +69,6 @@ import { ComposerControl } from "./chat/ComposerControl"; import { MiddleTruncate } from "./ui/middle-truncate"; import { BranchPicker, BranchPickerRefItem } from "./BranchPicker"; import { stackedThreadToast, toastManager } from "./ui/toast"; - -const MAX_SELECTED_REF_CONFIRM_PAGES = 10; export interface BranchToolbarBranchSelectorHandle { open: () => void; } @@ -387,10 +385,8 @@ export function BranchToolbarBranchSelector({ // claim before skipping the switch. Unconfirmed claims fall back to a real checkout, // which Git refuses if the branch is checked out elsewhere. const latestSelectionIdRef = useRef(0); - const readSelectedRef = async (cwd: string, listedRef: VcsRef): Promise => { - let cursor: number | undefined; - // Name matches aren't ranked, so page until the ref shows up or the list ends. - for (let page = 0; page < MAX_SELECTED_REF_CONFIRM_PAGES; page++) { + const readSelectedRef = (cwd: string, listedRef: VcsRef): Promise => + confirmSelectedRef(listedRef, async (cursor) => { const result = await readRefs({ environmentId, input: { @@ -401,14 +397,8 @@ export function BranchToolbarBranchSelector({ ...(cursor === undefined ? { refresh: true } : { cursor }), }, }); - if (result._tag !== "Success") return resolveConfirmedSelectedRef(listedRef, null); - const confirmed = resolveConfirmedSelectedRef(listedRef, result.value); - // It returns `listedRef` itself only when this page was truncated without the ref. - if (confirmed !== listedRef || result.value.nextCursor === null) return confirmed; - cursor = result.value.nextCursor; - } - return listedRef; - }; + return result._tag === "Success" ? result.value : null; + }); const selectBranch = async (listedRef: VcsRef) => { if (!canUpdateThreadBranch || !branchCwd || !activeProjectCwd || isBranchActionPending) return;