diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 1e644af9a83..5cddaae2a19 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -24,6 +24,7 @@ export default function ChecksPanel(): React.JSX.Element { const repo = useRepoById(activeWorktree?.repoId ?? null) const prCache = useAppStore((s) => s.prCache) const fetchPRForBranch = useAppStore((s) => s.fetchPRForBranch) + const enqueueGitHubPRRefresh = useAppStore((s) => s.enqueueGitHubPRRefresh) const gitConflictOperationByWorktree = useAppStore((s) => s.gitConflictOperationByWorktree) // Why: the sidebar stays mounted when closed (for performance). Gate @@ -301,6 +302,43 @@ export default function ChecksPanel(): React.JSX.Element { } }, [repo, branch, linkedPR, fetchPRForBranch, fetchPRChecks, fetchComments]) + const handleEntryRefresh = useCallback(async () => { + if (!repo || !branch || !activeWorktreeId) { + return + } + // Why: entering the Checks tab is automatic UI behavior, not an explicit + // user refresh. Route PR refresh through the coordinator so rate-limit + // guards still apply, while checks/comments can refresh from cached PR data. + enqueueGitHubPRRefresh(activeWorktreeId, 'active', 80) + + if (!pr) { + setChecks([]) + setComments([]) + return + } + + const refreshedChecks = fetchPRChecks(repo.path, pr.number, branch, pr.headSha, { + force: true + }).then( + (result) => { + setChecks(result) + const signature = JSON.stringify(result.map((c) => `${c.name}:${c.status}:${c.conclusion}`)) + pollIntervalRef.current = + signature === prevChecksRef.current + ? Math.min(pollIntervalRef.current * 2, 120_000) + : 30_000 + prevChecksRef.current = signature + }, + (err) => { + console.warn('Failed to fetch PR checks:', err) + setChecks([]) + } + ) + setChecksLoading(true) + const refreshedComments = fetchComments({ force: true, prNumberOverride: pr.number }) + await Promise.all([refreshedChecks.finally(() => setChecksLoading(false)), refreshedComments]) + }, [repo, branch, activeWorktreeId, enqueueGitHubPRRefresh, pr, fetchPRChecks, fetchComments]) + // Why: force a freshness check on each "entry" into the Checks tab so PRs // opened outside Orca, externally force-pushed heads, and stale checks/comments // appear without waiting for the cache TTL. The grace window suppresses @@ -340,8 +378,8 @@ export default function ChecksPanel(): React.JSX.Element { // a fresh baseline rather than colliding with the previous PR's backoff. pollIntervalRef.current = 30_000 prevChecksRef.current = '' - void handleRefresh() - }, [entryKey, prFetchedAt, checksFetchedAt, commentsFetchedAt, prNumber, handleRefresh]) + void handleEntryRefresh() + }, [entryKey, prFetchedAt, checksFetchedAt, commentsFetchedAt, prNumber, handleEntryRefresh]) const handleStartEdit = useCallback(() => { if (!pr) { diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index e419bf3ce73..4e3e37aa7e2 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -9,6 +9,7 @@ import { Bell, GitMerge, LoaderCircle, + PauseCircle, CircleCheck, CircleX, Server, @@ -64,7 +65,6 @@ const WorktreeCard = React.memo(function WorktreeCard({ }: WorktreeCardProps) { const openModal = useAppStore((s) => s.openModal) const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta) - const enqueueGitHubPRRefresh = useAppStore((s) => s.enqueueGitHubPRRefresh) const fetchIssue = useAppStore((s) => s.fetchIssue) const cardProps = useAppStore((s) => s.worktreeCardProperties) const handleEditIssue = useCallback( @@ -278,33 +278,8 @@ const WorktreeCard = React.memo(function WorktreeCard({ ] ) - const showPR = cardProps.includes('pr') - const showCI = cardProps.includes('ci') const showIssue = cardProps.includes('issue') - // Skip GitHub fetches when the corresponding card sections are hidden. - // This preference is purely presentational, so background refreshes would - // spend rate limit budget on data the user cannot see. - useEffect(() => { - if (repo && !isFolder && !worktree.isBare && prCacheKey && isActive && (showPR || showCI)) { - // Why: pass linkedPR so worktrees created from a PR (whose new local - // branch differs from the PR's head ref) still resolve their PR via - // a number-based fallback in the main process. - enqueueGitHubPRRefresh(worktree.id, 'active', 80) - } - }, [ - repo, - isFolder, - worktree.isBare, - worktree.id, - worktree.linkedPR, - enqueueGitHubPRRefresh, - isActive, - prCacheKey, - showPR, - showCI - ]) - // Same rationale for issues: once that section is hidden, polling only burns // GitHub calls and keeps stale-but-invisible data warm for no user benefit. useEffect(() => { @@ -530,19 +505,34 @@ const WorktreeCard = React.memo(function WorktreeCard({ - {pr.checksStatus === 'success' && ( - + {prRefreshState?.status === 'in-flight' && ( + )} - {pr.checksStatus === 'failure' && ( - - )} - {pr.checksStatus === 'pending' && ( - + {prRefreshState?.status === 'paused' && ( + )} + {!['in-flight', 'paused'].includes(prRefreshState?.status ?? '') && + pr.checksStatus === 'success' && ( + + )} + {!['in-flight', 'paused'].includes(prRefreshState?.status ?? '') && + pr.checksStatus === 'failure' && ( + + )} + {!['in-flight', 'paused'].includes(prRefreshState?.status ?? '') && + pr.checksStatus === 'pending' && ( + + )} - CI checks {checksLabel(pr.checksStatus).toLowerCase()} + + {prRefreshState?.status === 'in-flight' + ? 'Refreshing CI checks' + : prRefreshState?.status === 'paused' + ? 'CI refresh paused' + : `CI checks ${checksLabel(pr.checksStatus).toLowerCase()}`} +