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