Skip to content

Commit d66b103

Browse files
committed
improvement(chat): defer resource-menu list queries until the menu opens
The `+` resource menu and the `@`-mention menu both hydrate from `useAvailableResources`, which fired nine workspace-wide list queries and rebuilt ten item groups on mount — from two always-mounted call sites, for menus that were closed. Gate it on each menu's own open state. - add an `enabled` option to `useAvailableResources`; pass `open` from `AddResourceDropdown` and `PlusMenuDropdown` - add `options.enabled` to the six query hooks that lacked it, matching the convention already used by `useWorkspaceFiles` / `useKnowledgeBasesQuery` / `useLogsList` - skip the tab-name lookup's five list queries when there are no tabs to label - drop the `= []` destructuring defaults: a literal default allocates a fresh array every render while `data` is undefined, busting the group memo in exactly the disabled state - move the hook call into `PlusMenuDropdown`, which owns the open state, removing the `availableResources` pass-through from `usePromptEditor` - delete the `existingKeys`/`isOpen` plumbing — one consumer never read it; resolve it at the single call site instead
1 parent ae49070 commit d66b103

16 files changed

Lines changed: 141 additions & 153 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx

Lines changed: 69 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5154
interface 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+
5680
const LOG_DROPDOWN_LIMIT = 50
5781

5882
const LOG_DROPDOWN_FILTERS = {
@@ -69,76 +93,65 @@ const LOG_DROPDOWN_FILTERS = {
6993

7094
export 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

202203
export 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

206207
export 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

263263
interface WorkflowFolderTreeItemsProps {
264264
nodes: WorkflowTreeNode[]
265-
onSelect: (resource: MothershipResource, isOpen?: boolean) => void
265+
onSelect: (resource: MothershipResource) => void
266266
}
267267

268268
export 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

299297
export 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

303301
export 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

340336
interface FileFolderTreeItemsProps {
341337
nodes: FileFolderTreeNode[]
342-
onSelect: (resource: MothershipResource, isOpen?: boolean) => void
338+
onSelect: (resource: MothershipResource) => void
343339
}
344340

345341
export 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>

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
export type {
2-
AddResourceDropdownProps,
3-
AvailableItem,
4-
FileFolderTreeNode,
5-
WorkflowTreeNode,
6-
} from './add-resource-dropdown'
71
export {
82
AddResourceDropdown,
93
buildFileFolderTree,

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export type { AddResourceDropdownProps, AvailableItem } from './add-resource-dropdown'
21
export { AddResourceDropdown, useAvailableResources } from './add-resource-dropdown'
32
export { ResourceActions, ResourceContent } from './resource-content'
43
export type { ResourceTypeConfig } from './resource-registry'

0 commit comments

Comments
 (0)