orca/docs/refresh-on-checks-tab.md

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_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:

    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:

    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.