From 434db43c11ec89035b9f2fecc89cd8dfc6b915fd Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 25 Sep 2026 10:59:56 -0700 Subject: [PATCH 1/2] improvement(chat): nest folders inside their resources in the org resource menu --- .../add-resource-dropdown.tsx | 31 +++++++---- .../components/add-resource-dropdown/index.ts | 1 + .../plus-menu-dropdown/plus-menu-dropdown.tsx | 52 +++++++++++++++---- 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx index 4ea5c69e6ac..230476cd026 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx @@ -204,7 +204,6 @@ export function useResourceTreeSections({ } interface ResourceMenuSectionsProps { - flat?: boolean /** Foldered families, from {@link useResourceTreeSections}. */ sections: ResourceTreeSection[] /** Every available family. Foldered ones are taken from `sections` instead. */ @@ -226,7 +225,6 @@ interface ResourceMenuSectionsProps { * canonical order. */ export function ResourceMenuSections({ - flat = false, sections, groups, onSelect, @@ -235,7 +233,7 @@ export function ResourceMenuSections({ const sectionByType = new Map(sections.map((section) => [section.type, section])) const entries = groups .filter(({ type, items }) => - !flat && FOLDERED_RESOURCE_TYPES.has(type) ? sectionByType.has(type) : items.length > 0 + FOLDERED_RESOURCE_TYPES.has(type) ? sectionByType.has(type) : items.length > 0 ) .sort(byResourceMenuOrder) @@ -398,6 +396,8 @@ interface WorkspaceResourceMenuContentProps { enabled: boolean excludeTypes?: readonly MothershipResourceType[] searchable?: boolean + /** Offers every folder as an attachable entry, as chat does. */ + selectFolders?: boolean onSelect: (resource: MothershipResource) => void } @@ -406,13 +406,14 @@ function WorkspaceResourceMenuContent({ enabled, excludeTypes, searchable = true, + selectFolders, onSelect, }: WorkspaceResourceMenuContentProps) { const { groups, structureFolders, isHydrating } = useAvailableResources(workspaceId, { enabled, excludeTypes, }) - const sections = useResourceTreeSections({ groups, structureFolders }) + const sections = useResourceTreeSections({ groups, structureFolders, selectFolders }) const select = (resource: MothershipResource) => onSelect( resource.type === 'browser' || resource.type === 'terminal' @@ -429,15 +430,24 @@ function WorkspaceResourceMenuContent({ ) } -function WorkspaceResourceSubmenu({ - workspace, - excludeTypes, - onSelect, -}: { +interface WorkspaceResourceSubmenuProps { workspace: { id: string; name: string } + /** Must be referentially stable (a module constant) — it keys the group memo. */ excludeTypes?: readonly MothershipResourceType[] + selectFolders?: boolean onSelect: (resource: MothershipResource) => void -}) { +} + +/** + * One workspace of an organization-wide picker: its own foldered resource menu, + * fetched when the submenu first opens. Selections carry the workspace as owner. + */ +export function WorkspaceResourceSubmenu({ + workspace, + excludeTypes, + selectFolders, + onSelect, +}: WorkspaceResourceSubmenuProps) { const [open, setOpen] = useState(false) return ( @@ -450,6 +460,7 @@ function WorkspaceResourceSubmenu({ enabled={open} excludeTypes={excludeTypes} searchable={false} + selectFolders={selectFolders} onSelect={onSelect} /> diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/index.ts index db2ff5a6431..0e9f610be57 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/index.ts @@ -4,6 +4,7 @@ export { ResourceFolderTreeItems, ResourceMenuSections, useResourceTreeSections, + WorkspaceResourceSubmenu, } from './add-resource-dropdown' export { useAvailableResources } from './available-resources' export { resourceFromItem } from './resource-from-item' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx index 999a3092f15..0d59291a4c7 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx @@ -15,6 +15,7 @@ import { resourceFromItem, useAvailableResources, useResourceTreeSections, + WorkspaceResourceSubmenu, } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' import type { AvailableResources } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/available-resources' import { @@ -62,6 +63,22 @@ const MENTION_MAX_HEIGHT_CLASS = 'max-h-[min(280px,var(--radix-popper-available- * (`ADD_RESOURCE_EXCLUDED_TYPES` in `resource-tabs`). */ const MENTION_ONLY_RESOURCE_TYPES = new Set(['integration']) + +/** + * Families an organization chat's workspace submenus leave out: the mention-only + * ones, plus Browser and Terminal, which belong to this desktop rather than to a + * workspace and so sit once after the workspaces. + */ +const WORKSPACE_SUBMENU_EXCLUDED_TYPES: readonly MothershipResourceType[] = [ + ...MENTION_ONLY_RESOURCE_TYPES, + 'browser', + 'terminal', +] + +function isNativeResourceGroup({ type }: { type: MothershipResourceType }): boolean { + return type === 'browser' || type === 'terminal' +} + const EMPTY_BROWSER_TABS = [] as const const EMPTY_TERMINAL_TABS = [] as const @@ -138,12 +155,9 @@ export const PlusMenuDropdown = React.memo( : workspaceInventory const { structureFolders } = combined const availableResources = organizationId - ? [ - ...combined.groups, - ...workspaceInventory.groups.filter( - (group) => group.type === 'browser' || group.type === 'terminal' - ), - ].sort(byResourceMenuOrder) + ? [...combined.groups, ...workspaceInventory.groups.filter(isNativeResourceGroup)].sort( + byResourceMenuOrder + ) : combined.groups const isHydrating = combined.isHydrating || Boolean(organizationId && workspacesPending) @@ -183,9 +197,14 @@ export const PlusMenuDropdown = React.memo( terminalTabs, ]) + /** + * Built from this workspace's own inventory, which has no foldered families in an + * organization chat: there each workspace submenu builds its own sections, because + * ids are only unique within a workspace. + */ const treeSections = useResourceTreeSections({ - groups: availableResources, - structureFolders, + groups: workspaceInventory.groups, + structureFolders: workspaceInventory.structureFolders, selectFolders: true, }) @@ -386,10 +405,21 @@ export const PlusMenuDropdown = React.memo( {/* Always-mounted; swapping this subtree with filtered results makes Radix's menu FocusScope steal focus from the search input back to the content root. */}