mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 08:02:52 +00:00
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>
166 lines
7.6 KiB
Markdown
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`.
|