Skip to content

perf: Move ShowWorkspace CI polling off the UI IPC path - #223

Merged
Ziinc merged 7 commits into
mainfrom
cursor/async-pr-ci-polling-b216
Aug 8, 2026
Merged

perf: Move ShowWorkspace CI polling off the UI IPC path#223
Ziinc merged 7 commits into
mainfrom
cursor/async-pr-ci-polling-b216

Conversation

@Ziinc

@Ziinc Ziinc commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

When a workspace is opened, queue a single out-of-band PR info + CI checks refresh so the Rust background cache is warmed before the next poll tick. The UI keeps reading cache-only; the user should not see stale PR/CI state after switching workspaces.

Changes

  • Rust: PrStatusManager::queue_branch_refresh / refresh_branch_now — one call fetches PR + CI for a branch; queue is fire-and-forget and coalesces in-flight (repo, branch).
  • Tauri / NAPI: refresh_pr_branch_status returns immediately (does not block the JS event loop on gh).
  • Frontend: usePrInfoViaGh and usePrCiStatus call refreshPrBranchStatus on mount when a branch is present.
  • Tests: Rust coverage for async queue + coalescing; hook tests assert the refresh is queued; screenshot specs mock the new API.

Test plan

  • cargo test --lib pr_status
  • npm run test:run -- test/integration/useMergeQueueStatus.test.ts
  • npm run test:run -- src/components/CiStatusIndicator.test.tsx
  • App-qa: ci-status-indicator success / failure / pending screenshots
Open in Web Open in Cursor 

cursoragent and others added 2 commits August 8, 2026 19:15
PR info was already served from a Rust background cache, but
usePrCiStatus still shelled out to `gh pr checks` every 15s via
sync Tauri commands, which stuttered ShowWorkspace. Cache CI
rollups in the same poller, expose cache-only reads, and run
remaining gh force-fetch commands through spawn_blocking.

Co-authored-by: Ziinc <Ziinc@users.noreply.github.com>
Drop the leftover dynamic import after switching invalidatePrStatuses
to warm both PR info and CI caches in parallel.

Co-authored-by: Ziinc <Ziinc@users.noreply.github.com>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same stutter class as #218, but for CI: ShowWorkspace’s 15s gh pr checks was still on the command path.

  • Background poller now also caches CI; only shells out when a PR exists for the branch
  • UI reads stay cache-only; pr-statuses-updated carries ci_statuses so React Query updates without another gh

poll_one_repo

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force-fetch / panel paths still need gh, but must not stall IPC the way sync commands did (same pattern as gh_create_pr in #217).

  • get_pr_info_via_gh, get_pr_checks_*, and gh_list_pr_review_threads now use spawn_blocking
  • Force-fetch also warms the background CI cache so the next UI read is immediate

get_pr_checks_via_gh

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usePrCiStatus mirrors usePrInfoViaGh: start the Rust poller, listen for cache events, refetch from memory every 15s.

  • Removes the ShowWorkspace stutter source (sync getPrChecksViaGh on interval)
  • invalidatePrStatuses still force-warms both PR + CI after create-PR mutations

usePrCiStatus

cursoragent and others added 2 commits August 8, 2026 19:36
Reduce ShowWorkspace IPC load from inline PR review thread refreshes;
CI status already rides the Rust background cache.

Co-authored-by: Ziinc <Ziinc@users.noreply.github.com>
Keep gh pr checks off the UI path: the poller refreshes CI every 30s
(independently of 60s PR metadata) and usePrCiStatus only reads the cache.

Co-authored-by: Ziinc <Ziinc@users.noreply.github.com>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI now has its own 30s cadence (DEFAULT_CI_POLL_INTERVAL), independent of 60s PR metadata.

  • Mid-cycle tick calls refresh_ci_now / poll_ci_onlygh pr checks only, no gh pr view
  • UI still only reads get_cached_pr_ci_status; force-fetch get_pr_checks_via_gh is spawn_blocking + cache warm

DEFAULT_CI_POLL_INTERVAL · poll_ci_only

cursoragent and others added 3 commits August 8, 2026 19:42
Collapse applyPrStatusesToQueryCache args and hydrate CI via
listCachedPrCiStatuses during sidebar cache reads.

Co-authored-by: Ziinc <Ziinc@users.noreply.github.com>
Warm the Rust PR/CI cache out-of-band via a single fire-and-forget
refresh_pr_branch_status call so opening a workspace is not stuck on
the next poll tick. Hook tests avoid flaky NAPI createTestRepo.

Co-authored-by: Ziinc <Ziinc@users.noreply.github.com>
Satisfy the no-inline-comments lint rule after Biome/cargo fmt.

Co-authored-by: Ziinc <Ziinc@users.noreply.github.com>
}, [repoPath]);
if (branchName) {
void refreshPrBranchStatus(repoPath, branchName);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Workspace open already starts the poller; this queues an immediate branch warm so the first paint is not stuck on a stale 30s/60s tick.
  • Same call from usePrCiStatus — coalesced on the Rust side when both mount together.
  • Permalink:
    useEffect(() => {
    if (!repoPath) return;
    void startPrStatusPolling(repoPath);
    if (branchName) {
    void refreshPrBranchStatus(repoPath, branchName);
    }

inner.branch_refresh_inflight.lock().unwrap().remove(&key);
})
.expect("failed to spawn pr-status-branch-refresh thread");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • One call warms both PR metadata and CI for the opened branch; UI stays on cache reads.
  • Fire-and-forget + inflight coalesce so double-mount (PR hook + CI hook) does not spawn two gh shells.
  • Permalink:
    pub fn queue_branch_refresh(&self, repo_path: String, branch_name: String) {
    self.ensure_started();
    let key = format!("{repo_path}\0{branch_name}");
    {
    let mut inflight = self.inner.branch_refresh_inflight.lock().unwrap();
    if !inflight.insert(key.clone()) {
    return;
    }
    }
    let inner = Arc::clone(&self.inner);
    thread::Builder::new()
    .name("pr-status-branch-refresh".into())
    .spawn(move || {
    poll_one_branch(&inner, &repo_path, &branch_name);
    inner.branch_refresh_inflight.lock().unwrap().remove(&key);
    })
    .expect("failed to spawn pr-status-branch-refresh thread");
    }

// JS event loop on `gh` (which is what caused test timeouts when
// a spy was briefly restored to the real invoke).
treq_lib::pr_status::global().queue_branch_refresh(repo_path, branch_name);
Ok(Value::Null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Must return before gh finishes: invokeSync runs on the Node event loop, so a blocking refresh previously timed out Vitest and stuttered the UI path.
  • Permalink:
    "refresh_pr_branch_status" => {
    let repo_path = get_str(&args, "repoPath")?;
    let branch_name = get_str(&args, "branchName")?;
    treq_lib::pr_status::global().watch_repo(&repo_path);
    // Fire-and-forget — same as the Tauri command. Must not block the
    // JS event loop on `gh` (which is what caused test timeouts when
    // a spy was briefly restored to the real invoke).
    treq_lib::pr_status::global().queue_branch_refresh(repo_path, branch_name);
    Ok(Value::Null)

@Ziinc
Ziinc marked this pull request as ready for review August 8, 2026 20:30
@Ziinc
Ziinc merged commit 2daa55b into main Aug 8, 2026
10 checks passed
@Ziinc
Ziinc deleted the cursor/async-pr-ci-polling-b216 branch August 8, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants