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 {