From ef477199af467d7c18b8ba2f9fbdb3ba089e5ae6 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:23:40 -0700 Subject: [PATCH] Keep worktree PR badges stable during refresh (#6397) Co-authored-by: Orca --- .../sidebar/WorktreeCard.pr-display.test.tsx | 169 ++++++++++++++++++ .../src/components/sidebar/WorktreeCard.tsx | 40 ++++- 2 files changed, 200 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx index f3bc4768bc2..92282083b26 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx @@ -668,4 +668,173 @@ describe('WorktreeCard linked PR display', () => { expect(markup).not.toContain('Branch') expect(markup).not.toContain('lucide-git-branch') }) + + it('keeps the detailed right-side PR badge during a transient hosted-review miss', async () => { + settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false } + worktreeCardProperties = ['pr'] + hostedReviewCache = { + 'local::repo-1::feature/local-branch': { + data: null, + fetchedAt: 100 + } + } + prCache = { + 'repo-1::feature/local-branch': { + data: makePRInfo({ + number: 6340, + title: 'Remove split terminal from onboarding checklist', + state: 'open', + checksStatus: 'success' + }), + fetchedAt: 200 + } + } + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderWorktreeCardMarkup( + + ) + + expect(markup).toContain('Linked PR #6340') + expect(markup).not.toContain('Linked PR #456') + }) + + it('keeps the detailed PR badge when a transient miss still has an older review hint', async () => { + settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false } + worktreeCardProperties = ['pr'] + hostedReviewCache = { + 'local::repo-1::feature/local-branch': { + data: null, + fetchedAt: 100, + linkedReviewHintKey: 'github:999' + } + } + prCache = { + 'repo-1::feature/local-branch': { + data: makePRInfo({ + number: 6340, + title: 'Remove split terminal from onboarding checklist', + state: 'open', + checksStatus: 'success' + }), + fetchedAt: 200 + } + } + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderWorktreeCardMarkup( + + ) + + expect(markup).toContain('Linked PR #6340') + expect(markup).not.toContain('Linked PR #456') + }) + + it('keeps durable non-GitHub linked review metadata ahead of branch PR cache', async () => { + settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false } + worktreeCardProperties = ['pr'] + hostedReviewCache = { + 'local::repo-1::feature/local-branch': { + data: null, + fetchedAt: 100 + } + } + prCache = { + 'repo-1::feature/local-branch': { + data: makePRInfo({ + number: 6340, + title: 'Remove split terminal from onboarding checklist', + state: 'open', + checksStatus: 'success' + }), + fetchedAt: 200 + } + } + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderWorktreeCardMarkup( + + ) + + expect(markup).toContain('Linked MR #77') + expect(markup).not.toContain('Linked PR #6340') + }) + + it('does not resurrect an older PR cache entry after a newer hosted-review miss', async () => { + settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false } + worktreeCardProperties = ['pr'] + hostedReviewCache = { + 'local::repo-1::feature/local-branch': { + data: null, + fetchedAt: 200 + } + } + prCache = { + 'repo-1::feature/local-branch': { + data: makePRInfo({ + number: 6340, + title: 'Remove split terminal from onboarding checklist', + state: 'open', + checksStatus: 'success' + }), + fetchedAt: 100 + } + } + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderWorktreeCardMarkup( + + ) + + expect(markup).not.toContain('Linked PR #6340') + }) + + it('does not resurrect PR cache on the same millisecond as a hosted-review miss', async () => { + settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false } + worktreeCardProperties = ['pr'] + hostedReviewCache = { + 'local::repo-1::feature/local-branch': { + data: null, + fetchedAt: 200 + } + } + prCache = { + 'repo-1::feature/local-branch': { + data: makePRInfo({ + number: 6340, + title: 'Remove split terminal from onboarding checklist', + state: 'open', + checksStatus: 'success' + }), + fetchedAt: 200 + } + } + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderWorktreeCardMarkup( + + ) + + expect(markup).not.toContain('Linked PR #6340') + }) }) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 7945c35a149..4d88afd55dd 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -441,26 +441,48 @@ const WorktreeCard = React.memo(function WorktreeCard({ const hostedReview: HostedReviewInfo | null | undefined = hostedReviewEntry !== undefined ? hostedReviewEntry.data : undefined - // Why: ChecksPanel can discover a branch PR before hosted-review metadata - // warms. The sidebar status slot should not fall back to "branch" while - // the branch PR cache already knows the review state. - const cachedBranchReview = - hostedReview === undefined && prCacheEntry?.data - ? hostedReviewInfoFromGitHubPRInfo(prCacheEntry.data) - : hostedReview + const linkedGitHubPR = worktree.linkedPR ?? null const linkedGitLabMR = worktree.linkedGitLabMR ?? null const linkedBitbucketPR = worktree.linkedBitbucketPR ?? null const linkedAzureDevOpsPR = worktree.linkedAzureDevOpsPR ?? null const linkedGiteaPR = worktree.linkedGiteaPR ?? null + const hasNonGitHubLinkedReview = + linkedGitLabMR !== null || + linkedBitbucketPR !== null || + linkedAzureDevOpsPR !== null || + linkedGiteaPR !== null + const hasLinkedReview = + linkedGitHubPR !== null || + linkedGitLabMR !== null || + linkedBitbucketPR !== null || + linkedAzureDevOpsPR !== null || + linkedGiteaPR !== null + // Why: ChecksPanel can discover a branch PR before hosted-review metadata + // warms, and transient older hosted-review misses can race with that cache. + // Newer hosted-review misses still win so stale PR cache cannot resurrect. + const cachedBranchPR = prCacheEntry?.data + const cachedBranchPRFetchedAt = prCacheEntry?.fetchedAt + const useCachedBranchReview = + cachedBranchPR !== undefined && + cachedBranchPR !== null && + !hasNonGitHubLinkedReview && + (hostedReview === undefined || + (hostedReview === null && + cachedBranchPRFetchedAt !== undefined && + cachedBranchPRFetchedAt > (hostedReviewEntry?.fetchedAt ?? 0))) + const cachedBranchReview = useCachedBranchReview + ? hostedReviewInfoFromGitHubPRInfo(cachedBranchPR) + : hostedReview const prDisplay = getWorktreeCardPrDisplay( cachedBranchReview, - worktree.linkedPR, + linkedGitHubPR, linkedGitLabMR, linkedBitbucketPR, linkedAzureDevOpsPR, linkedGiteaPR, { - reviewHintKey: hostedReviewEntry?.linkedReviewHintKey ?? (prCacheEntry?.data ? '' : undefined) + reviewHintKey: + useCachedBranchReview && !hasLinkedReview ? '' : hostedReviewEntry?.linkedReviewHintKey } ) const issue: IssueInfo | null | undefined = worktree.linkedIssue