Skip to content

feat(desktop): add batch fetching for GitHub PR statuses#2959

Closed
vaishcodescape wants to merge 1 commit into
superset-sh:mainfrom
vaishcodescape:aditya/features
Closed

feat(desktop): add batch fetching for GitHub PR statuses#2959
vaishcodescape wants to merge 1 commit into
superset-sh:mainfrom
vaishcodescape:aditya/features

Conversation

@vaishcodescape
Copy link
Copy Markdown
Contributor

@vaishcodescape vaishcodescape commented Mar 27, 2026

Description

  • Introduced batchGetGitHubStatuses procedure to efficiently retrieve statuses for multiple workspaces.
  • Enhanced GitHub utilities with batchFetchGitHubPRStatuses for improved performance.
  • Updated workspace-related components to utilize the new batch fetching mechanism.
  • Adjusted query policies to align with the new batch polling interval for workspace list items.
  • Refactored related types and functions to support the new batch processing capabilities.

Related Issues

Closes PR #2958

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

  • batch-pr-status.test.ts — 10 test cases covering:
  • GHGraphQLPRNodeSchema parsing (full node, minimal node, rejects missing __typename)
  • normalizeGraphQLPR conversion (basic fields, statusCheckRollup flattening, reviewRequests normalization, null handling, draft/merged states, null context filtering)
  • Schema compatibility — validates normalizeGraphQLPR output passes GHPRResponseSchema.safeParse(), ensuring the GraphQL→CLI shape translation is lossless
  • githubQueryPolicy.test.ts — updated assertions for the new workspace-list-item stale time (15s aligned with batch interval)

Screenshots (if applicable)

Additional Notes


Summary by cubic

Adds batch fetching for GitHub PR statuses in the desktop app to replace N+1 lookups with a single GraphQL request. Improves sidebar responsiveness and aligns polling to a 15s interval.

  • New Features

    • Added batchGetGitHubStatuses to return a map of workspace IDs to GitHubStatus, persisting meaningful changes to local DB.
    • Implemented batchFetchGitHubPRStatuses that groups entries by repo and runs one GraphQL query via gh, then normalizes results.
    • Introduced useBatchGitHubStatus to fetch once at the sidebar level and seed per-workspace caches without overwriting richer data.
    • Workspace list items and rows no longer rely on hover to fetch; they use the batch results by default.
  • Refactors

    • Exported sortPRCandidates and formatPRData; added GraphQL schemas and normalizeGraphQLPR.
    • Set BATCH_GITHUB_STATUS_REFETCH_INTERVAL_MS to 15s and aligned workspace list item stale time.
    • Added tests for GraphQL PR normalization and query policy.

Written for commit 1c29b5b. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Introduced batch fetching of GitHub PR status for all workspaces, improving performance and reducing network overhead
  • Bug Fixes

    • GitHub status queries for worktree-type workspaces are now always enabled instead of hover-dependent, ensuring consistent and timely updates

- Introduced `batchGetGitHubStatuses` procedure to efficiently retrieve statuses for multiple workspaces.
- Enhanced GitHub utilities with `batchFetchGitHubPRStatuses` for improved performance.
- Updated workspace-related components to utilize the new batch fetching mechanism.
- Adjusted query policies to align with the new batch polling interval for workspace list items.
- Refactored related types and functions to support the new batch processing capabilities.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a batch GitHub PR status fetching system for workspaces. Instead of individual per-workspace queries, it implements a new TRPC procedure that fetches GitHub statuses for all workspaces in a single GraphQL query, with caching and normalization logic, plus a React hook to invoke the batch operation periodically while preserving existing per-workspace cache data.

Changes

