From 690830b48aa73daace8cfcd3fd86ab9e38d8a628 Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Tue, 10 Nov 2026 21:55:39 +0800 Subject: [PATCH] fix(web): a branch picked right after opening the picker no longer waits for its status read Co-Authored-By: Claude Opus 5.5 --- .../BranchToolbarBranchSelector.tsx | 7 ++- packages/client-runtime/src/state/vcs.test.ts | 61 +++++++++++++++++++ packages/client-runtime/src/state/vcs.ts | 7 +++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 9782f258afe19..ac303209cb1ce 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -125,7 +125,7 @@ export function BranchToolbarBranchSelector({ const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false, }); - const refreshStatus = useAtomCommand(vcsEnvironment.refreshStatus, { + const refreshLocalStatusForPicker = useAtomCommand(vcsEnvironment.refreshLocalStatusForPicker, { reportFailure: false, }); const createRefMutation = useAtomCommand(vcsEnvironment.createRef, { @@ -512,7 +512,10 @@ export function BranchToolbarBranchSelector({ const handleOpenChange = (open: boolean) => { setIsBranchMenuOpen(open); if (open && !isBranchMenuOpen && branchCwd !== null) { - void refreshStatus({ environmentId, input: { cwd: branchCwd, localOnly: true } }); + void refreshLocalStatusForPicker({ + environmentId, + input: { cwd: branchCwd, localOnly: true }, + }); } if (!open) { setBranchQuery(""); diff --git a/packages/client-runtime/src/state/vcs.test.ts b/packages/client-runtime/src/state/vcs.test.ts index 7327c8cc203ee..1ae04382edc8d 100644 --- a/packages/client-runtime/src/state/vcs.test.ts +++ b/packages/client-runtime/src/state/vcs.test.ts @@ -467,6 +467,67 @@ describe("cached VCS refs", () => { ), ); + it.effect("a checkout does not wait behind a picker's local status refresh", () => + Effect.scoped( + Effect.gen(function* () { + const refreshStarted = yield* Deferred.make(); + const releaseRefresh = yield* Deferred.make(); + const client = { + [WS_METHODS.vcsRefreshStatus]: () => + Deferred.succeed(refreshStarted, undefined).pipe( + Effect.andThen(Deferred.await(releaseRefresh)), + ), + [WS_METHODS.vcsSwitchRef]: () => Effect.succeed({ refName: "feature" }), + } as unknown as WsRpcProtocolClient; + const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ + target: TARGET, + state: yield* SubscriptionRef.make(CONNECTED_CONNECTION_STATE), + session: yield* SubscriptionRef.make(Option.some(session(client))), + prepared: yield* SubscriptionRef.make(Option.none()), + connect: Effect.void, + disconnect: Effect.void, + retryNow: Effect.void, + } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); + const run: EnvironmentRegistry.EnvironmentRegistry["Service"]["run"] = ( + _environmentId, + effect, + ) => Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor); + const runtime = Atom.runtime( + Layer.merge( + Layer.succeed( + EnvironmentRegistry.EnvironmentRegistry, + EnvironmentRegistry.EnvironmentRegistry.of({ + run, + } as unknown as EnvironmentRegistry.EnvironmentRegistry["Service"]), + ), + Layer.succeed(Persistence.EnvironmentCacheStore, cacheWithRefs(Option.none())), + ), + ); + const atoms = createVcsEnvironmentAtoms(runtime); + const registry = yield* Effect.acquireRelease(Effect.sync(AtomRegistry.make), (registry) => + Effect.sync(() => registry.dispose()), + ); + const target = { environmentId: TARGET.environmentId, input: { cwd: "/repo" } }; + + const refresh = atoms.refreshLocalStatusForPicker.run(registry, { + ...target, + input: { cwd: "/repo", localOnly: true }, + }); + yield* Deferred.await(refreshStarted); + const checkout = yield* Effect.promise(() => + atoms.switchRef.run(registry, { + ...target, + input: { cwd: "/repo", refName: "feature" }, + }), + ); + + expect(AsyncResult.isSuccess(checkout)).toBe(true); + yield* Deferred.succeed(releaseRefresh, undefined); + expect(AsyncResult.isSuccess(yield* Effect.promise(() => refresh))).toBe(true); + }), + ), + ); + it.effect("suppresses persisted snapshots after an environment-wide clear fails", () => Effect.scoped( Effect.gen(function* () { diff --git a/packages/client-runtime/src/state/vcs.ts b/packages/client-runtime/src/state/vcs.ts index 12d55a7477058..310515ceb9915 100644 --- a/packages/client-runtime/src/state/vcs.ts +++ b/packages/client-runtime/src/state/vcs.ts @@ -341,6 +341,13 @@ export function createVcsEnvironmentAtoms( concurrency: vcsCommandConcurrency, onSettled: invalidateRefs, }), + // Outside the serial queue: it only reads status, and a checkout picked right after opening + // the picker must not wait behind that read. + refreshLocalStatusForPicker: createEnvironmentRpcCommand(runtime, { + label: "environment-data:vcs:refresh-local-status-for-picker", + tag: WS_METHODS.vcsRefreshStatus, + onSettled: invalidateRefs, + }), createWorktree: createEnvironmentRpcCommand(runtime, { label: "environment-data:vcs:create-worktree", tag: WS_METHODS.vcsCreateWorktree,