diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index c6c23f95b..9572ade51 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -124,6 +124,7 @@ import { shouldPollChecksPanelRuntimeSshStatus, type ChecksPanelGitStatusSnapshot } from './checks-panel-git-status-snapshot' +import { resolveChecksPanelPRRefreshRequest } 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' @@ -471,6 +472,7 @@ export default function ChecksPanel(): React.JSX.Element { const confirm = useConfirmationDialog() const prevChecksRef = useRef('') const conflictSummaryRefreshKeyRef = useRef(null) + const panelVisibleSinceRef = useRef(null) commentsRef.current = comments const prGenerationRecords = useAppStore((s) => s.pullRequestGenerationRecords) const allocatePullRequestGenerationRequestId = useAppStore( @@ -652,6 +654,7 @@ export default function ChecksPanel(): React.JSX.Element { // the entry for the active repo and branch. const prCacheEntry = useAppStore((s) => selectReviewCacheEntry(s.prCache, prCacheKey || null)) const pr: PRInfo | null = prCacheEntry?.data ?? null + const prCachedHasPR = prCacheEntry ? prCacheEntry.data !== null : null const hostedReview = useAppStore((s) => hostedReviewCacheKey ? (s.hostedReviewCache[hostedReviewCacheKey]?.data ?? null) : null ) @@ -736,6 +739,14 @@ export default function ChecksPanel(): React.JSX.Element { repo?.id ]) + useEffect(() => { + if (!isPanelVisible) { + panelVisibleSinceRef.current = null + return + } + panelVisibleSinceRef.current = Date.now() + }, [isPanelVisible, panelContextKey]) + // Why: select only timestamps (not whole cache records) so the entry-refresh // effect doesn't re-run on every cache mutation. See // docs/refresh-on-checks-tab.md. @@ -1221,7 +1232,12 @@ export default function ChecksPanel(): React.JSX.Element { staleWhileRevalidate: true }) if (activeWorktreeId && !isGitLabReviewContext) { - enqueueGitHubPRRefresh(activeWorktreeId, 'swr', 30) + const refreshRequest = resolveChecksPanelPRRefreshRequest({ + cachedHasPR: prCachedHasPR, + cachedFetchedAt: prFetchedAt ?? null, + panelVisibleSince: panelVisibleSinceRef.current + }) + enqueueGitHubPRRefresh(activeWorktreeId, refreshRequest.reason, refreshRequest.priority) } } }, [ @@ -1239,6 +1255,8 @@ export default function ChecksPanel(): React.JSX.Element { linkedGiteaPR, linkedGitLabMR, linkedPR, + prCachedHasPR, + prFetchedAt, 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 new file mode 100644 index 000000000..39c27d71c --- /dev/null +++ b/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { resolveChecksPanelPRRefreshRequest } from './checks-panel-pr-refresh-request' + +describe('resolveChecksPanelPRRefreshRequest', () => { + it('uses an active refresh for a cached miss from before the checks panel became visible', () => { + expect( + resolveChecksPanelPRRefreshRequest({ + cachedHasPR: false, + cachedFetchedAt: 100, + panelVisibleSince: 200 + }) + ).toEqual({ reason: 'active', priority: 80 }) + }) + + it('keeps fresh empty lookups on the background path', () => { + expect( + resolveChecksPanelPRRefreshRequest({ + cachedHasPR: false, + cachedFetchedAt: 200, + panelVisibleSince: 100 + }) + ).toEqual({ reason: 'swr', priority: 30 }) + }) + + it('keeps populated or unknown cache entries on the background path', () => { + expect( + resolveChecksPanelPRRefreshRequest({ + cachedHasPR: true, + cachedFetchedAt: 100, + panelVisibleSince: 200 + }) + ).toEqual({ reason: 'swr', priority: 30 }) + + expect( + resolveChecksPanelPRRefreshRequest({ + cachedHasPR: null, + cachedFetchedAt: null, + panelVisibleSince: 200 + }) + ).toEqual({ reason: 'swr', priority: 30 }) + }) +}) 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 new file mode 100644 index 000000000..e78afd3de --- /dev/null +++ b/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.ts @@ -0,0 +1,30 @@ +import type { GitHubPRRefreshReason } from '../../../../shared/types' + +type ChecksPanelPRRefreshRequestInput = { + cachedHasPR: boolean | null + cachedFetchedAt: number | null + panelVisibleSince: number | null +} + +type ChecksPanelPRRefreshRequest = { + reason: GitHubPRRefreshReason + priority: number +} + +export function resolveChecksPanelPRRefreshRequest( + input: ChecksPanelPRRefreshRequestInput +): ChecksPanelPRRefreshRequest { + const cachedMissPredatesVisiblePanel = + input.cachedHasPR === false && + input.cachedFetchedAt !== null && + input.panelVisibleSince !== null && + input.cachedFetchedAt < input.panelVisibleSince + + if (cachedMissPredatesVisiblePanel) { + // Why: external agents can create/merge a PR after Orca cached "none"; + // visible empty-state checks need one foreground lookup to recover. + return { reason: 'active', priority: 80 } + } + + return { reason: 'swr', priority: 30 } +}