Files
orca/docs/refresh-on-checks-tab.md
JinjingandOrca 648f207281 feat(checks-panel): auto-refresh on entering Checks tab (#1688)
Force a freshness check each time the user enters the Checks tab
(open sidebar, switch to Checks tab, or switch active worktree/branch)
so stale PR metadata, cached-null "no PR" results, stale checks, and
stale comments are surfaced immediately rather than waiting for the
cache TTL.

- Extracts entry-refresh logic into `checks-entry-refresh.ts` with a
  30 s grace window to suppress rapid show/hide duplicate fetches.
- Adds a `shouldEntryRefresh` effect in `ChecksPanel` keyed by
  `activeWorktreeId::repo.path::branch`; resets on panel hide so
  close-and-reopen re-evaluates freshness.
- Fixes a stale-closure bug in `handleRefresh`: `fetchPRChecks` is now
  called directly with the freshly resolved `headSha` after PR refresh
  instead of reusing the pre-refresh closure's captured sha.
- Adds 11 unit tests in `checks-entry-refresh.test.ts`.
- Design doc: `docs/refresh-on-checks-tab.md`.

Co-authored-by: Orca <help@stably.ai>
2026-05-10 23:16:41 -07:00

166 lines
7.6 KiB
Markdown

# Auto-refresh on entering the Checks tab
## Problem
The Checks panel fetches on visibility, but those fetches are cache-respecting:
- `CACHE_TTL = 300_000` for PRs and comments.
- `CHECKS_CACHE_TTL = 60_000` for checks.
- `ChecksPanel.tsx` fetches PRs on repo/branch changes without `force`.
- 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
1. **Entry trigger.** Add an effect in `ChecksPanel.tsx` keyed by:
```ts
const entryKey = isPanelVisible && repo && !isFolder && branch
? `${activeWorktreeId ?? ''}::${repo.path}::${branch}`
: ''
```
Track the last processed visible `entryKey` in a ref. When `entryKey` is empty,
reset the ref to `''`. When it becomes non-empty and differs from the ref, this
is an entry. This is required; a `prevKey !== currentKey` check that does not
reset on hide would miss closing and reopening the same PR.
2. **Grace window.** Define `ENTRY_REFRESH_GRACE_MS = 15_000` in
`ChecksPanel.tsx`. Select only timestamps, not whole cache records:
```ts
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 `null` is 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
than `Date.now() - ENTRY_REFRESH_GRACE_MS`; otherwise skip.
3. **Reuse `handleRefresh`, but pass the refreshed head SHA through.** The
manual refresh flow is the right shape: force `fetchPRForBranch`, then if a
PR is returned, force checks and comments for the returned PR. Extend
`fetchChecks()` to accept a `headShaOverride` (or call `fetchPRChecks`
directly from `handleRefresh`) so the checks request uses
`refreshedPR.headSha`, not the stale `pr?.headSha` captured before the PR
refresh completed. This handles PR number changes, cached `null`, and
external force-pushes correctly.
4. **Reset polling attention state before refresh.** When the entry refresh runs,
set `pollIntervalRef.current = 30_000` and `prevChecksRef.current = ''` before
calling `handleRefresh()`. `fetchChecks()` will then write the new signature
from the forced result.
5. **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 })` and `fetchPRComments({ 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 pass
`noCache: true` to the main process.
Accept that tradeoff for this feature. It avoids duplicate `gh` calls 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 `prNumber` is null. Refresh
the PR cache entry on tab entry when its timestamp is outside the grace window.
- **First Checks entry after app start.** Only `prCache` and `issueCache` are
persisted. `checksCache` and `commentsCache` start empty, so a known PR should
force checks/comments on entry.
- **Worktree switch while Checks is visible.** Include `activeWorktreeId` in 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 `noCache` request. 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 by `fetchPRForBranch` before fetching checks, so checks are requested
with the current `headSha`.
- **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 `gh` path is used.
## Rollout
1. Add timestamp selectors, `ENTRY_REFRESH_GRACE_MS`, and the entry effect in
`ChecksPanel.tsx`.
2. Keep `handleRefresh()` as the single refresh implementation.
3. Add `ChecksPanel.test.tsx` if 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.
4. Run `pnpm typecheck && pnpm lint`.