Cohort / File(s) Summary
Batch GitHub Status Fetching
apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/github/batch-pr-status.ts
Added batchGetGitHubStatuses TRPC query procedure and batchFetchGitHubPRStatuses utility function. Fetches statuses for all non-deleted workspaces via single aliased GraphQL query, validates/normalizes results, filters matching PRs, and conditionally updates local database with meaningful status changes.
GraphQL PR Schema & Normalization
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts
Added GHGraphQLPRNodeSchema Zod schema for validating GraphQL PR nodes with discriminated unions for check runs and status contexts. Added normalizeGraphQLPR function to convert GraphQL PR structure into existing GHPRResponse format, flattening status checks and transforming review requests.
Test Coverage & Helper Exports
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/batch-pr-status.test.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/github/pr-resolution.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/github/index.ts
Added comprehensive test suite for GraphQL PR parsing, normalization, and schema validation (361 lines). Exported previously internal functions sortPRCandidates and formatPRData. Re-exported new batch functions and BatchWorkspaceEntry type.
React Hook & Query Policy
apps/desktop/src/renderer/hooks/useBatchGitHubStatus.ts, apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts, apps/desktop/src/renderer/lib/githubQueryPolicy/index.ts, apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts
Added useBatchGitHubStatus hook that periodically invokes batch query and seeds workspace cache only when empty. Introduced BATCH_GITHUB_STATUS_REFETCH_INTERVAL_MS constant (15s), unified workspace-list-item stale time to use this interval.
UI Component Updates
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx, apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx
Invoked useBatchGitHubStatus in sidebar. Removed hover-dependency (hasHovered) from GitHub status query activation in list item and row components, making queries active based on workspace type only.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer as Renderer<br/>(useBatchGitHubStatus)
    participant tRPC as tRPC<br/>(batchGetGitHubStatuses)
    participant BatchUtil as Batch Utility<br/>(batchFetchGitHubPRStatuses)
    participant GraphQL as GitHub<br/>GraphQL API
    participant LocalDB as Local DB<br/>(cache/update)
    participant Cache as React Query<br/>Cache

    Renderer->>tRPC: periodic query (every 15s)
    activate tRPC
    tRPC->>tRPC: load all workspaces & worktrees
    tRPC->>tRPC: build (workspaceId, path, branch) entries
    tRPC->>BatchUtil: fetch statuses for entries
    activate BatchUtil
    BatchUtil->>BatchUtil: group by repo, deduplicate branches
    BatchUtil->>BatchUtil: build aliased GraphQL query
    BatchUtil->>GraphQL: execute batch query (30s timeout)
    GraphQL-->>BatchUtil: PR nodes per alias
    BatchUtil->>BatchUtil: validate with GHGraphQLPRNodeSchema
    BatchUtil->>BatchUtil: normalize GraphQL→GHPRResponse
    BatchUtil->>BatchUtil: filter matches, select best PR
    BatchUtil->>LocalDB: cache status (when changed)
    BatchUtil-->>tRPC: Map<workspaceId, GitHubStatus>
    deactivate BatchUtil
    tRPC->>LocalDB: update worktree githubStatus (if changed)
    tRPC-->>Renderer: batch result
    deactivate tRPC
    Renderer->>Cache: seed workspace cache only if empty
    Cache-->>Renderer: cache updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A hop and a batch, statuses dance,
GraphQL queries in one grand prance,
No hover delay, no fetching alone,
All workspaces checked with a periodic tone!
Cache seeding soft, the cache stays clear,
Status updates fresh every fifteen here! 🚀

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers key changes and includes a completed type of change selection, but testing details are incomplete and lack clarity on verification steps. Clarify and expand the Testing section with specific steps to verify the batch fetching works correctly and improve overall formatting for readability.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding batch fetching functionality for GitHub PR statuses, which is the core objective of this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 13 files

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx (1)

39-39: hasHovered state appears to be unused after this change.

The hasHovered state and its setHasHovered call at line 72 no longer gate any behavior since isActive in the query policy (line 44) no longer depends on it. Consider removing this dead state.

♻️ Suggested cleanup
-const [hasHovered, setHasHovered] = useState(false);
 ...
-onMouseEnter={() => !hasHovered && setHasHovered(true)}
+onMouseEnter={() => {}}

Or remove the onMouseEnter handler entirely if no other hover behavior is needed.

Also applies to: 72-72

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx`
at line 39, Remove the dead hover state by deleting the hasHovered/useState
declaration and any calls to setHasHovered (e.g., the onMouseEnter handler
inside the WorkspaceRow component); since isActive no longer depends on
hasHovered, eliminate the unused state and the onMouseEnter prop so no
hover-related state is kept in WorkspaceRow.
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts (1)

328-339: Consider filtering out null reviewers instead of returning empty objects.

When reviewer is null at line 330, returning an empty object {} may introduce entries that don't match GHReviewRequestSchema expectations (which has optional fields but expects meaningful data). Downstream code iterating over reviewRequests might encounter unexpected empty objects.

♻️ Suggested improvement
 const reviewRequests: GHPRResponse["reviewRequests"] =
   node.reviewRequests?.nodes
     ?.filter((rr): rr is NonNullable<typeof rr> => rr !== null)
+    .filter((rr) => rr.requestedReviewer !== null)
     .map((rr) => {
       const reviewer = rr.requestedReviewer;
-      if (!reviewer) return {};
+      // reviewer is guaranteed non-null after filter
       if (reviewer.__typename === "User") {
         return { login: reviewer.login, type: "User" as const };
       }
       return {
         slug: reviewer.slug,
         name: reviewer.name,
         type: "Team" as const,
       };
     }) ?? null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts` around
lines 328 - 339, The map that transforms rr.requestedReviewer (in the
reviewRequests creation) returns {} when reviewer is falsy which can produce
invalid empty objects; instead filter out falsy reviewers before mapping (or use
flatMap to skip nulls) so only real User or Team objects are returned; update
the mapping that references rr.requestedReviewer to either pre-filter
rr.requestedReviewer != null or return null and then .filter(Boolean) so the
resulting array conforms to GHReviewRequestSchema and contains only valid
reviewer objects.
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/batch-pr-status.ts (2)

282-293: branchExistsOnRemote: true may be inaccurate.

Both the "PR found" and "no PR" code paths hardcode branchExistsOnRemote: true. This doesn't reflect actual remote branch existence—the batch query only checks for PRs by head branch name, not whether the branch itself exists on the remote.

