7.6 KiB
Auto-refresh on entering the Checks tab
Problem
The Checks panel fetches on visibility, but those fetches are cache-respecting:
CACHE_TTL = 300_000for PRs and comments.CHECKS_CACHE_TTL = 60_000for checks.ChecksPanel.tsxfetches PRs on repo/branch changes withoutforce.- Checks polling and the comments effect also call the store without
force.
That means entering the Checks tab can render stale PR metadata, a cached null
"no PR" result, stale comments, or stale checks. The checks path is partially
protected by a shorter renderer TTL, but the main process also uses gh api --cache 60s for the REST checks endpoint unless noCache is set. Manual refresh
already bypasses these renderer and gh api caches for the requests it starts.
Goal
When the user enters the Checks tab, run one freshness check for the active worktree and force-refresh only when the relevant cache timestamps are older than a small grace window. "Entering" means:
- opening the right sidebar while the Checks tab is selected;
- switching from another right-sidebar tab to Checks;
- switching active worktree/repo/branch while Checks is already visible.
The refresh must cover PR discovery too, including cached null, so a PR opened
outside Orca can appear without waiting up to 5 minutes.
Non-goals
- Changing the existing polling cadence while the panel stays visible.
- Refreshing on window focus, network reconnect, or arbitrary background events.
- Changing the manual refresh button.
- Prefetching checks for non-active worktrees.
- Adding cross-renderer or cross-window request coordination. Orca currently has one live main window; renderer in-flight maps are not a multi-window primitive.
Design
-
Entry trigger. Add an effect in
ChecksPanel.tsxkeyed by:const entryKey = isPanelVisible && repo && !isFolder && branch ? `${activeWorktreeId ?? ''}::${repo.path}::${branch}` : ''Track the last processed visible
entryKeyin a ref. WhenentryKeyis empty, reset the ref to''. When it becomes non-empty and differs from the ref, this is an entry. This is required; aprevKey !== currentKeycheck that does not reset on hide would miss closing and reopening the same PR. -
Grace window. Define
ENTRY_REFRESH_GRACE_MS = 15_000inChecksPanel.tsx. Select only timestamps, not whole cache records:const prFetchedAt = useAppStore( (s) => (prCacheKey ? s.prCache[prCacheKey]?.fetchedAt : undefined) ) const checksFetchedAt = useAppStore( (s) => prNumber ? s.checksCache[`${repo?.path ?? ''}::pr-checks::${prNumber}`]?.fetchedAt : undefined ) const commentsFetchedAt = useAppStore( (s) => prNumber ? s.commentsCache[`${repo?.path ?? ''}::pr-comments::${prNumber}`]?.fetchedAt : undefined )Missing PR cache is stale. A cached PR value of
nullis still a PR cache entry and should be refreshed on entry once outside the grace window. When a PR number is known, missing checks/comments timestamps are stale. When no PR is known, checks/comments are not relevant yet.Run
handleRefresh()if the oldest relevant timestamp is missing or older thanDate.now() - ENTRY_REFRESH_GRACE_MS; otherwise skip. -
Reuse
handleRefresh, but pass the refreshed head SHA through. The manual refresh flow is the right shape: forcefetchPRForBranch, then if a PR is returned, force checks and comments for the returned PR. ExtendfetchChecks()to accept aheadShaOverride(or callfetchPRChecksdirectly fromhandleRefresh) so the checks request usesrefreshedPR.headSha, not the stalepr?.headShacaptured before the PR refresh completed. This handles PR number changes, cachednull, and external force-pushes correctly. -
Reset polling attention state before refresh. When the entry refresh runs, set
pollIntervalRef.current = 30_000andprevChecksRef.current = ''before callinghandleRefresh().fetchChecks()will then write the new signature from the forced result. -
Do not overstate in-flight behavior. Current store behavior is:
fetchPRForBranch({ force: true })bypasses a non-forced in-flight PR request and uses a generation guard so the older result cannot overwrite the newer cache entry.fetchPRChecks({ force: true })andfetchPRComments({ force: true })do not bypass any in-flight request for the same key. If a non-forced poll is already in flight, entry refresh will join it and may not passnoCache: trueto the main process.
Accept that tradeoff for this feature. It avoids duplicate
ghcalls during a visible polling race. If strict "entry always bypasses gh cache" semantics are required later, change checks/comments in-flight maps to track{ promise, force, generation }like PRs.
API cost and feasibility
This is not free. A cold entry refresh can start:
- one PR lookup (
gh:prForBranch); - one checks request (
gh:prChecks); - one comments request (
gh:prComments).
gh:prChecks usually calls gh api repos/{owner}/{repo}/commits/{sha}/check-runs
and uses --cache 60s unless forced; if that fails it falls back to gh pr checks, which does not use the --cache flag.
gh:prComments is heavier than "one GitHub API call": it runs issue comments
REST, review threads GraphQL, and reviews REST in parallel. noCache only
removes --cache 60s from the REST gh api calls; the GraphQL call is always
made.
The 15 s grace window is therefore required, not cosmetic.
Edge cases
- Cached no-PR result. Do not skip just because
prNumberis null. Refresh the PR cache entry on tab entry when its timestamp is outside the grace window. - First Checks entry after app start. Only
prCacheandissueCacheare persisted.checksCacheandcommentsCachestart empty, so a known PR should force checks/comments on entry. - Worktree switch while Checks is visible. Include
activeWorktreeIdin the entry key so same repo/branch switches still count as a new entry. The existing render-time local reset handles stale title/loading state. - Rapid tab toggles. Hiding the panel resets the processed entry key; showing it again re-evaluates timestamps. The grace window suppresses duplicate calls.
- Concurrent polling tick. Checks/comments may reuse the in-flight poll
request instead of forcing a new
noCacherequest. This is intentional for now. - Conflicting PR refresh. The existing
mergeable === 'CONFLICTING'effect can still force a PR refresh. Entry refresh may also run, but PR generation guards prevent older PR responses from overwriting newer cache entries. - PR head changed externally.
handleRefresh()must use the refreshed PR returned byfetchPRForBranchbefore fetching checks, so checks are requested with the currentheadSha. - Component unmount mid-fetch. The comments auto-fetch guards stale
responses; the checks fetch path and
handleRefresh()do not cancel local setters. This feature should keep that behavior unless tests expose a warning or stale-state regression. - SSH / remote repo. No path manipulation or platform-specific shortcut code
is needed. The same IPC-backed
ghpath is used.
Rollout
- Add timestamp selectors,
ENTRY_REFRESH_GRACE_MS, and the entry effect inChecksPanel.tsx. - Keep
handleRefresh()as the single refresh implementation. - Add
ChecksPanel.test.tsxif absent. Cover stale PR/null cache refreshes, fresh-within-grace skips, known PR with missing checks/comments refreshes, hidden panel no-op, and hide/show same PR re-evaluation. - Run
pnpm typecheck && pnpm lint.