perf(integrations): local-first settings list so the project page paints instantly#189
Conversation
…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>
|
Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI. |
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
pr-reviewer could not complete review for #189 in AgentWorkforce/pear. |
|
ℹ️ 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. |
There was a problem hiding this comment.
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.
| 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) | ||
| } | ||
| }) |
There was a problem hiding this comment.
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)
}
})|
pr-reviewer could not complete review for #189 in AgentWorkforce/pear. |
|
ℹ️ 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. |
|
pr-reviewer could not complete review for #189 in AgentWorkforce/pear. |
|
ℹ️ 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. |
|
pr-reviewer could not complete review for #189 in AgentWorkforce/pear. |
|
ℹ️ 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. |
|
pr-reviewer could not complete review for #189 in AgentWorkforce/pear. |
|
ℹ️ 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. |
|
pr-reviewer could not complete review for #189 in AgentWorkforce/pear. |
|
ℹ️ 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. |
Problem
On the project page, the connected-integrations panel was slow to load because
listConnectedForSettingsblocked 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.listConnected) but was gated behind that round-trip, so the panel showed a spinner for the whole duration. No local-first render existed (ProjectSettings.tsxsetIntegrations(await pear.integrations.list(...))).Fix — local-first
listConnectedForSettingsreturns the local list immediately and kicks cloud hydration in the background. First paint no longer waits on whoami or the workspace GET.hydrateProjectCloudIntegrations) merges cloud into the store and, only when that changes the stored list, emits a newintegrations-hydratedevent so the renderer swaps in the merged result (the renderer already re-loads on project-scoped integration events).integration-auth-requiredevent (so the sign-in prompt still appears), and generic/transient failures are swallowed because first paint already showed local.PROJECT_CLOUD_HYDRATION_THROTTLE_MS) so the renderer re-rendering onintegrations-hydrated(which re-calls the list) cannot spin a fetch loop.No renderer change needed
ProjectSettingsalready paints whatever the list returns and refreshes on integration events, and itsintegration-auth-requiredhandler 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:npx vitest run src/main/integrations.test.ts→ 30 passed, 2 failed. Those 2 failures (remote Slack reader/joinpath) are pre-existing onmain— verified by stashing this branch — and are fixed separately by theRelayfileSetup.joinWorkspacemock PR (#188).npm test(node:test) → 111 passed.🤖 Generated with Claude Code