@@ -25,10 +25,7 @@ import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
2525import { mapWorkflow , WORKFLOW_LIST_STALE_TIME } from '@/hooks/queries/utils/workflow-list-query'
2626import { normalizeWorkspacesResponse } from '@/hooks/queries/utils/workspace-list-query'
2727import { WORKSPACE_PERMISSIONS_STALE_TIME , workspaceKeys } from '@/hooks/queries/workspace'
28- import {
29- WORKSPACE_FILES_LIST_STALE_TIME ,
30- workspaceFilesKeys ,
31- } from '@/hooks/queries/workspace-files'
28+ import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
3229import {
3330 WORKSPACE_HOST_CONTEXT_STALE_TIME ,
3431 workspaceHostKeys ,
@@ -95,6 +92,56 @@ async function seedWorkspaceList(
9592 }
9693}
9794
95+ /**
96+ * How many files the layout is willing to inline into the document.
97+ *
98+ * The file list is seeded on EVERY workspace route (see the call site), so its cost is
99+ * paid per navigation into the app, not per visit to Files. At roughly 500 bytes of
100+ * serialized JSON per file, this budgets the entry at ~150 KB; a workspace with
101+ * thousands of files would otherwise push more than a megabyte of HTML ahead of first
102+ * paint on the logs, settings, and editor routes that never read it.
103+ *
104+ * The read is capped at one row past the budget purely to detect the overflow. A
105+ * workspace above it seeds NOTHING rather than a prefix: the sidebar search filters this
106+ * list client-side and the Files browser renders it as the workspace's files, so a
107+ * truncated seed would silently hide files. Those workspaces fetch the complete list
108+ * from the route instead — which for a list that large is also the cheaper first paint.
109+ */
110+ export const WORKSPACE_FILE_SEED_MAX = 300
111+
112+ /**
113+ * Seeds the workspace's file list, which the sidebar's search modal reads on EVERY
114+ * workspace route — so this query is registered by sidebar chrome before any page
115+ * renders. That ordering is why it has to be seeded HERE and not only by the Files
116+ * pages: `HydrationBoundary` hydrates a query the cache has already seen from a
117+ * `useEffect`, which never runs during SSR, so a page-level boundary can only ever hand
118+ * this entry to the client. Seeding it with the layout's own boundary — the first one to
119+ * render — is what lets the server paint the Files browser and the open file's header
120+ * populated instead of shipping a spinner and resolving it a beat later on the client.
121+ *
122+ * Seeded rather than prefetched so it can decline to create an entry at all when the
123+ * workspace exceeds {@link WORKSPACE_FILE_SEED_MAX}: `prefetchQuery` always creates one,
124+ * and a partial one would be read as the whole list.
125+ *
126+ * The shape comes from the same contract-parsed reader `GET /api/workspaces/[id]/files`
127+ * responds with, so a seeded entry is identical to what the client hook would cache.
128+ */
129+ async function seedWorkspaceFiles ( queryClient : QueryClient , workspaceId : string ) : Promise < void > {
130+ try {
131+ const files = await listWorkspaceFilesWithShares ( workspaceId , 'active' , {
132+ limit : WORKSPACE_FILE_SEED_MAX + 1 ,
133+ } )
134+ if ( files . length > WORKSPACE_FILE_SEED_MAX ) return
135+ queryClient . setQueryData ( workspaceFilesKeys . list ( workspaceId , 'active' ) , files )
136+ } catch ( error ) {
137+ /** Optimization only: the client fetch reaches the route instead. Logged so drift between
138+ * this read and the contract's response schema doesn't degrade silently into a waterfall. */
139+ logger . warn ( 'Workspace file list seed failed; client will fetch' , {
140+ error : getErrorMessage ( error ) ,
141+ } )
142+ }
143+ }
144+
98145/**
99146 * Prefetches the sidebar's workflow, chat, folder, workspace-permissions,
100147 * workspace, and viewer-profile reads for a workspace and stores them under the
@@ -153,22 +200,7 @@ export async function prefetchWorkspaceSidebar(
153200 ]
154201 : [ ] ) ,
155202 prefetchResourceFolders ( queryClient , workspaceId , 'workflow' , userId ) ,
156- /**
157- * The sidebar reads the workspace's files for its search modal, on EVERY workspace route — so this
158- * query is registered by sidebar chrome before any page renders. That ordering is why it has to be
159- * prefetched HERE and not only by the Files pages: `HydrationBoundary` hydrates a query the cache
160- * has already seen from a `useEffect`, which never runs during SSR, so a page-level boundary can
161- * only ever hand this entry to the client. Seeding it with the layout's own boundary — the first
162- * one to render — is what lets the server paint the Files browser and the open file's header
163- * populated instead of shipping a spinner and resolving it a beat later on the client.
164- *
165- * Same key + shape as {@link prefetchFilesBrowser}, so whichever runs is a no-op for the other.
166- */
167- queryClient . prefetchQuery ( {
168- queryKey : workspaceFilesKeys . list ( workspaceId , 'active' ) ,
169- queryFn : ( ) => listWorkspaceFilesWithShares ( workspaceId , 'active' ) ,
170- staleTime : WORKSPACE_FILES_LIST_STALE_TIME ,
171- } ) ,
203+ seedWorkspaceFiles ( queryClient , workspaceId ) ,
172204 queryClient . prefetchQuery ( {
173205 queryKey : workspaceKeys . permissions ( workspaceId ) ,
174206 queryFn : ( ) =>
0 commit comments