@@ -42,17 +42,41 @@ export interface AddResourceDropdownProps {
4242 existingKeys : Set < string >
4343 onAdd : ( resource : MothershipResource ) => void
4444 onSwitch ?: ( resourceId : string ) => void
45- /** Resource types to hide from the dropdown (e.g. `['folder', 'task']`). */
45+ /**
46+ * Resource types to hide from the dropdown. Must be referentially stable
47+ * (a module constant) — it keys the underlying group memo.
48+ */
4649 excludeTypes ?: readonly MothershipResourceType [ ]
4750}
4851
49- export type AvailableItem = { id : string ; name : string ; isOpen ?: boolean ; [ key : string ] : unknown }
52+ export type AvailableItem = { id : string ; name : string ; [ key : string ] : unknown }
5053
5154interface AvailableItemsByType {
5255 type : MothershipResourceType
5356 items : AvailableItem [ ]
5457}
5558
59+ interface UseAvailableResourcesOptions {
60+ /**
61+ * Skips the underlying list queries and the group construction they feed
62+ * while `false`, returning a stable empty result. Menus pass their own open
63+ * state so a closed one costs nothing; the lists fetch on first open.
64+ *
65+ * Note this only defers the lists nothing else on the surface already needs —
66+ * the mothership tab bar independently resolves tab names from the workflow,
67+ * table, file, knowledge-base, and folder lists, so those stay warm there.
68+ */
69+ enabled ?: boolean
70+ /**
71+ * Resource types to omit from the result. Must be referentially stable
72+ * (a module constant) — it keys the group memo.
73+ */
74+ excludeTypes ?: readonly MothershipResourceType [ ]
75+ }
76+
77+ /** Stable identity for the disabled result, so downstream memos never bust. */
78+ const NO_RESOURCE_GROUPS : AvailableItemsByType [ ] = [ ]
79+
5680const LOG_DROPDOWN_LIMIT = 50
5781
5882const LOG_DROPDOWN_FILTERS = {
@@ -69,76 +93,65 @@ const LOG_DROPDOWN_FILTERS = {
6993
7094export function useAvailableResources (
7195 workspaceId : string ,
72- existingKeys : Set < string > ,
73- excludeTypes ?: readonly MothershipResourceType [ ]
96+ options ?: UseAvailableResourcesOptions
7497) : AvailableItemsByType [ ] {
75- const { data : workflows = [ ] } = useWorkflows ( workspaceId )
76- const { data : tables = [ ] } = useTablesList ( workspaceId )
77- const { data : files = [ ] } = useWorkspaceFiles ( workspaceId )
78- const { data : knowledgeBases } = useKnowledgeBasesQuery ( workspaceId )
79- const { data : folders = [ ] } = useFolders ( workspaceId )
80- const { data : fileFolders = [ ] } = useWorkspaceFileFolders ( workspaceId )
81- const { data : tasks = [ ] } = useMothershipChats ( workspaceId )
82- const { data : schedules = [ ] } = useWorkspaceSchedules ( workspaceId )
83- const { data : logsData } = useLogsList ( workspaceId , LOG_DROPDOWN_FILTERS )
98+ const enabled = options ?. enabled ?? true
99+ const excludeTypes = options ?. excludeTypes
100+ // Destructured without `= []` defaults on purpose: a literal default allocates a
101+ // fresh array every render while `data` is undefined (exactly the disabled state),
102+ // which would bust the group memo below on every render. Undefined is stable.
103+ const { data : workflows } = useWorkflows ( workspaceId , { enabled } )
104+ const { data : tables } = useTablesList ( workspaceId , 'active' , { enabled } )
105+ const { data : files } = useWorkspaceFiles ( workspaceId , 'active' , { enabled } )
106+ const { data : knowledgeBases } = useKnowledgeBasesQuery ( workspaceId , { enabled } )
107+ const { data : folders } = useFolders ( workspaceId , { enabled } )
108+ const { data : fileFolders } = useWorkspaceFileFolders ( workspaceId , 'active' , { enabled } )
109+ const { data : tasks } = useMothershipChats ( workspaceId , { enabled } )
110+ const { data : schedules } = useWorkspaceSchedules ( workspaceId , { enabled } )
111+ const { data : logsData } = useLogsList ( workspaceId , LOG_DROPDOWN_FILTERS , { enabled } )
84112 const logs = useMemo ( ( ) => ( logsData ?. pages ?? [ ] ) . flatMap ( ( page ) => page . logs ) , [ logsData ] )
85113
86114 return useMemo ( ( ) => {
115+ if ( ! enabled ) return NO_RESOURCE_GROUPS
87116 const excluded = new Set < MothershipResourceType > ( excludeTypes ?? [ ] )
88117 const groups : AvailableItemsByType [ ] = [
89118 {
90119 type : 'workflow' as const ,
91- items : workflows . map ( ( w ) => ( {
120+ items : ( workflows ?? [ ] ) . map ( ( w ) => ( {
92121 id : w . id ,
93122 name : w . name ,
94123 folderId : w . folderId ?? null ,
95124 sortOrder : w . sortOrder ,
96- isOpen : existingKeys . has ( `workflow:${ w . id } ` ) ,
97125 } ) ) ,
98126 } ,
99127 {
100128 type : 'folder' as const ,
101- items : folders . map ( ( f ) => ( {
129+ items : ( folders ?? [ ] ) . map ( ( f ) => ( {
102130 id : f . id ,
103131 name : f . name ,
104132 parentId : f . parentId ?? null ,
105133 sortOrder : f . sortOrder ,
106- isOpen : existingKeys . has ( `folder:${ f . id } ` ) ,
107134 } ) ) ,
108135 } ,
109136 {
110137 type : 'table' as const ,
111- items : tables . map ( ( t ) => ( {
112- id : t . id ,
113- name : t . name ,
114- isOpen : existingKeys . has ( `table:${ t . id } ` ) ,
115- } ) ) ,
138+ items : ( tables ?? [ ] ) . map ( ( t ) => ( { id : t . id , name : t . name } ) ) ,
116139 } ,
117140 {
118141 type : 'file' as const ,
119- items : files . map ( ( f ) => ( {
120- id : f . id ,
121- name : f . name ,
122- folderId : f . folderId ?? null ,
123- isOpen : existingKeys . has ( `file:${ f . id } ` ) ,
124- } ) ) ,
142+ items : ( files ?? [ ] ) . map ( ( f ) => ( { id : f . id , name : f . name , folderId : f . folderId ?? null } ) ) ,
125143 } ,
126144 {
127145 type : 'filefolder' as const ,
128- items : fileFolders . map ( ( f ) => ( {
146+ items : ( fileFolders ?? [ ] ) . map ( ( f ) => ( {
129147 id : f . id ,
130148 name : f . name ,
131149 parentId : f . parentId ?? null ,
132- isOpen : existingKeys . has ( `filefolder:${ f . id } ` ) ,
133150 } ) ) ,
134151 } ,
135152 {
136153 type : 'knowledgebase' as const ,
137- items : ( knowledgeBases ?? [ ] ) . map ( ( kb ) => ( {
138- id : kb . id ,
139- name : kb . name ,
140- isOpen : existingKeys . has ( `knowledgebase:${ kb . id } ` ) ,
141- } ) ) ,
154+ items : ( knowledgeBases ?? [ ] ) . map ( ( kb ) => ( { id : kb . id , name : kb . name } ) ) ,
142155 } ,
143156 {
144157 type : 'integration' as const ,
@@ -147,44 +160,33 @@ export function useAvailableResources(
147160 name : integration . name ,
148161 iconComponent : integration . icon ,
149162 bgColor : integration . bgColor ,
150- isOpen : existingKeys . has ( `integration:${ integration . blockType } ` ) ,
151163 } ) ) ,
152164 } ,
153165 {
154166 type : 'task' as const ,
155- items : tasks . map ( ( t ) => ( {
156- id : t . id ,
157- name : t . name ,
158- isOpen : existingKeys . has ( `task:${ t . id } ` ) ,
159- } ) ) ,
167+ items : ( tasks ?? [ ] ) . map ( ( t ) => ( { id : t . id , name : t . name } ) ) ,
160168 } ,
161169 {
162170 type : 'scheduledtask' as const ,
163- items : schedules
171+ items : ( schedules ?? [ ] )
164172 . filter ( ( s ) => s . sourceType === 'job' )
165173 . map ( ( s ) => ( {
166174 id : s . id ,
167175 name : s . jobTitle || truncate ( s . prompt ?? '' , 40 ) || 'Scheduled Task' ,
168- isOpen : existingKeys . has ( `scheduledtask:${ s . id } ` ) ,
169176 } ) ) ,
170177 } ,
171178 {
172179 type : 'log' as const ,
173180 items : logs . map ( ( log ) => {
174181 const workflowName = log . workflow ?. name ?? log . workflowId ?? 'Unknown'
175182 const time = formatDate ( log . createdAt ) . compact
176- return {
177- id : log . id ,
178- name : `${ workflowName } · ${ time } ` ,
179- workflowName,
180- time,
181- isOpen : existingKeys . has ( `log:${ log . id } ` ) ,
182- }
183+ return { id : log . id , name : `${ workflowName } · ${ time } ` , workflowName, time }
183184 } ) ,
184185 } ,
185186 ]
186187 return groups . filter ( ( g ) => ! excluded . has ( g . type ) )
187188 } , [
189+ enabled ,
188190 workflows ,
189191 folders ,
190192 fileFolders ,
@@ -194,13 +196,12 @@ export function useAvailableResources(
194196 tasks ,
195197 schedules ,
196198 logs ,
197- existingKeys ,
198199 excludeTypes ,
199200 ] )
200201}
201202
202203export type WorkflowTreeNode =
203- | { kind : 'workflow' ; id : string ; name : string ; isOpen ?: boolean }
204+ | { kind : 'workflow' ; id : string ; name : string }
204205 | { kind : 'folder' ; id : string ; name : string ; children : WorkflowTreeNode [ ] }
205206
206207export function buildWorkflowFolderTree (
@@ -222,7 +223,6 @@ export function buildWorkflowFolderTree(
222223 kind : 'workflow' ,
223224 id : w . id ,
224225 name : w . name ,
225- isOpen : w . isOpen ,
226226 } )
227227
228228 const buildLevel = ( parentId : string | null ) : WorkflowTreeNode [ ] => {
@@ -262,7 +262,7 @@ export function buildWorkflowFolderTree(
262262
263263interface WorkflowFolderTreeItemsProps {
264264 nodes : WorkflowTreeNode [ ]
265- onSelect : ( resource : MothershipResource , isOpen ?: boolean ) => void
265+ onSelect : ( resource : MothershipResource ) => void
266266}
267267
268268export function WorkflowFolderTreeItems ( { nodes, onSelect } : WorkflowFolderTreeItemsProps ) {
@@ -272,9 +272,7 @@ export function WorkflowFolderTreeItems({ nodes, onSelect }: WorkflowFolderTreeI
272272 node . kind === 'workflow' ? (
273273 < DropdownMenuItem
274274 key = { node . id }
275- onClick = { ( ) =>
276- onSelect ( { type : 'workflow' , id : node . id , title : node . name } , node . isOpen )
277- }
275+ onClick = { ( ) => onSelect ( { type : 'workflow' , id : node . id , title : node . name } ) }
278276 >
279277 { getResourceConfig ( 'workflow' ) . renderDropdownItem ( {
280278 item : { id : node . id , name : node . name } ,
@@ -297,8 +295,8 @@ export function WorkflowFolderTreeItems({ nodes, onSelect }: WorkflowFolderTreeI
297295}
298296
299297export type FileFolderTreeNode =
300- | { kind : 'file' ; id : string ; name : string ; isOpen ?: boolean }
301- | { kind : 'folder' ; id : string ; name : string ; isOpen ?: boolean ; children : FileFolderTreeNode [ ] }
298+ | { kind : 'file' ; id : string ; name : string }
299+ | { kind : 'folder' ; id : string ; name : string ; children : FileFolderTreeNode [ ] }
302300
303301export function buildFileFolderTree (
304302 fileItems : AvailableItem [ ] ,
@@ -319,17 +317,15 @@ export function buildFileFolderTree(
319317 const childFiles = byFolder . get ( parentId ) ?? [ ]
320318 const nodes : FileFolderTreeNode [ ] = [ ]
321319 for ( const folder of childFolders ) {
322- const children = buildLevel ( folder . id )
323320 nodes . push ( {
324321 kind : 'folder' ,
325322 id : folder . id ,
326323 name : folder . name ,
327- isOpen : folder . isOpen ,
328- children,
324+ children : buildLevel ( folder . id ) ,
329325 } )
330326 }
331327 for ( const file of childFiles ) {
332- nodes . push ( { kind : 'file' , id : file . id , name : file . name , isOpen : file . isOpen } )
328+ nodes . push ( { kind : 'file' , id : file . id , name : file . name } )
333329 }
334330 return nodes
335331 }
@@ -339,7 +335,7 @@ export function buildFileFolderTree(
339335
340336interface FileFolderTreeItemsProps {
341337 nodes : FileFolderTreeNode [ ]
342- onSelect : ( resource : MothershipResource , isOpen ?: boolean ) => void
338+ onSelect : ( resource : MothershipResource ) => void
343339}
344340
345341export function FileFolderTreeItems ( { nodes, onSelect } : FileFolderTreeItemsProps ) {
@@ -349,7 +345,7 @@ export function FileFolderTreeItems({ nodes, onSelect }: FileFolderTreeItemsProp
349345 node . kind === 'file' ? (
350346 < DropdownMenuItem
351347 key = { node . id }
352- onClick = { ( ) => onSelect ( { type : 'file' , id : node . id , title : node . name } , node . isOpen ) }
348+ onClick = { ( ) => onSelect ( { type : 'file' , id : node . id , title : node . name } ) }
353349 >
354350 { getResourceConfig ( 'file' ) . renderDropdownItem ( {
355351 item : { id : node . id , name : node . name } ,
@@ -363,9 +359,7 @@ export function FileFolderTreeItems({ nodes, onSelect }: FileFolderTreeItemsProp
363359 </ DropdownMenuSubTrigger >
364360 < DropdownMenuSubContent >
365361 < DropdownMenuItem
366- onClick = { ( ) =>
367- onSelect ( { type : 'filefolder' , id : node . id , title : node . name } , node . isOpen )
368- }
362+ onClick = { ( ) => onSelect ( { type : 'filefolder' , id : node . id , title : node . name } ) }
369363 >
370364 < Folder className = 'size-[14px]' />
371365 < span > { node . name } </ span >
@@ -391,10 +385,8 @@ export function AddResourceDropdown({
391385 const [ open , setOpen ] = useState ( false )
392386 const [ search , setSearch ] = useState ( '' )
393387 const [ activeIndex , setActiveIndex ] = useState ( 0 )
394- const available = useAvailableResources ( workspaceId , existingKeys , [
395- ...( excludeTypes ?? [ ] ) ,
396- 'integration' ,
397- ] )
388+ // Gated on `open` so an idle tab bar never fetches the workspace lists.
389+ const available = useAvailableResources ( workspaceId , { enabled : open , excludeTypes } )
398390 const handleOpenChange = ( next : boolean ) => {
399391 setOpen ( next )
400392 if ( ! next ) {
@@ -403,8 +395,8 @@ export function AddResourceDropdown({
403395 }
404396 }
405397
406- const select = ( resource : MothershipResource , isOpen ?: boolean ) => {
407- if ( isOpen && onSwitch ) {
398+ const select = ( resource : MothershipResource ) => {
399+ if ( onSwitch && existingKeys . has ( ` ${ resource . type } : ${ resource . id } ` ) ) {
408400 onSwitch ( resource . id )
409401 } else {
410402 onAdd ( resource )
@@ -446,7 +438,7 @@ export function AddResourceDropdown({
446438 if ( filtered . length > 0 && filtered [ activeIndex ] ) {
447439 e . preventDefault ( )
448440 const { type, item } = filtered [ activeIndex ]
449- select ( { type, id : item . id , title : item . name } , item . isOpen )
441+ select ( { type, id : item . id , title : item . name } )
450442 }
451443 }
452444 }
@@ -494,7 +486,7 @@ export function AddResourceDropdown({
494486 key = { `${ type } :${ item . id } ` }
495487 className = { cn ( index === activeIndex && 'bg-[var(--surface-active)]' ) }
496488 onMouseEnter = { ( ) => setActiveIndex ( index ) }
497- onClick = { ( ) => select ( { type, id : item . id , title : item . name } , item . isOpen ) }
489+ onClick = { ( ) => select ( { type, id : item . id , title : item . name } ) }
498490 >
499491 { config . renderDropdownItem ( { item } ) }
500492 </ DropdownMenuItem >
@@ -553,9 +545,7 @@ export function AddResourceDropdown({
553545 { items . map ( ( item ) => (
554546 < DropdownMenuItem
555547 key = { item . id }
556- onClick = { ( ) =>
557- select ( { type, id : item . id , title : item . name } , item . isOpen )
558- }
548+ onClick = { ( ) => select ( { type, id : item . id , title : item . name } ) }
559549 >
560550 { config . renderDropdownItem ( { item } ) }
561551 </ DropdownMenuItem >
0 commit comments