From 724887717f1b5ded5b60bd5abe7e41a9ce5b47e5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 27 Jul 2026 17:00:19 +0200 Subject: [PATCH] fix(web): scope PR state to the thread branch (#4460) Co-authored-by: codex --- .../components/BranchToolbar.logic.test.ts | 30 +++++++++++++++++++ .../web/src/components/BranchToolbar.logic.ts | 7 +++++ .../BranchToolbarBranchSelector.tsx | 7 +++-- apps/web/src/components/ChatView.tsx | 1 - apps/web/src/components/Sidebar.tsx | 1 - apps/web/src/components/SidebarV2.tsx | 1 - .../components/ThreadStatusIndicators.test.ts | 28 +++++++++-------- .../src/components/ThreadStatusIndicators.tsx | 8 +---- 8 files changed, 59 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/BranchToolbar.logic.test.ts b/apps/web/src/components/BranchToolbar.logic.test.ts index c4ae7b96694ccf4c42ee3eba46c00ebc7f79e3dc..e59ffc8f68b0d66019e06c68e3b07139eacf51c5 100644 --- a/apps/web/src/components/BranchToolbar.logic.test.ts +++ b/apps/web/src/components/BranchToolbar.logic.test.ts @@ -9,6 +9,7 @@ import { resolveDraftEnvModeAfterBranchChange, resolveEffectiveEnvMode, resolveEnvModeLabel, + resolveBranchToolbarPrBranch, resolveBranchToolbarValue, resolveLockedWorkspaceLabel, resolveLocalCheckoutBranchMismatch, @@ -173,6 +174,35 @@ describe("resolveBranchToolbarValue", () => { }); }); +describe("resolveBranchToolbarPrBranch", () => { + it("uses the explicit thread branch when it matches the displayed branch", () => { + expect( + resolveBranchToolbarPrBranch({ + activeThreadBranch: "feature/current", + resolvedActiveBranch: "feature/current", + }), + ).toBe("feature/current"); + }); + + it("hides PR state while an optimistic branch switch is in flight", () => { + expect( + resolveBranchToolbarPrBranch({ + activeThreadBranch: "feature/current", + resolvedActiveBranch: "feature/next", + }), + ).toBeNull(); + }); + + it("does not infer PR state without an explicit thread branch", () => { + expect( + resolveBranchToolbarPrBranch({ + activeThreadBranch: null, + resolvedActiveBranch: "feature/current", + }), + ).toBeNull(); + }); +}); + describe("resolveLocalCheckoutBranchMismatch", () => { it("detects when a local thread is associated with a different branch than the checkout", () => { expect( diff --git a/apps/web/src/components/BranchToolbar.logic.ts b/apps/web/src/components/BranchToolbar.logic.ts index 8fe35fa464a612e4939e197b3a91fdc796e31bce..3549eff6eea59e13fd23d871706e1774c72a6d1d 100644 --- a/apps/web/src/components/BranchToolbar.logic.ts +++ b/apps/web/src/components/BranchToolbar.logic.ts @@ -156,6 +156,13 @@ export function resolveBranchToolbarValue(input: { return currentGitBranch ?? activeThreadBranch; } +export function resolveBranchToolbarPrBranch(input: { + activeThreadBranch: string | null; + resolvedActiveBranch: string | null; +}): string | null { + return input.activeThreadBranch === input.resolvedActiveBranch ? input.activeThreadBranch : null; +} + export function resolveLocalCheckoutBranchMismatch(input: { effectiveEnvMode: EnvMode; activeWorktreePath: string | null; diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 763de56d8c43135f2dcd07e1c4071a43d25bda7e..a61427f207adbdec40b5457bad93fb043491b6f1 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -35,6 +35,7 @@ import { parsePullRequestReference } from "../pullRequestReference"; import { getSourceControlPresentation } from "../sourceControlPresentation"; import { deriveLocalBranchNameFromRemoteRef, + resolveBranchToolbarPrBranch, resolveBranchSelectionTarget, resolveBranchToolbarValue, resolveDraftEnvModeAfterBranchChange, @@ -591,9 +592,11 @@ export function BranchToolbarBranchSelector({ // PR pill shown next to the branch selector when the active branch has one. const branchPr = resolveThreadPr({ - threadBranch: resolvedActiveBranch, + threadBranch: resolveBranchToolbarPrBranch({ + activeThreadBranch, + resolvedActiveBranch, + }), gitStatus: branchStatusQuery.data ?? null, - hasDedicatedWorktree: activeWorktreePath !== null, }); const branchPrStatus = prStatusIndicator(branchPr, branchStatusQuery.data?.sourceControlProvider); // Action-oriented tooltip (the pill opens the PR), distinct from the sidebar's diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 73fb98bc97bfa794d4dafd879d6f2f382e90bc4e..b234d0765f24337a9680761cba4c8639440679d2 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3874,7 +3874,6 @@ function ChatViewContent(props: ChatViewProps) { const activeThreadPr = resolveThreadPr({ threadBranch: activeThread?.branch ?? null, gitStatus: gitStatusQuery.data ?? null, - hasDedicatedWorktree: (activeThread?.worktreePath ?? null) !== null, }); const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true; const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true; diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index a1d95eaa7340cc6dcdb65b465a9a68eedece4382..b05b3a39d90450961876f5b3860ed531a6410792 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -452,7 +452,6 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr const pr = resolveThreadPr({ threadBranch: thread.branch, gitStatus: gitStatus.data, - hasDedicatedWorktree: thread.worktreePath !== null, }); const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider); const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index 8c5891ebe7e363b1294f3d88517f5df312f42e53..a5f9eca5f4ae1ad1e049592c883daf1f0393de09 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -502,7 +502,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { const pr = resolveThreadPr({ threadBranch: thread.branch, gitStatus: gitStatus.data, - hasDedicatedWorktree: thread.worktreePath !== null, }); const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider); const settledPrHoverClass = pr ? settledPrHoverColorClass(pr.state) : undefined; diff --git a/apps/web/src/components/ThreadStatusIndicators.test.ts b/apps/web/src/components/ThreadStatusIndicators.test.ts index 9fb4535f2665f88e60b868031bd5e8a972280a55..3eb8e4f710f1fa8b866628cbfb5c008eec67e81f 100644 --- a/apps/web/src/components/ThreadStatusIndicators.test.ts +++ b/apps/web/src/components/ThreadStatusIndicators.test.ts @@ -36,31 +36,35 @@ describe("resolveThreadPr", () => { resolveThreadPr({ threadBranch: "feature/other", gitStatus: status(), - hasDedicatedWorktree: false, }), ).toBeNull(); }); - it("shows PR indicators for dedicated worktree threads even when branch metadata is stale", () => { - const gitStatus = status(); - + it("hides PR indicators when a dedicated worktree has switched away from the thread branch", () => { expect( resolveThreadPr({ - threadBranch: "feature/old-name", - gitStatus, - hasDedicatedWorktree: true, + threadBranch: "stack/base", + gitStatus: status(), }), - ).toBe(gitStatus.pr); + ).toBeNull(); }); - it("shows PR indicators for dedicated worktree threads even when branch metadata is missing", () => { - const gitStatus = status(); - + it("hides PR indicators when thread branch metadata is missing", () => { expect( resolveThreadPr({ threadBranch: null, + gitStatus: status(), + }), + ).toBeNull(); + }); + + it("shows the PR when the live checkout matches the stored thread branch", () => { + const gitStatus = status(); + + expect( + resolveThreadPr({ + threadBranch: "feature/current", gitStatus, - hasDedicatedWorktree: true, }), ).toBe(gitStatus.pr); }); diff --git a/apps/web/src/components/ThreadStatusIndicators.tsx b/apps/web/src/components/ThreadStatusIndicators.tsx index 0d3291a9e2852e2b30264a76dc243e2223282245..af53d1a78b20d1c5872dd6d2bc5fa9e2c1fc4118 100644 --- a/apps/web/src/components/ThreadStatusIndicators.tsx +++ b/apps/web/src/components/ThreadStatusIndicators.tsx @@ -113,17 +113,12 @@ export function PrStatusTooltipContent({ status }: { status: PrStatusIndicator } export function resolveThreadPr(input: { threadBranch: string | null; gitStatus: VcsStatusResult | null; - hasDedicatedWorktree: boolean; }): ThreadPr | null { - const { threadBranch, gitStatus, hasDedicatedWorktree } = input; + const { threadBranch, gitStatus } = input; if (gitStatus === null) { return null; } - if (hasDedicatedWorktree) { - return gitStatus.pr ?? null; - } - if (threadBranch === null || gitStatus.refName !== threadBranch) { return null; } @@ -258,7 +253,6 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar const pr = resolveThreadPr({ threadBranch: thread.branch, gitStatus: gitStatus.data, - hasDedicatedWorktree: thread.worktreePath !== null, }); const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider); const threadStatus = resolveThreadStatusPill({ -- 2.54.0