Skip to content

perf(integrations): local-first settings list so the project page paints instantly#189

Merged
kjgbot merged 1 commit into
mainfrom
perf/integrations-local-first-load
Jun 9, 2026
Merged

perf(integrations): local-first settings list so the project page paints instantly#189
kjgbot merged 1 commit into
mainfrom
perf/integrations-local-first-load

Conversation

@kjgbot

@kjgbot kjgbot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Problem

On the project page, the connected-integrations panel was slow to load because listConnectedForSettings blocked first paint on the full cloud hydration:

  • getAccountWorkspaceId (whoami — cached when warm, but a fresh whoami is ~3.1s on a cold cache, with up to 8× retries) and the workspace-integrations GET ran sequentially, and the method threw on cloud failure.
  • The on-disk list was already available (listConnected) but was gated behind that round-trip, so the panel showed a spinner for the whole duration. No local-first render existed (ProjectSettings.tsx setIntegrations(await pear.integrations.list(...))).

Fix — local-first

  • listConnectedForSettings returns the local list immediately and kicks cloud hydration in the background. First paint no longer waits on whoami or the workspace GET.
  • Background hydration (hydrateProjectCloudIntegrations) merges cloud into the store and, only when that changes the stored list, emits a new integrations-hydrated event so the renderer swaps in the merged result (the renderer already re-loads on project-scoped integration events).
  • Cloud failures no longer throw: auth/workspace failures raise the recovery banner through the existing integration-auth-required event (so the sign-in prompt still appears), and generic/transient failures are swallowed because first paint already showed local.
  • Per-project hydration is deduped + throttled (PROJECT_CLOUD_HYDRATION_THROTTLE_MS) so the renderer re-rendering on integrations-hydrated (which re-calls the list) cannot spin a fetch loop.

No renderer change needed

ProjectSettings already paints whatever the list returns and refreshes on integration events, and its integration-auth-required handler already shows the sign-in / workspace panel. So the end state is preserved — reached after an instant local paint instead of a multi-second blocking spinner — without touching renderer code.

Deferred

The "parallelize listCatalog() + getAccountWorkspaceId()" micro-optimization is intentionally not included: it raced the shared fetch mock and the benefit is marginal now that first paint is instant and the catalog is cached in steady state. Can be revisited if profiling shows the background merge itself is slow.

Tests

