Skip to content

fix(sidebar): prefetch chats + workflows so cold loads don't flash skeletons#5104

Merged
waleedlatif1 merged 4 commits into
stagingfrom
worktree-fix-sidebar-skeleton-reload
Jun 17, 2026
Merged

fix(sidebar): prefetch chats + workflows so cold loads don't flash skeletons#5104
waleedlatif1 merged 4 commits into
stagingfrom
worktree-fix-sidebar-skeleton-reload

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • 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 Chats + Workflows lists, flashing loading skeletons and looking like the whole sidebar reloaded.
  • Prefetch both lists server-side in the workspace layout and hydrate them via HydrationBoundary, under the same query keys + mappers the client hooks use, so the sidebar paints populated on the first render.
  • Prefetch runs concurrently with the existing org-settings fetch and prefetchQuery never throws, so it adds no blocking work in the common case and falls back to client fetching on error.

Type of Change

  • Bug fix

Testing

Tested manually. tsc, biome, check:api-validation, and check:react-query all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…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.
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 17, 2026 2:21am

Request Review

@cursor

cursor Bot commented Jun 17, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Mostly read-path refactor and SSR prefetch; auth stays on routes/prefetch guards, with no intentional behavior change beyond faster initial sidebar paint.

Overview
Fixes cold workspace loads (e.g. after the browser discards an idle tab) where the sidebar showed skeletons until client fetches finished.

The workspace layout now prefetches active workflows and mothership chats server-side, hydrates them through HydrationBoundary, and uses the same React Query keys, stale times, and mappers as the sidebar hooks so lists render populated on first paint. Prefetch calls the shared data layer directly (no internal HTTP); it skips when the user lacks workspace access so the client can still surface errors.

Shared query modules were extracted so routes and prefetch stay aligned: listMothershipChats, listWorkflowsForUser, getUserSettings, and getUserProfile. Mothership/workflow list helpers also normalize timestamps to ISO strings on the wire. Settings/profile prefetches in the settings section were switched from cookie-forwarded API fetches to those modules (billing subscription prefetch still goes through the billing API for JSON date shape parity).

Reviewed by Cursor Bugbot for commit 7de9458. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 HydrationBoundary, so the sidebar renders populated on the first paint. It also eliminates duplicate DB query logic across three API routes by extracting shared data-layer functions (listWorkflowsForUser, listMothershipChats, getUserSettings, getUserProfile).

  • New sidebar prefetch (prefetch.ts): calls the shared data layer directly (no internal HTTP hops), verifies workspace access first, and stores results under the exact query keys + mappers the client hooks use — ensuring the hydrated cache hits for the full staleTime.
  • Shared data-layer modules (lib/workflows/queries.ts, lib/copilot/chat/list-mothership-chats.ts, lib/users/queries.ts): extracted from the API route handlers; all three API routes now delegate to these functions, removing ~120 lines of duplicated Drizzle queries.
  • Settings prefetch (settings/[section]/prefetch.ts): migrated from internal HTTP fetches for settings/profile to direct data-layer calls, removing the getForwardedHeaders helper; the billing prefetch intentionally retains the HTTP path due to Date serialization semantics on the wire contract.

Confidence Score: 5/5

Safe 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

Filename Overview
apps/sim/app/workspace/[workspaceId]/prefetch.ts New file implementing server-side sidebar prefetch; correctly gates on workspace access, calls the data layer directly, and uses the same query keys + mappers as the client hooks for a cache hit on hydration.
apps/sim/app/workspace/[workspaceId]/layout.tsx Starts the sidebar prefetch concurrently with getOrgWhitelabelSettings, awaits both before render, and wraps WorkspaceChrome in HydrationBoundary — clean integration of the new prefetch pattern.
apps/sim/lib/workflows/queries.ts New shared data-layer function for workflow listing; extracts and DRYs the Drizzle query from the API route, normalizes timestamps to ISO strings to match the WorkflowListItem wire contract, and adds a clean scopeCondition helper.
apps/sim/lib/copilot/chat/list-mothership-chats.ts New shared function for listing mothership chats; preserves the exact logic (stream marker reconciliation, ISO timestamp normalization) from the old API route handler.
apps/sim/lib/users/queries.ts New shared data-layer module for user settings and profile; exports defaultUserSettings as a named constant, allowing both the API route error handler and data function to reference a single source of truth.
apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts Migrates settings and profile prefetches from internal HTTP fetches to direct data-layer calls; subscription prefetch intentionally retains the HTTP path with a clear comment explaining the Date serialization rationale.
apps/sim/hooks/queries/mothership-chats.ts Exports MOTHERSHIP_CHAT_LIST_STALE_TIME and mapChat so the prefetch can stay in sync with the hook — mirrors the existing WORKFLOW_LIST_STALE_TIME / mapWorkflow pattern.
apps/sim/app/api/mothership/chats/route.ts GET handler now delegates to the shared listMothershipChats function, removing ~30 lines of duplicated Drizzle code while preserving identical behavior.
apps/sim/app/api/workflows/route.ts GET handler delegates to listWorkflowsForUser, removing ~60 lines of duplicated Drizzle query logic with no behavioral change.
apps/sim/app/api/users/me/settings/route.ts GET handler now uses getUserSettings/defaultUserSettings from the shared module; the unauthenticated fast-path and error fallback are preserved with equivalent logic.
apps/sim/app/api/users/me/profile/route.ts GET handler now delegates to getUserProfile from the shared module; no behavioral change.

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
Loading
%%{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
Loading

Reviews (4): Last reviewed commit: "fix(prefetch): keep subscription prefetc..." | Re-trigger Greptile

Comment thread apps/sim/app/workspace/[workspaceId]/prefetch.ts Outdated
Comment thread apps/sim/app/workspace/[workspaceId]/prefetch.ts Outdated
… 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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/prefetch.ts
…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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ 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.

@waleedlatif1 waleedlatif1 merged commit 8353145 into staging Jun 17, 2026
16 checks passed
@waleedlatif1 waleedlatif1 deleted the worktree-fix-sidebar-skeleton-reload branch June 17, 2026 02:28
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ 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.

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