For workspaces with no PR, the branch may or may not exist on the remote. Consider either:

  1. Omitting branchExistsOnRemote from batch results (let per-workspace queries populate it accurately)
  2. Adding a separate check for branch existence

Also applies to: 301-311

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/github/batch-pr-status.ts`
around lines 282 - 293, The code is incorrectly hardcoding branchExistsOnRemote:
true in the GitHubStatus objects (in the batch PR path inside batch-pr-status
and the corresponding no-PR branch around lines 301-311), which misrepresents
remote branch existence; remove the hardcoded branchExistsOnRemote from the
GitHubStatus construction (both the "PR found" and "no PR" branches) so the
batch result does not claim the branch exists, and instead let per-workspace
queries populate branchExistsOnRemote accurately (update the GitHubStatus
type/nullable handling if necessary so setCachedGitHubStatus and results.set
accept a missing/undefined branchExistsOnRemote).

317-319: Consider escaping additional control characters in GraphQL strings.

The current escape function handles \ and ", which prevents basic injection. However, GraphQL string literals also interpret \n, \r, \t, and other escape sequences. If a branch name contains a literal newline or carriage return (unusual but possible), it could produce malformed queries.

More complete escape function
 function escapeGraphQLString(value: string): string {
-	return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
+	return value
+		.replace(/\\/g, "\\\\")
+		.replace(/"/g, '\\"')
+		.replace(/\n/g, "\\n")
+		.replace(/\r/g, "\\r")
+		.replace(/\t/g, "\\t");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/github/batch-pr-status.ts`
around lines 317 - 319, The escapeGraphQLString function only handles
backslashes and double quotes but must also escape control characters (e.g.,
newline \n, carriage return \r, tab \t, backspace \b, form feed \f and other
non-printables) to avoid producing malformed GraphQL string literals; update
escapeGraphQLString to replace those characters with their corresponding escaped
sequences (e.g., "\n" -> "\\n", "\r" -> "\\r", "\t" -> "\\t", "\b" -> "\\b",
"\f" -> "\\f") or use a robust serializer approach (e.g., via JSON.stringify on
the value and strip surrounding quotes) to ensure all control/Unicode characters
are safely encoded when building GraphQL queries that include branch names or
other user data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apps/desktop/src/lib/trpc/routers/workspaces/utils/github/batch-pr-status.ts`:
- Around line 282-293: The code is incorrectly hardcoding branchExistsOnRemote:
true in the GitHubStatus objects (in the batch PR path inside batch-pr-status
and the corresponding no-PR branch around lines 301-311), which misrepresents
remote branch existence; remove the hardcoded branchExistsOnRemote from the
GitHubStatus construction (both the "PR found" and "no PR" branches) so the
batch result does not claim the branch exists, and instead let per-workspace
queries populate branchExistsOnRemote accurately (update the GitHubStatus
type/nullable handling if necessary so setCachedGitHubStatus and results.set
accept a missing/undefined branchExistsOnRemote).
- Around line 317-319: The escapeGraphQLString function only handles backslashes
and double quotes but must also escape control characters (e.g., newline \n,
carriage return \r, tab \t, backspace \b, form feed \f and other non-printables)
to avoid producing malformed GraphQL string literals; update escapeGraphQLString
to replace those characters with their corresponding escaped sequences (e.g.,
"\n" -> "\\n", "\r" -> "\\r", "\t" -> "\\t", "\b" -> "\\b", "\f" -> "\\f") or
use a robust serializer approach (e.g., via JSON.stringify on the value and
strip surrounding quotes) to ensure all control/Unicode characters are safely
encoded when building GraphQL queries that include branch names or other user
data.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts`:
- Around line 328-339: The map that transforms rr.requestedReviewer (in the
reviewRequests creation) returns {} when reviewer is falsy which can produce
invalid empty objects; instead filter out falsy reviewers before mapping (or use
flatMap to skip nulls) so only real User or Team objects are returned; update
the mapping that references rr.requestedReviewer to either pre-filter
rr.requestedReviewer != null or return null and then .filter(Boolean) so the
resulting array conforms to GHReviewRequestSchema and contains only valid
reviewer objects.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx`:
- Line 39: Remove the dead hover state by deleting the hasHovered/useState
declaration and any calls to setHasHovered (e.g., the onMouseEnter handler
inside the WorkspaceRow component); since isActive no longer depends on
hasHovered, eliminate the unused state and the onMouseEnter prop so no
hover-related state is kept in WorkspaceRow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e492b70-dd08-46b7-a43f-9b2b4b1b5137

📥 Commits

Reviewing files that changed from the base of the PR and between 653cb3a and 1c29b5b.

📒 Files selected for processing (13)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/batch-pr-status.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/batch-pr-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/pr-resolution.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts
  • apps/desktop/src/renderer/hooks/useBatchGitHubStatus.ts
  • apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts
  • apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts
  • apps/desktop/src/renderer/lib/githubQueryPolicy/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx

@vaishcodescape
Copy link
Copy Markdown
Contributor Author

vaishcodescape commented Mar 28, 2026

@saddlepaddle Can you please review this PR I believe the checks are passing correctly ?

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.

1 participant