Added/updated in integrations.test.ts:

  • first paint returns the local list while the cloud call is gated/pending (proves it doesn't wait on cloud);
  • the cloud-auth and 403 paths raise the recovery banner via background hydration instead of throwing;
  • a generic (non-auth) background failure is swallowed with no banner;
  • the existing account-workspace-required path still returns local + sets recovery (now deterministically awaited).

npx vitest run src/main/integrations.test.ts → 30 passed, 2 failed. Those 2 failures (remote Slack reader /join path) are pre-existing on main — verified by stashing this branch — and are fixed separately by the RelayfileSetup.joinWorkspace mock PR (#188). npm test (node:test) → 111 passed.

🤖 Generated with Claude Code

…nts instantly

Loading the connected-integrations list on the project page awaited the full
cloud hydration before painting anything: listConnectedForSettings ran
getAccountWorkspaceId (whoami — cached when warm, but a fresh whoami is ~3.1s
on a cold cache and retries up to 8x) and the workspace-integrations GET
sequentially, and threw on cloud failure. The on-disk list was already
available but gated behind that round-trip, so the panel showed a spinner for
the whole duration.

Make it local-first:
- listConnectedForSettings returns the local list immediately and kicks cloud
  hydration in the background. First paint no longer waits on whoami or the
  workspace GET.
- Background hydration merges cloud into the store and, only when that changes
  the stored list, emits a new `integrations-hydrated` event so the renderer
  swaps in the merged result (it already re-loads on project-scoped events).
- Cloud failures no longer throw: auth/workspace failures raise the recovery
  banner through the existing `integration-auth-required` event (so the
  sign-in prompt still appears), and generic/transient failures are swallowed
  because first paint already showed the local list.
- Per-project hydration is deduped + throttled so the renderer re-rendering on
  `integrations-hydrated` (which re-calls the list) cannot spin a fetch loop.

No renderer change is needed: ProjectSettings already paints whatever the list
returns and refreshes on integration events, and the auth-required event
handler still shows the sign-in panel — so the end state is preserved, reached
after an instant local paint instead of a multi-second blocking spinner.

Tests: first paint returns local while the cloud call is gated/pending; the
cloud-auth and 403 paths raise the banner via background hydration instead of
throwing; a generic background failure is swallowed with no banner; the
account-workspace-required path still returns local + sets recovery. The 2
remaining failures in integrations.test.ts (remote Slack reader /join path) are
pre-existing on main and fixed separately by the RelayfileSetup.joinWorkspace
mock PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codeant-ai

codeant-ai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@kjgbot, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 7 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c5fbf479-3211-49f5-b2d7-304cb5d94c1d

📥 Commits

Reviewing files that changed from the base of the PR and between 4e41f8a and 2561765.

📒 Files selected for processing (3)
  • src/main/integrations.test.ts
  • src/main/integrations.ts
  • src/shared/types/ipc.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/integrations-local-first-load

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.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a local-first approach for loading project integrations in the IntegrationsManager. Instead of waiting for cloud workspace integrations to resolve, which blocks first paint, listConnectedForSettings now returns the local list immediately and triggers background cloud hydration (hydrateProjectCloudIntegrations). If the background hydration updates the list, an integrations-hydrated event is emitted. Additionally, cloud failures are handled gracefully by setting the auth recovery state rather than throwing errors. The review feedback suggests optimizing the background hydration by returning the updated integrations list directly from the async IIFE to avoid an extra synchronous disk read, and handling HTTP 401 errors in the catch block to ensure expired or invalid sessions are properly surfaced via the cloud-auth-required banner.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/main/integrations.ts
Comment on lines +1001 to +1034
const before = JSON.stringify(this.listConnected(projectId))
const pending = (async (): Promise<void> => {
const cloud = await this.listCloudWorkspaceIntegrations()
if (cloud.length === 0) {
console.log(`[integrations] listConnectedForSettings: cloud returned 0 integrations for workspace; using local list only (${local.length} items)`)
return this.withLocalMountPaths(local)
}
return this.withLocalMountPaths(await this.mergeCloudIntegrationsIntoProject(projectId, cloud))
} catch (error) {
if (isAccountWorkspaceRequiredError(error)) {
console.warn(
'[integrations] Account workspace unavailable; integration recovery is required'
)
const failureClass = integrationAuthRecoveryFailureClass(error)
this.setAuthRecoveryState('account-workspace-required', failureClass)
const decorated = await this.withLocalMountPaths(local)
this.setAuthRecoveryState('account-workspace-required', failureClass)
return decorated
}
if (isCloudAuthRequiredError(error)) throw error
if (isHttpStatus(error, 403)) {
this.setAuthRecoveryState('cloud-auth-required', 'workspace-access-revoked')
throw new Error('cloud-auth-required:workspace-access-revoked')
console.log(`[integrations] hydrateProjectCloudIntegrations: cloud returned 0 integrations; keeping local list for ${projectId}`)
return
}
await this.mergeCloudIntegrationsIntoProject(projectId, cloud)
})()
.then(() => {
const after = JSON.stringify(this.listConnected(projectId))
if (after !== before) this.emit({ type: 'integrations-hydrated', projectId })
})
.catch((error) => {
if (isAccountWorkspaceRequiredError(error)) {
console.warn('[integrations] Account workspace unavailable; integration recovery is required')
this.setAuthRecoveryState('account-workspace-required', integrationAuthRecoveryFailureClass(error))
return
}
if (isCloudAuthRequiredError(error)) {
this.setAuthRecoveryState('cloud-auth-required', integrationAuthRecoveryFailureClass(error))
return
}
if (isHttpStatus(error, 403)) {
this.setAuthRecoveryState('cloud-auth-required', 'workspace-access-revoked')
return
}
console.warn('[integrations] Background cloud hydration failed:', toErrorMessage(error))
})
.finally(() => {
if (this.projectCloudHydrationPromises.get(projectId) === pending) {
this.projectCloudHydrationPromises.delete(projectId)
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

We can optimize the background hydration by returning the updated integrations list directly from the async IIFE, avoiding an extra synchronous disk read (loadStore()) in the .then() block. Additionally, we should handle HTTP 401 (Unauthorized) errors in the .catch() block to raise the cloud-auth-required banner, ensuring that expired or invalid sessions are properly surfaced to the user rather than failing silently.

    const before = JSON.stringify(this.listConnected(projectId))
    const pending = (async (): Promise<ConnectedIntegration[]> => {
      const cloud = await this.listCloudWorkspaceIntegrations()
      if (cloud.length === 0) {
        console.log("[integrations] hydrateProjectCloudIntegrations: cloud returned 0 integrations; keeping local list for " + projectId)
        return this.listConnected(projectId)
      }
      return this.mergeCloudIntegrationsIntoProject(projectId, cloud)
    })()
      .then((afterList) => {
        const after = JSON.stringify(afterList)
        if (after !== before) this.emit({ type: 'integrations-hydrated', projectId })
      })
      .catch((error) => {
        if (isAccountWorkspaceRequiredError(error)) {
          console.warn('[integrations] Account workspace unavailable; integration recovery is required')
          this.setAuthRecoveryState('account-workspace-required', integrationAuthRecoveryFailureClass(error))
          return
        }
        if (isCloudAuthRequiredError(error)) {
          this.setAuthRecoveryState('cloud-auth-required', integrationAuthRecoveryFailureClass(error))
          return
        }
        if (isHttpStatus(error, 401) || isHttpStatus(error, 403)) {
          this.setAuthRecoveryState(
            'cloud-auth-required',
            isHttpStatus(error, 403) ? 'workspace-access-revoked' : integrationAuthRecoveryFailureClass(error)
          )
          return
        }
        console.warn('[integrations] Background cloud hydration failed:', toErrorMessage(error))
      })
      .finally(() => {
        if (this.projectCloudHydrationPromises.get(projectId) === pending) {
          this.projectCloudHydrationPromises.delete(projectId)
        }
      })

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #189 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@kjgbot kjgbot merged commit c67ca6a into main Jun 9, 2026
4 checks passed
@kjgbot kjgbot deleted the perf/integrations-local-first-load branch June 9, 2026 18:17
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