fix(sidebar): prefetch chats + workflows so cold loads don't flash skeletons#5104
Conversation
…eletons On a cold load (e.g. when the browser discards an idle tab and reloads), the persistent sidebar started with an empty React Query cache and client-fetched its chat + workflow lists, flashing loading skeletons. Prefetch both lists server-side in the workspace layout and hydrate them via HydrationBoundary, under the same query keys and mappers the client hooks use, so the sidebar paints populated on the first render. The prefetch runs concurrently with the existing org-settings fetch and never throws, so it adds no blocking work in the common case and falls back to client fetching on error.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryLow Risk Overview The workspace layout now prefetches active workflows and mothership chats server-side, hydrates them through Shared query modules were extracted so routes and prefetch stay aligned: Reviewed by Cursor Bugbot for commit 7de9458. Configure here. |
Greptile SummaryThis PR fixes sidebar cold-load skeleton flashes by prefetching the workspace's workflow and chat lists server-side in the workspace layout and hydrating them via
Confidence Score: 5/5Safe to merge — the change is purely additive server-side prefetching with a clean fallback to client fetching on any error. All three new data-layer modules are direct extractions of the logic that was already running in the API route handlers, so behavior is unchanged for every existing code path. The new prefetch gates on checkWorkspaceAccess before touching the DB, swallows errors silently via prefetchQuery, and runs concurrently with the existing org-settings fetch — no new blocking work is added to the critical render path. Query keys, stale times, and mappers all match what the client hooks use, so the hydrated cache will hit correctly for the full stale window. No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Browser
participant WorkspaceLayout as WorkspaceLayout (Server)
participant PrefetchFn as prefetchWorkspaceSidebar
participant DataLayer as Data Layer (DB)
participant QueryClient as Server QueryClient
participant HydrationBoundary as HydrationBoundary (Client)
participant SidebarHook as useWorkflowList / useMothershipChats
Browser->>WorkspaceLayout: GET /workspace/[id]
WorkspaceLayout->>PrefetchFn: prefetchWorkspaceSidebar(queryClient, workspaceId, userId)
PrefetchFn->>DataLayer: checkWorkspaceAccess(workspaceId, userId)
DataLayer-->>PrefetchFn: "hasAccess=true"
par Concurrent DB calls
PrefetchFn->>DataLayer: "listWorkflowsForUser({ workspaceId, scope:'active' })"
DataLayer-->>PrefetchFn: WorkflowListItem[]
and
PrefetchFn->>DataLayer: listMothershipChats(userId, workspaceId)
DataLayer-->>PrefetchFn: MothershipChat[]
end
PrefetchFn->>QueryClient: prefetchQuery(workflowKeys.list(...))
PrefetchFn->>QueryClient: prefetchQuery(mothershipChatKeys.list(...))
WorkspaceLayout->>HydrationBoundary: "state={dehydrate(queryClient)}"
HydrationBoundary->>SidebarHook: hydrate client cache
Note over SidebarHook: Cache HIT — no skeleton flash
SidebarHook-->>Browser: Sidebar renders populated immediately
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Browser
participant WorkspaceLayout as WorkspaceLayout (Server)
participant PrefetchFn as prefetchWorkspaceSidebar
participant DataLayer as Data Layer (DB)
participant QueryClient as Server QueryClient
participant HydrationBoundary as HydrationBoundary (Client)
participant SidebarHook as useWorkflowList / useMothershipChats
Browser->>WorkspaceLayout: GET /workspace/[id]
WorkspaceLayout->>PrefetchFn: prefetchWorkspaceSidebar(queryClient, workspaceId, userId)
PrefetchFn->>DataLayer: checkWorkspaceAccess(workspaceId, userId)
DataLayer-->>PrefetchFn: "hasAccess=true"
par Concurrent DB calls
PrefetchFn->>DataLayer: "listWorkflowsForUser({ workspaceId, scope:'active' })"
DataLayer-->>PrefetchFn: WorkflowListItem[]
and
PrefetchFn->>DataLayer: listMothershipChats(userId, workspaceId)
DataLayer-->>PrefetchFn: MothershipChat[]
end
PrefetchFn->>QueryClient: prefetchQuery(workflowKeys.list(...))
PrefetchFn->>QueryClient: prefetchQuery(mothershipChatKeys.list(...))
WorkspaceLayout->>HydrationBoundary: "state={dehydrate(queryClient)}"
HydrationBoundary->>SidebarHook: hydrate client cache
Note over SidebarHook: Cache HIT — no skeleton flash
SidebarHook-->>Browser: Sidebar renders populated immediately
Reviews (4): Last reviewed commit: "fix(prefetch): keep subscription prefetc..." | Re-trigger Greptile |
… self-fetch The sidebar and settings prefetches fetched their data by making internal HTTP requests to our own API routes. Replace those self-fetches with direct calls to shared server-side data functions, so each route handler and its prefetch read from one source with no extra network hop, serialization, or re-auth. - Extract listWorkflowsForUser (lib/workflows/queries) and listMothershipChats (lib/copilot/chat) from their routes; both routes and the sidebar prefetch now call them. - Extract getUserSettings/getUserProfile (lib/users/queries) shared by the settings/profile routes and their prefetches. - Subscription prefetch calls the existing getSimplifiedBillingSummary + getEffectiveBillingStatus directly. - Sidebar prefetch checks workspace access once via checkWorkspaceAccess and skips silently when denied.
Export MOTHERSHIP_CHAT_LIST_STALE_TIME from the chats hook and use it in both useMothershipChats and the sidebar prefetch, mirroring WORKFLOW_LIST_STALE_TIME so the prefetch and client hook can't drift.
|
@greptile review |
|
@cursor review |
…nal billing API The billing summary returns Date fields (and an untyped metadata blob) that the JSON API serializes to strings. Calling the data layer directly would cache Date objects (App Router preserves them through RSC serialization), mismatching the string wire shape the client useSubscriptionData hook caches. Route the subscription prefetch through the internal billing API so server-hydrated and client-fetched data share the exact same shape. The date-free general-settings and profile prefetches keep calling the data layer directly.
|
@greptile review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 7de9458. Configure here.
|
@greptile review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 7de9458. Configure here.
Summary
HydrationBoundary, under the same query keys + mappers the client hooks use, so the sidebar paints populated on the first render.prefetchQuerynever throws, so it adds no blocking work in the common case and falls back to client fetching on error.Type of Change
Testing
Tested manually.
tsc,biome,check:api-validation, andcheck:react-queryall pass.Checklist