From 27185dad09ea577d28c54a7295366d724a2f27a0 Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Fri, 26 Jun 2026 22:29:01 -0700 Subject: [PATCH] Cover imported detached PR sidebar status Co-authored-by: Orca --- .../WorktreeCard.merged-pr-display.test.tsx | 51 +++++++++++++++++++ .../src/components/sidebar/WorktreeCard.tsx | 24 +++++++-- .../folder-workspace-card-pr-display.test.ts | 31 +++++++++++ .../folder-workspace-card-pr-display.ts | 33 ++++++++---- 4 files changed, 125 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx index e6146f00b1d..7e5961099a5 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx @@ -193,6 +193,57 @@ describe('WorktreeCard merged PR fallback display', () => { expect(markup).not.toContain('Detached HEAD @ merge-c') }) + it('shows detached PRs from legacy path-scoped cache entries', async () => { + hostedReviewCache = { + 'local::repo-1::__detached_head__:merge-commit': { + data: null, + fetchedAt: 200 + } + } + prCache = { + '/repo::__detached_head__:merge-commit': { + data: makePRInfo({ + headSha: 'pr-head-before-merge' + }), + fetchedAt: 100 + } + } + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderWorktreeCardMarkup( + + ) + + expect(markup).toContain('Linked PR #6340') + expect(markup).not.toContain('Detached HEAD @ merge-c') + }) + + it('hides detached HEAD badge when the parent list supplies PR status', async () => { + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderWorktreeCardMarkup( + + ) + + expect(markup).not.toContain('Detached HEAD @ merge-c') + }) + it('suppresses cached merged PR after a newer hosted-review miss when the worktree head moved', async () => { prCache = { 'repo-1::feature/local-branch': { diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 9cf9b709be7..2eeedbdf746 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -3,7 +3,11 @@ import React, { useEffect, useCallback, useState } from 'react' import { useAppStore } from '@/store' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review' import { issueCacheKey as getIssueCacheKey } from '@/store/slices/github' -import { getGitHubPRCacheBranch, getGitHubPRCacheKey } from '@/store/slices/github-cache-key' +import { + getGitHubPRCacheBranch, + getGitHubPRCacheKey, + getLegacyGitHubPRCacheKey +} from '@/store/slices/github-cache-key' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' @@ -435,6 +439,15 @@ const WorktreeCard = React.memo(function WorktreeCard({ true ) : '' + const canUseLegacyPRCache = repo !== undefined && !repo.connectionId && !repo.executionHostId + const legacyRepoScopedPRCacheKey = + canUseLegacyPRCache && prCacheBranch + ? getLegacyGitHubPRCacheKey(repo.path, repo.id, prCacheBranch) + : '' + const legacyPathScopedPRCacheKey = + canUseLegacyPRCache && prCacheBranch + ? getLegacyGitHubPRCacheKey(repo.path, undefined, prCacheBranch) + : '' const issueCacheKey = repo && worktree.linkedIssue ? getIssueCacheKey( @@ -455,7 +468,12 @@ const WorktreeCard = React.memo(function WorktreeCard({ const hostedReviewEntry = useAppStore((s) => hostedReviewCacheKey ? s.hostedReviewCache[hostedReviewCacheKey] : undefined ) - const prCacheEntry = useAppStore((s) => (prCacheKey ? s.prCache?.[prCacheKey] : undefined)) + const prCacheEntry = useAppStore( + (s) => + (prCacheKey ? s.prCache?.[prCacheKey] : undefined) ?? + (legacyRepoScopedPRCacheKey ? s.prCache?.[legacyRepoScopedPRCacheKey] : undefined) ?? + (legacyPathScopedPRCacheKey ? s.prCache?.[legacyPathScopedPRCacheKey] : undefined) + ) const issueEntry = useAppStore((s) => (issueCacheKey ? s.issueCache[issueCacheKey] : undefined)) const linearIssueEntry = useAppStore((s) => linearIssueCacheKey ? s.linearIssueCache[linearIssueCacheKey] : undefined @@ -1171,7 +1189,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ // Why: a detached merge commit can still have PR context; once shown, the PR // status is the higher-signal sidebar identity than the raw commit badge. const showDetachedHeadInMetaRow = - !compactCards && !isFolder && detachedHeadDisplay !== null && hoverReview === null + !compactCards && !isFolder && detachedHeadDisplay !== null && statusLaneReview === null const showBranch = !isFolder && branch.length > 0 && diff --git a/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts b/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts index 6ca326b3c61..5ac1f8863be 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts @@ -145,6 +145,37 @@ describe('getFolderWorkspaceCardPrDisplay', () => { expect(display).toMatchObject({ number: 4, status: 'success' }) }) + + it('uses detached HEAD-scoped PR cache for attached worktrees', () => { + const detached = makeWorktree({ + id: 'detached', + branch: '', + head: 'merge-commit', + linkedPR: null + }) + + const display = getFolderWorkspaceCardPrDisplay({ + folderWorkspaceId: 'folder-1', + workspaceLineageByChildKey: { [detached.id]: makeWorkspaceLineage(detached) }, + worktreeLineageById: {}, + worktreeMap: new Map([[detached.id, detached]]), + repoMap: new Map([[repo.id, repo]]), + hostedReviewCache: { + 'local::repo-1::__detached_head__:merge-commit': { data: null } + }, + prCache: { + 'repo-1::__detached_head__:merge-commit': { + data: makePr(24, 'failure') + } + } + }) + + expect(display).toMatchObject({ + provider: 'github', + number: 24, + status: 'failure' + }) + }) }) function makePr(number: number, checksStatus: CheckStatus) { diff --git a/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.ts b/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.ts index fb43b506dcd..1c79d66215a 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.ts @@ -1,6 +1,10 @@ import type { AppState } from '@/store/types' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review' -import { getGitHubPRCacheKey, getLegacyGitHubPRCacheKey } from '@/store/slices/github-cache-key' +import { + getGitHubPRCacheBranch, + getGitHubPRCacheKey, + getLegacyGitHubPRCacheKey +} from '@/store/slices/github-cache-key' import { branchName } from '@/lib/git-utils' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import type { @@ -162,13 +166,14 @@ function getAttachedWorktreePrDisplay({ } const branch = branchName(worktree.branch).trim() - if (!branch) { + const cacheBranch = getGitHubPRCacheBranch(branch, worktree.head) + if (!cacheBranch) { return getLinkedReviewDisplay(worktree) } const hostedReviewCacheKey = getHostedReviewCacheKey( repo.path, - branch, + cacheBranch, settings, repo.id, repo.connectionId, @@ -193,7 +198,7 @@ function getAttachedWorktreePrDisplay({ return hostedReviewDisplay } - const cachedGitHubPr = getCachedGitHubPr({ worktree, repo, branch, prCache, settings }) + const cachedGitHubPr = getCachedGitHubPr({ worktree, repo, cacheBranch, prCache, settings }) if (cachedGitHubPr) { return { provider: 'github', @@ -222,24 +227,24 @@ function getLinkedReviewDisplay(worktree: Worktree): WorktreeCardPrDisplay | nul function getCachedGitHubPr({ worktree, repo, - branch, + cacheBranch, prCache, settings }: { worktree: Worktree repo: Repo - branch: string + cacheBranch: string prCache: Record | null settings?: AppState['settings'] }): PRInfo | null { - if (!prCache || worktree.linkedPR === null) { + if (!prCache || (worktree.linkedPR === null && !cacheBranch.startsWith('__detached_head__:'))) { return null } const cacheKey = getGitHubPRCacheKey( repo.path, repo.id, - branch, + cacheBranch, settings, repo.connectionId, repo.executionHostId, @@ -247,10 +252,10 @@ function getCachedGitHubPr({ ) const canUseLegacyPRCache = !repo.connectionId && !repo.executionHostId const legacyRepoScopedCacheKey = canUseLegacyPRCache - ? getLegacyGitHubPRCacheKey(repo.path, repo.id, branch) + ? getLegacyGitHubPRCacheKey(repo.path, repo.id, cacheBranch) : '' const legacyPathScopedCacheKey = canUseLegacyPRCache - ? getLegacyGitHubPRCacheKey(repo.path, undefined, branch) + ? getLegacyGitHubPRCacheKey(repo.path, undefined, cacheBranch) : '' const entry = (prCache[cacheKey] as PrCacheEntry | undefined) ?? @@ -261,7 +266,13 @@ function getCachedGitHubPr({ ? (prCache[legacyPathScopedCacheKey] as PrCacheEntry | undefined) : undefined) const pr = entry?.data ?? null - return pr?.number === worktree.linkedPR ? pr : null + if (!pr) { + return null + } + if (cacheBranch.startsWith('__detached_head__:')) { + return pr + } + return pr.number === worktree.linkedPR ? pr : null } function compareReviewDisplays(left: WorktreeCardPrDisplay, right: WorktreeCardPrDisplay): number {