Tighten GitHub refresh UI and pacing

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-05-12 17:49:11 -04:00
co-authored by Orca
parent d21effd905
commit 70857de542
2 changed files with 64 additions and 36 deletions
@@ -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) {
@@ -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({
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center opacity-80 hover:opacity-100 transition-opacity">
{pr.checksStatus === 'success' && (
<CircleCheck className="size-3.5 text-emerald-500" />
{prRefreshState?.status === 'in-flight' && (
<LoaderCircle className="size-3.5 text-muted-foreground animate-spin" />
)}
{pr.checksStatus === 'failure' && (
<CircleX className="size-3.5 text-rose-500" />
)}
{pr.checksStatus === 'pending' && (
<LoaderCircle className="size-3.5 text-amber-500 animate-spin" />
{prRefreshState?.status === 'paused' && (
<PauseCircle className="size-3.5 text-muted-foreground" />
)}
{!['in-flight', 'paused'].includes(prRefreshState?.status ?? '') &&
pr.checksStatus === 'success' && (
<CircleCheck className="size-3.5 text-emerald-500" />
)}
{!['in-flight', 'paused'].includes(prRefreshState?.status ?? '') &&
pr.checksStatus === 'failure' && (
<CircleX className="size-3.5 text-rose-500" />
)}
{!['in-flight', 'paused'].includes(prRefreshState?.status ?? '') &&
pr.checksStatus === 'pending' && (
<LoaderCircle className="size-3.5 text-amber-500 animate-spin" />
)}
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<span>CI checks {checksLabel(pr.checksStatus).toLowerCase()}</span>
<span>
{prRefreshState?.status === 'in-flight'
? 'Refreshing CI checks'
: prRefreshState?.status === 'paused'
? 'CI refresh paused'
: `CI checks ${checksLabel(pr.checksStatus).toLowerCase()}`}
</span>
</TooltipContent>
</Tooltip>
</div>