diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 59d148434ae..063aad0366e 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -134,7 +134,11 @@ import { shouldPollChecksPanelRuntimeSshStatus, type ChecksPanelGitStatusSnapshot } from './checks-panel-git-status-snapshot' -import { resolveChecksPanelPRRefreshRequest } from './checks-panel-pr-refresh-request' +import { + getChecksPanelForegroundReviewEvidenceKey, + resolveChecksPanelPRRefreshRequest, + resolveChecksPanelReviewEvidenceProvider +} from './checks-panel-pr-refresh-request' import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval' import { useMountedRef } from '@/hooks/useMountedRef' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' @@ -965,7 +969,22 @@ export default function ChecksPanel(): React.JSX.Element { hostedReviewCreation?.review?.number ?? checksPanelReviewLookupResult.openReviewUrl ?? 'unknown' - const unrenderedReviewEvidenceProvider = hostedReviewCreation?.provider ?? hostedReview?.provider + const unrenderedReviewEvidenceProvider = resolveChecksPanelReviewEvidenceProvider({ + linkedGitHubPR: linkedPR, + linkedGitLabMR, + linkedBitbucketPR, + linkedAzureDevOpsPR, + linkedGiteaPR, + eligibilityProvider: hostedReviewCreation?.provider, + cachedProvider: hostedReview?.provider + }) + const foregroundReviewEvidenceKey = getChecksPanelForegroundReviewEvidenceKey({ + refreshContextKey, + reviewEvidenceIdentity: unrenderedReviewEvidenceIdentity, + reviewEvidenceProvider: unrenderedReviewEvidenceProvider, + hasUnrenderedReviewEvidence, + isGitHubReviewContext + }) // Confirmed readiness from the last eligibility snapshot, not live canCreate (which would be circular and flap during transient failures). const hardErrorObservedAt = isGitHubReviewContext && hardRefreshError && hardRefreshError.contextKey === panelContextKey @@ -1359,7 +1378,7 @@ export default function ChecksPanel(): React.JSX.Element { }, [agentComposerState?.commentResolution, stateRequestKey]) useEffect(() => { - if (!hasUnrenderedReviewEvidence || !isPanelVisible) { + if (foregroundReviewEvidenceKey === null || !isPanelVisible) { foregroundedUnrenderedReviewKeyRef.current = null } if (isPanelVisible && repo && !isFolder && branch) { @@ -1376,17 +1395,17 @@ export default function ChecksPanel(): React.JSX.Element { }) // Why: the gh-based refresh coordinator is GitHub-only; running it elsewhere gave a spurious gh_unavailable error hiding a valid composer. if (activeWorktreeId && isGitHubReviewContext) { - const evidenceKey = `${refreshContextKey}::${unrenderedReviewEvidenceIdentity}` const refreshRequest = resolveChecksPanelPRRefreshRequest({ cachedHasPR: prCachedHasPR, cachedFetchedAt: prFetchedAt ?? null, panelVisibleSince: panelVisibleSinceRef.current, - hasUnrenderedReviewEvidence, - reviewEvidenceProvider: unrenderedReviewEvidenceProvider, - hasRequestedForegroundRefresh: foregroundedUnrenderedReviewKeyRef.current === evidenceKey + hasUnrenderedReviewEvidence: foregroundReviewEvidenceKey !== null, + hasRequestedForegroundRefresh: + foregroundReviewEvidenceKey !== null && + foregroundedUnrenderedReviewKeyRef.current === foregroundReviewEvidenceKey }) - if (refreshRequest.reason === 'active' && hasUnrenderedReviewEvidence) { - foregroundedUnrenderedReviewKeyRef.current = evidenceKey + if (refreshRequest.reason === 'active' && foregroundReviewEvidenceKey !== null) { + foregroundedUnrenderedReviewKeyRef.current = foregroundReviewEvidenceKey } enqueueGitHubPRRefresh(activeWorktreeId, refreshRequest.reason, refreshRequest.priority) } @@ -1397,7 +1416,7 @@ export default function ChecksPanel(): React.JSX.Element { enqueueGitHubPRRefresh, fallbackGitHubPRNumber, fetchHostedReviewForBranch, - hasUnrenderedReviewEvidence, + foregroundReviewEvidenceKey, isFolder, isGitHubReviewContext, isPanelVisible, @@ -1409,9 +1428,6 @@ export default function ChecksPanel(): React.JSX.Element { linkedPR, prCachedHasPR, prFetchedAt, - refreshContextKey, - unrenderedReviewEvidenceIdentity, - unrenderedReviewEvidenceProvider, repo ]) diff --git a/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.test.ts b/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.test.ts index df9786e62f7..cc3c9c49b7f 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.test.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.test.ts @@ -1,5 +1,73 @@ import { describe, expect, it } from 'vitest' -import { resolveChecksPanelPRRefreshRequest } from './checks-panel-pr-refresh-request' +import { + getChecksPanelForegroundReviewEvidenceKey, + resolveChecksPanelPRRefreshRequest, + resolveChecksPanelReviewEvidenceProvider +} from './checks-panel-pr-refresh-request' + +describe('resolveChecksPanelReviewEvidenceProvider', () => { + const noLinkedReviews = { + linkedGitHubPR: null, + linkedGitLabMR: null, + linkedBitbucketPR: null, + linkedAzureDevOpsPR: null, + linkedGiteaPR: null + } + + it.each([ + ['linkedGitHubPR', 'github'], + ['linkedGitLabMR', 'gitlab'], + ['linkedBitbucketPR', 'bitbucket'], + ['linkedAzureDevOpsPR', 'azure-devops'], + ['linkedGiteaPR', 'gitea'] + ] as const)('lets an explicit %s link outrank stale cached metadata', (linkedField, provider) => { + expect( + resolveChecksPanelReviewEvidenceProvider({ + ...noLinkedReviews, + [linkedField]: 42, + cachedProvider: 'unsupported' + }) + ).toBe(provider) + }) + + it('uses eligibility before cached provider metadata when no review is linked', () => { + expect( + resolveChecksPanelReviewEvidenceProvider({ + ...noLinkedReviews, + eligibilityProvider: 'bitbucket', + cachedProvider: 'gitlab' + }) + ).toBe('bitbucket') + }) +}) + +describe('getChecksPanelForegroundReviewEvidenceKey', () => { + const input = { + refreshContextKey: 'worktree::cache::branch', + reviewEvidenceIdentity: 42, + hasUnrenderedReviewEvidence: true, + isGitHubReviewContext: true + } as const + + it('keeps optimistic and confirmed GitHub evidence on one request key', () => { + const optimisticKey = getChecksPanelForegroundReviewEvidenceKey(input) + const confirmedKey = getChecksPanelForegroundReviewEvidenceKey({ + ...input, + reviewEvidenceProvider: 'github' + }) + expect(optimisticKey).toBe('worktree::cache::branch::github::42') + expect(confirmedKey).toBe(optimisticKey) + }) + + it('clears the request key when evidence switches to another provider', () => { + expect( + getChecksPanelForegroundReviewEvidenceKey({ + ...input, + reviewEvidenceProvider: 'gitlab' + }) + ).toBeNull() + }) +}) describe('resolveChecksPanelPRRefreshRequest', () => { it('uses an active refresh for a cached miss from before the checks panel became visible', () => { @@ -56,18 +124,6 @@ describe('resolveChecksPanelPRRefreshRequest', () => { ).toEqual({ reason: 'swr', priority: 30 }) }) - it('does not promote non-GitHub review evidence into an active GitHub lookup', () => { - expect( - resolveChecksPanelPRRefreshRequest({ - cachedHasPR: null, - cachedFetchedAt: null, - panelVisibleSince: 200, - hasUnrenderedReviewEvidence: true, - reviewEvidenceProvider: 'gitlab' - }) - ).toEqual({ reason: 'swr', priority: 30 }) - }) - it('keeps populated or unknown cache entries on the background path', () => { expect( resolveChecksPanelPRRefreshRequest({ diff --git a/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.ts b/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.ts index c2a52b5eb3f..2ade777964d 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.ts @@ -7,7 +7,6 @@ type ChecksPanelPRRefreshRequestInput = { panelVisibleSince: number | null // A known-but-unrendered review needs one foreground lookup to resolve its transient state. hasUnrenderedReviewEvidence?: boolean - reviewEvidenceProvider?: HostedReviewProvider hasRequestedForegroundRefresh?: boolean } @@ -16,6 +15,58 @@ type ChecksPanelPRRefreshRequest = { priority: number } +type ChecksPanelReviewEvidenceProviderInput = { + linkedGitHubPR: number | null + linkedGitLabMR: number | null + linkedBitbucketPR: number | null + linkedAzureDevOpsPR: number | null + linkedGiteaPR: number | null + eligibilityProvider?: HostedReviewProvider | undefined + cachedProvider?: HostedReviewProvider | undefined +} + +type ChecksPanelForegroundReviewEvidenceKeyInput = { + refreshContextKey: string + reviewEvidenceIdentity: number | string + reviewEvidenceProvider?: HostedReviewProvider | undefined + hasUnrenderedReviewEvidence: boolean + isGitHubReviewContext: boolean +} + +export function resolveChecksPanelReviewEvidenceProvider( + input: ChecksPanelReviewEvidenceProviderInput +): HostedReviewProvider | undefined { + if (input.linkedGitHubPR !== null) { + return 'github' + } + if (input.linkedGitLabMR !== null) { + return 'gitlab' + } + if (input.linkedBitbucketPR !== null) { + return 'bitbucket' + } + if (input.linkedAzureDevOpsPR !== null) { + return 'azure-devops' + } + if (input.linkedGiteaPR !== null) { + return 'gitea' + } + return input.eligibilityProvider ?? input.cachedProvider +} + +export function getChecksPanelForegroundReviewEvidenceKey( + input: ChecksPanelForegroundReviewEvidenceKeyInput +): string | null { + if ( + !input.hasUnrenderedReviewEvidence || + !input.isGitHubReviewContext || + (input.reviewEvidenceProvider !== undefined && input.reviewEvidenceProvider !== 'github') + ) { + return null + } + return `${input.refreshContextKey}::github::${input.reviewEvidenceIdentity}` +} + export function resolveChecksPanelPRRefreshRequest( input: ChecksPanelPRRefreshRequestInput ): ChecksPanelPRRefreshRequest { @@ -25,9 +76,7 @@ export function resolveChecksPanelPRRefreshRequest( input.panelVisibleSince !== null && input.cachedFetchedAt < input.panelVisibleSince const unresolvedEvidenceNeedsForeground = - input.hasUnrenderedReviewEvidence && - input.cachedHasPR !== true && - (input.reviewEvidenceProvider === undefined || input.reviewEvidenceProvider === 'github') + input.hasUnrenderedReviewEvidence && input.cachedHasPR !== true if ( !input.hasRequestedForegroundRefresh &&