authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-27 17:00:19+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-27 17:00:19+02:00
log724887717f1b5ded5b60bd5abe7e41a9ce5b47e5
tree0e068a20abb251a1a0bcc26fe95b0c1d8a27f003
parentbdf99c17bf7ae634728481677cf1bca8db65d00e
signature Signed by PGP key B5690EEEBB952194

fix(web): scope PR state to the thread branch (#4460)

Co-authored-by: codex <codex@users.noreply.github.com>

8 files changed, 57 insertions(+), 22 deletions(-)

apps/web/src/components/BranchToolbar.logic.test.ts+30
......@@ -9,6 +9,7 @@ import {
99 resolveDraftEnvModeAfterBranchChange,
1010 resolveEffectiveEnvMode,
1111 resolveEnvModeLabel,
12 resolveBranchToolbarPrBranch,
1213 resolveBranchToolbarValue,
1314 resolveLockedWorkspaceLabel,
1415 resolveLocalCheckoutBranchMismatch,
......@@ -173,6 +174,35 @@ describe("resolveBranchToolbarValue", () => {
173174 });
174175});
175176
177describe("resolveBranchToolbarPrBranch", () => {
178 it("uses the explicit thread branch when it matches the displayed branch", () => {
179 expect(
180 resolveBranchToolbarPrBranch({
181 activeThreadBranch: "feature/current",
182 resolvedActiveBranch: "feature/current",
183 }),
184 ).toBe("feature/current");
185 });
186
187 it("hides PR state while an optimistic branch switch is in flight", () => {
188 expect(
189 resolveBranchToolbarPrBranch({
190 activeThreadBranch: "feature/current",
191 resolvedActiveBranch: "feature/next",
192 }),
193 ).toBeNull();
194 });
195
196 it("does not infer PR state without an explicit thread branch", () => {
197 expect(
198 resolveBranchToolbarPrBranch({
199 activeThreadBranch: null,
200 resolvedActiveBranch: "feature/current",
201 }),
202 ).toBeNull();
203 });
204});
205
176206describe("resolveLocalCheckoutBranchMismatch", () => {
177207 it("detects when a local thread is associated with a different branch than the checkout", () => {
178208 expect(
apps/web/src/components/BranchToolbar.logic.ts+7
......@@ -156,6 +156,13 @@ export function resolveBranchToolbarValue(input: {
156156 return currentGitBranch ?? activeThreadBranch;
157157}
158158
159export function resolveBranchToolbarPrBranch(input: {
160 activeThreadBranch: string | null;
161 resolvedActiveBranch: string | null;
162}): string | null {
163 return input.activeThreadBranch === input.resolvedActiveBranch ? input.activeThreadBranch : null;
164}
165
159166export function resolveLocalCheckoutBranchMismatch(input: {
160167 effectiveEnvMode: EnvMode;
161168 activeWorktreePath: string | null;
apps/web/src/components/BranchToolbarBranchSelector.tsx+5-2
......@@ -35,6 +35,7 @@ import { parsePullRequestReference } from "../pullRequestReference";
3535import { getSourceControlPresentation } from "../sourceControlPresentation";
3636import {
3737 deriveLocalBranchNameFromRemoteRef,
38 resolveBranchToolbarPrBranch,
3839 resolveBranchSelectionTarget,
3940 resolveBranchToolbarValue,
4041 resolveDraftEnvModeAfterBranchChange,
......@@ -591,9 +592,11 @@ export function BranchToolbarBranchSelector({
591592
592593 // PR pill shown next to the branch selector when the active branch has one.
593594 const branchPr = resolveThreadPr({
594 threadBranch: resolvedActiveBranch,
595 threadBranch: resolveBranchToolbarPrBranch({
596 activeThreadBranch,
597 resolvedActiveBranch,
598 }),
595599 gitStatus: branchStatusQuery.data ?? null,
596 hasDedicatedWorktree: activeWorktreePath !== null,
597600 });
598601 const branchPrStatus = prStatusIndicator(branchPr, branchStatusQuery.data?.sourceControlProvider);
599602 // Action-oriented tooltip (the pill opens the PR), distinct from the sidebar's
apps/web/src/components/ChatView.tsx-1
......@@ -3874,7 +3874,6 @@ function ChatViewContent(props: ChatViewProps) {
38743874 const activeThreadPr = resolveThreadPr({
38753875 threadBranch: activeThread?.branch ?? null,
38763876 gitStatus: gitStatusQuery.data ?? null,
3877 hasDedicatedWorktree: (activeThread?.worktreePath ?? null) !== null,
38783877 });
38793878 const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true;
38803879 const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true;
apps/web/src/components/Sidebar.tsx-1
......@@ -452,7 +452,6 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
452452 const pr = resolveThreadPr({
453453 threadBranch: thread.branch,
454454 gitStatus: gitStatus.data,
455 hasDedicatedWorktree: thread.worktreePath !== null,
456455 });
457456 const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider);
458457 const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds);
apps/web/src/components/SidebarV2.tsx-1
......@@ -502,7 +502,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
502502 const pr = resolveThreadPr({
503503 threadBranch: thread.branch,
504504 gitStatus: gitStatus.data,
505 hasDedicatedWorktree: thread.worktreePath !== null,
506505 });
507506 const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider);
508507 const settledPrHoverClass = pr ? settledPrHoverColorClass(pr.state) : undefined;
apps/web/src/components/ThreadStatusIndicators.test.ts+14-10
......@@ -36,31 +36,35 @@ describe("resolveThreadPr", () => {
3636 resolveThreadPr({
3737 threadBranch: "feature/other",
3838 gitStatus: status(),
39 hasDedicatedWorktree: false,
4039 }),
4140 ).toBeNull();
4241 });
4342
44 it("shows PR indicators for dedicated worktree threads even when branch metadata is stale", () => {
45 const gitStatus = status();
43 it("hides PR indicators when a dedicated worktree has switched away from the thread branch", () => {
44 expect(
45 resolveThreadPr({
46 threadBranch: "stack/base",
47 gitStatus: status(),
48 }),
49 ).toBeNull();
50 });
4651
52 it("hides PR indicators when thread branch metadata is missing", () => {
4753 expect(
4854 resolveThreadPr({
49 threadBranch: "feature/old-name",
50 gitStatus,
51 hasDedicatedWorktree: true,
55 threadBranch: null,
56 gitStatus: status(),
5257 }),
53 ).toBe(gitStatus.pr);
58 ).toBeNull();
5459 });
5560
56 it("shows PR indicators for dedicated worktree threads even when branch metadata is missing", () => {
61 it("shows the PR when the live checkout matches the stored thread branch", () => {
5762 const gitStatus = status();
5863
5964 expect(
6065 resolveThreadPr({
61 threadBranch: null,
66 threadBranch: "feature/current",
6267 gitStatus,
63 hasDedicatedWorktree: true,
6468 }),
6569 ).toBe(gitStatus.pr);
6670 });
apps/web/src/components/ThreadStatusIndicators.tsx+1-7
......@@ -113,17 +113,12 @@ export function PrStatusTooltipContent({ status }: { status: PrStatusIndicator }
113113export function resolveThreadPr(input: {
114114 threadBranch: string | null;
115115 gitStatus: VcsStatusResult | null;
116 hasDedicatedWorktree: boolean;
117116}): ThreadPr | null {
118 const { threadBranch, gitStatus, hasDedicatedWorktree } = input;
117 const { threadBranch, gitStatus } = input;
119118 if (gitStatus === null) {
120119 return null;
121120 }
122121
123 if (hasDedicatedWorktree) {
124 return gitStatus.pr ?? null;
125 }
126
127122 if (threadBranch === null || gitStatus.refName !== threadBranch) {
128123 return null;
129124 }
......@@ -258,7 +253,6 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar
258253 const pr = resolveThreadPr({
259254 threadBranch: thread.branch,
260255 gitStatus: gitStatus.data,
261 hasDedicatedWorktree: thread.worktreePath !== null,
262256 });
263257 const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider);
264258 const threadStatus = resolveThreadStatusPill({