| author | |
| committer | |
| log | 724887717f1b5ded5b60bd5abe7e41a9ce5b47e5 |
| tree | 0e068a20abb251a1a0bcc26fe95b0c1d8a27f003 |
| parent | bdf99c17bf7ae634728481677cf1bca8db65d00e |
| signature | Signed by PGP key B5690EEEBB952194 |
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 { |
| 9 | 9 | resolveDraftEnvModeAfterBranchChange, |
| 10 | 10 | resolveEffectiveEnvMode, |
| 11 | 11 | resolveEnvModeLabel, |
| 12 | resolveBranchToolbarPrBranch, | |
| 12 | 13 | resolveBranchToolbarValue, |
| 13 | 14 | resolveLockedWorkspaceLabel, |
| 14 | 15 | resolveLocalCheckoutBranchMismatch, |
| ... | ... | @@ -173,6 +174,35 @@ describe("resolveBranchToolbarValue", () => { |
| 173 | 174 | }); |
| 174 | 175 | }); |
| 175 | 176 | |
| 177 | describe("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 | ||
| 176 | 206 | describe("resolveLocalCheckoutBranchMismatch", () => { |
| 177 | 207 | it("detects when a local thread is associated with a different branch than the checkout", () => { |
| 178 | 208 | expect( |
apps/web/src/components/BranchToolbar.logic.ts+7| ... | ... | @@ -156,6 +156,13 @@ export function resolveBranchToolbarValue(input: { |
| 156 | 156 | return currentGitBranch ?? activeThreadBranch; |
| 157 | 157 | } |
| 158 | 158 | |
| 159 | export 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 | ||
| 159 | 166 | export function resolveLocalCheckoutBranchMismatch(input: { |
| 160 | 167 | effectiveEnvMode: EnvMode; |
| 161 | 168 | activeWorktreePath: string | null; |
apps/web/src/components/BranchToolbarBranchSelector.tsx+5-2| ... | ... | @@ -35,6 +35,7 @@ import { parsePullRequestReference } from "../pullRequestReference"; |
| 35 | 35 | import { getSourceControlPresentation } from "../sourceControlPresentation"; |
| 36 | 36 | import { |
| 37 | 37 | deriveLocalBranchNameFromRemoteRef, |
| 38 | resolveBranchToolbarPrBranch, | |
| 38 | 39 | resolveBranchSelectionTarget, |
| 39 | 40 | resolveBranchToolbarValue, |
| 40 | 41 | resolveDraftEnvModeAfterBranchChange, |
| ... | ... | @@ -591,9 +592,11 @@ export function BranchToolbarBranchSelector({ |
| 591 | 592 | |
| 592 | 593 | // PR pill shown next to the branch selector when the active branch has one. |
| 593 | 594 | const branchPr = resolveThreadPr({ |
| 594 | threadBranch: resolvedActiveBranch, | |
| 595 | threadBranch: resolveBranchToolbarPrBranch({ | |
| 596 | activeThreadBranch, | |
| 597 | resolvedActiveBranch, | |
| 598 | }), | |
| 595 | 599 | gitStatus: branchStatusQuery.data ?? null, |
| 596 | hasDedicatedWorktree: activeWorktreePath !== null, | |
| 597 | 600 | }); |
| 598 | 601 | const branchPrStatus = prStatusIndicator(branchPr, branchStatusQuery.data?.sourceControlProvider); |
| 599 | 602 | // 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) { |
| 3874 | 3874 | const activeThreadPr = resolveThreadPr({ |
| 3875 | 3875 | threadBranch: activeThread?.branch ?? null, |
| 3876 | 3876 | gitStatus: gitStatusQuery.data ?? null, |
| 3877 | hasDedicatedWorktree: (activeThread?.worktreePath ?? null) !== null, | |
| 3878 | 3877 | }); |
| 3879 | 3878 | const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true; |
| 3880 | 3879 | 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 |
| 452 | 452 | const pr = resolveThreadPr({ |
| 453 | 453 | threadBranch: thread.branch, |
| 454 | 454 | gitStatus: gitStatus.data, |
| 455 | hasDedicatedWorktree: thread.worktreePath !== null, | |
| 456 | 455 | }); |
| 457 | 456 | const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider); |
| 458 | 457 | const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); |
apps/web/src/components/SidebarV2.tsx-1| ... | ... | @@ -502,7 +502,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { |
| 502 | 502 | const pr = resolveThreadPr({ |
| 503 | 503 | threadBranch: thread.branch, |
| 504 | 504 | gitStatus: gitStatus.data, |
| 505 | hasDedicatedWorktree: thread.worktreePath !== null, | |
| 506 | 505 | }); |
| 507 | 506 | const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider); |
| 508 | 507 | const settledPrHoverClass = pr ? settledPrHoverColorClass(pr.state) : undefined; |
apps/web/src/components/ThreadStatusIndicators.test.ts+14-10| ... | ... | @@ -36,31 +36,35 @@ describe("resolveThreadPr", () => { |
| 36 | 36 | resolveThreadPr({ |
| 37 | 37 | threadBranch: "feature/other", |
| 38 | 38 | gitStatus: status(), |
| 39 | hasDedicatedWorktree: false, | |
| 40 | 39 | }), |
| 41 | 40 | ).toBeNull(); |
| 42 | 41 | }); |
| 43 | 42 | |
| 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 | }); | |
| 46 | 51 | |
| 52 | it("hides PR indicators when thread branch metadata is missing", () => { | |
| 47 | 53 | expect( |
| 48 | 54 | resolveThreadPr({ |
| 49 | threadBranch: "feature/old-name", | |
| 50 | gitStatus, | |
| 51 | hasDedicatedWorktree: true, | |
| 55 | threadBranch: null, | |
| 56 | gitStatus: status(), | |
| 52 | 57 | }), |
| 53 | ).toBe(gitStatus.pr); | |
| 58 | ).toBeNull(); | |
| 54 | 59 | }); |
| 55 | 60 | |
| 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", () => { | |
| 57 | 62 | const gitStatus = status(); |
| 58 | 63 | |
| 59 | 64 | expect( |
| 60 | 65 | resolveThreadPr({ |
| 61 | threadBranch: null, | |
| 66 | threadBranch: "feature/current", | |
| 62 | 67 | gitStatus, |
| 63 | hasDedicatedWorktree: true, | |
| 64 | 68 | }), |
| 65 | 69 | ).toBe(gitStatus.pr); |
| 66 | 70 | }); |
apps/web/src/components/ThreadStatusIndicators.tsx+1-7| ... | ... | @@ -113,17 +113,12 @@ export function PrStatusTooltipContent({ status }: { status: PrStatusIndicator } |
| 113 | 113 | export function resolveThreadPr(input: { |
| 114 | 114 | threadBranch: string | null; |
| 115 | 115 | gitStatus: VcsStatusResult | null; |
| 116 | hasDedicatedWorktree: boolean; | |
| 117 | 116 | }): ThreadPr | null { |
| 118 | const { threadBranch, gitStatus, hasDedicatedWorktree } = input; | |
| 117 | const { threadBranch, gitStatus } = input; | |
| 119 | 118 | if (gitStatus === null) { |
| 120 | 119 | return null; |
| 121 | 120 | } |
| 122 | 121 | |
| 123 | if (hasDedicatedWorktree) { | |
| 124 | return gitStatus.pr ?? null; | |
| 125 | } | |
| 126 | ||
| 127 | 122 | if (threadBranch === null || gitStatus.refName !== threadBranch) { |
| 128 | 123 | return null; |
| 129 | 124 | } |
| ... | ... | @@ -258,7 +253,6 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar |
| 258 | 253 | const pr = resolveThreadPr({ |
| 259 | 254 | threadBranch: thread.branch, |
| 260 | 255 | gitStatus: gitStatus.data, |
| 261 | hasDedicatedWorktree: thread.worktreePath !== null, | |
| 262 | 256 | }); |
| 263 | 257 | const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider); |
| 264 | 258 | const threadStatus = resolveThreadStatusPill({ |