Skip to content

Commit 606477e

Browse files
authored
feat(home): add folders to resource menu (#4000)
* feat(home): add folders to resource menu * fix(home): add folder to API validation and dedup logic * fix(home): add folder context processing and generic title dedup * fix(home): add folder icon to mention chip overlay * fix(home): add folder to AgentContextType and context persistence * fix(home): add workspace scoping to folder resolver, fix folderId type and dedup * user message
1 parent 5eb494d commit 606477e

File tree

19 files changed

+213
-13
lines changed

19 files changed

+213
-13
lines changed

apps/sim/app/api/copilot/chat/resources/route.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,35 @@ import type { ChatResource, ResourceType } from '@/lib/copilot/resources'
1515

1616
const logger = createLogger('CopilotChatResourcesAPI')
1717

18-
const VALID_RESOURCE_TYPES = new Set<ResourceType>(['table', 'file', 'workflow', 'knowledgebase'])
19-
const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow', 'Knowledge Base'])
18+
const VALID_RESOURCE_TYPES = new Set<ResourceType>([
19+
'table',
20+
'file',
21+
'workflow',
22+
'knowledgebase',
23+
'folder',
24+
])
25+
const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow', 'Knowledge Base', 'Folder'])
2026

2127
const AddResourceSchema = z.object({
2228
chatId: z.string(),
2329
resource: z.object({
24-
type: z.enum(['table', 'file', 'workflow', 'knowledgebase']),
30+
type: z.enum(['table', 'file', 'workflow', 'knowledgebase', 'folder']),
2531
id: z.string(),
2632
title: z.string(),
2733
}),
2834
})
2935

3036
const RemoveResourceSchema = z.object({
3137
chatId: z.string(),
32-
resourceType: z.enum(['table', 'file', 'workflow', 'knowledgebase']),
38+
resourceType: z.enum(['table', 'file', 'workflow', 'knowledgebase', 'folder']),
3339
resourceId: z.string(),
3440
})
3541

3642
const ReorderResourcesSchema = z.object({
3743
chatId: z.string(),
3844
resources: z.array(
3945
z.object({
40-
type: z.enum(['table', 'file', 'workflow', 'knowledgebase']),
46+
type: z.enum(['table', 'file', 'workflow', 'knowledgebase', 'folder']),
4147
id: z.string(),
4248
title: z.string(),
4349
})

apps/sim/app/api/copilot/chat/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const ChatMessageSchema = z.object({
8888
'docs',
8989
'table',
9090
'file',
91+
'folder',
9192
]),
9293
label: z.string(),
9394
chatId: z.string().optional(),
@@ -99,6 +100,7 @@ const ChatMessageSchema = z.object({
99100
executionId: z.string().optional(),
100101
tableId: z.string().optional(),
101102
fileId: z.string().optional(),
103+
folderId: z.string().optional(),
102104
})
103105
)
104106
.optional(),

apps/sim/app/api/mothership/chat/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const FileAttachmentSchema = z.object({
3636
})
3737

3838
const ResourceAttachmentSchema = z.object({
39-
type: z.enum(['workflow', 'table', 'file', 'knowledgebase']),
39+
type: z.enum(['workflow', 'table', 'file', 'knowledgebase', 'folder']),
4040
id: z.string().min(1),
4141
title: z.string().optional(),
4242
active: z.boolean().optional(),
@@ -66,6 +66,7 @@ const MothershipMessageSchema = z.object({
6666
'docs',
6767
'table',
6868
'file',
69+
'folder',
6970
]),
7071
label: z.string(),
7172
chatId: z.string().optional(),
@@ -77,6 +78,7 @@ const MothershipMessageSchema = z.object({
7778
executionId: z.string().optional(),
7879
tableId: z.string().optional(),
7980
fileId: z.string().optional(),
81+
folderId: z.string().optional(),
8082
})
8183
)
8284
.optional(),
@@ -224,6 +226,7 @@ export async function POST(req: NextRequest) {
224226
...(c.knowledgeId && { knowledgeId: c.knowledgeId }),
225227
...(c.tableId && { tableId: c.tableId }),
226228
...(c.fileId && { fileId: c.fileId }),
229+
...(c.folderId && { folderId: c.folderId }),
227230
})),
228231
}),
229232
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
MothershipResource,
2525
MothershipResourceType,
2626
} from '@/app/workspace/[workspaceId]/home/types'
27+
import { useFolders } from '@/hooks/queries/folders'
2728
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
2829
import { useTablesList } from '@/hooks/queries/tables'
2930
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -51,6 +52,7 @@ export function useAvailableResources(
5152
const { data: tables = [] } = useTablesList(workspaceId)
5253
const { data: files = [] } = useWorkspaceFiles(workspaceId)
5354
const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId)
55+
const { data: folders = [] } = useFolders(workspaceId)
5456

5557
return useMemo(
5658
() => [
@@ -63,6 +65,14 @@ export function useAvailableResources(
6365
isOpen: existingKeys.has(`workflow:${w.id}`),
6466
})),
6567
},
68+
{
69+
type: 'folder' as const,
70+
items: folders.map((f) => ({
71+
id: f.id,
72+
name: f.name,
73+
isOpen: existingKeys.has(`folder:${f.id}`),
74+
})),
75+
},
6676
{
6777
type: 'table' as const,
6878
items: tables.map((t) => ({
@@ -88,7 +98,7 @@ export function useAvailableResources(
8898
})),
8999
},
90100
],
91-
[workflows, tables, files, knowledgeBases, existingKeys]
101+
[workflows, folders, tables, files, knowledgeBases, existingKeys]
92102
)
93103
}
94104

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

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import { createLogger } from '@sim/logger'
55
import { Square } from 'lucide-react'
66
import { useRouter } from 'next/navigation'
77
import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn'
8-
import { Download, FileX, SquareArrowUpRight, WorkflowX } from '@/components/emcn/icons'
8+
import {
9+
Download,
10+
FileX,
11+
Folder as FolderIcon,
12+
SquareArrowUpRight,
13+
WorkflowX,
14+
} from '@/components/emcn/icons'
915
import {
1016
cancelRunToolExecution,
1117
markRunToolManuallyStopped,
@@ -37,6 +43,7 @@ import {
3743
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components'
3844
import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
3945
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
46+
import { useFolders } from '@/hooks/queries/folders'
4047
import { useWorkflows } from '@/hooks/queries/workflows'
4148
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
4249
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
@@ -147,6 +154,9 @@ export const ResourceContent = memo(function ResourceContent({
147154
/>
148155
)
149156

157+
case 'folder':
158+
return <EmbeddedFolder key={resource.id} workspaceId={workspaceId} folderId={resource.id} />
159+
150160
case 'generic':
151161
return (
152162
<GenericResourceContent key={resource.id} data={genericResourceData ?? { entries: [] }} />
@@ -172,6 +182,7 @@ export function ResourceActions({ workspaceId, resource }: ResourceActionsProps)
172182
return (
173183
<EmbeddedKnowledgeBaseActions workspaceId={workspaceId} knowledgeBaseId={resource.id} />
174184
)
185+
case 'folder':
175186
case 'generic':
176187
return null
177188
default:
@@ -450,6 +461,72 @@ function EmbeddedFile({ workspaceId, fileId, previewMode, streamingContent }: Em
450461
)
451462
}
452463

464+
interface EmbeddedFolderProps {
465+
workspaceId: string
466+
folderId: string
467+
}
468+
469+
function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
470+
const { data: folderList, isPending: isFoldersPending } = useFolders(workspaceId)
471+
const { data: workflowList = [] } = useWorkflows(workspaceId)
472+
473+
const folder = useMemo(
474+
() => (folderList ?? []).find((f) => f.id === folderId),
475+
[folderList, folderId]
476+
)
477+
478+
const folderWorkflows = useMemo(
479+
() => workflowList.filter((w) => w.folderId === folderId),
480+
[workflowList, folderId]
481+
)
482+
483+
if (isFoldersPending) return LOADING_SKELETON
484+
485+
if (!folder) {
486+
return (
487+
<div className='flex h-full flex-col items-center justify-center gap-3'>
488+
<FolderIcon className='h-[32px] w-[32px] text-[var(--text-icon)]' />
489+
<div className='flex flex-col items-center gap-1'>
490+
<h2 className='font-medium text-[20px] text-[var(--text-primary)]'>Folder not found</h2>
491+
<p className='text-[var(--text-body)] text-small'>
492+
This folder may have been deleted or moved
493+
</p>
494+
</div>
495+
</div>
496+
)
497+
}
498+
499+
return (
500+
<div className='flex h-full flex-col overflow-y-auto p-6'>
501+
<h2 className='mb-4 font-medium text-[16px] text-[var(--text-primary)]'>{folder.name}</h2>
502+
{folderWorkflows.length === 0 ? (
503+
<p className='text-[13px] text-[var(--text-muted)]'>No workflows in this folder</p>
504+
) : (
505+
<div className='flex flex-col gap-1'>
506+
{folderWorkflows.map((w) => (
507+
<button
508+
key={w.id}
509+
type='button'
510+
onClick={() => window.open(`/workspace/${workspaceId}/w/${w.id}`, '_blank')}
511+
className='flex items-center gap-2 rounded-[6px] px-3 py-2 text-left transition-colors hover:bg-[var(--surface-4)]'
512+
>
513+
<div
514+
className='h-[12px] w-[12px] flex-shrink-0 rounded-[3px] border-[2px]'
515+
style={{
516+
backgroundColor: w.color,
517+
borderColor: `${w.color}60`,
518+
backgroundClip: 'padding-box',
519+
}}
520+
/>
521+
<span className='truncate text-[13px] text-[var(--text-primary)]'>{w.name}</span>
522+
</button>
523+
))}
524+
</div>
525+
)}
526+
</div>
527+
)
528+
}
529+
453530
function extractFileContent(raw: string): string {
454531
const marker = '"content":'
455532
const idx = raw.indexOf(marker)

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useParams } from 'next/navigation'
66
import {
77
Database,
88
File as FileIcon,
9+
Folder as FolderIcon,
910
Table as TableIcon,
1011
TerminalWindow,
1112
} from '@/components/emcn/icons'
@@ -18,6 +19,7 @@ import type {
1819
} from '@/app/workspace/[workspaceId]/home/types'
1920
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
2021
import { tableKeys } from '@/hooks/queries/tables'
22+
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
2123
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
2224
import { useWorkflows } from '@/hooks/queries/workflows'
2325
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
@@ -140,6 +142,15 @@ export const RESOURCE_REGISTRY: Record<MothershipResourceType, ResourceTypeConfi
140142
),
141143
renderDropdownItem: (props) => <IconDropdownItem {...props} icon={Database} />,
142144
},
145+
folder: {
146+
type: 'folder',
147+
label: 'Folders',
148+
icon: FolderIcon,
149+
renderTabIcon: (_resource, className) => (
150+
<FolderIcon className={cn(className, 'text-[var(--text-icon)]')} />
151+
),
152+
renderDropdownItem: (props) => <IconDropdownItem {...props} icon={FolderIcon} />,
153+
},
143154
} as const
144155

145156
export const RESOURCE_TYPES = Object.values(RESOURCE_REGISTRY)
@@ -171,6 +182,9 @@ const RESOURCE_INVALIDATORS: Record<
171182
qc.invalidateQueries({ queryKey: knowledgeKeys.detail(id) })
172183
qc.invalidateQueries({ queryKey: knowledgeKeys.tagDefinitions(id) })
173184
},
185+
folder: (qc) => {
186+
qc.invalidateQueries({ queryKey: folderKeys.lists() })
187+
},
174188
}
175189

176190
/**

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
MothershipResource,
2424
MothershipResourceType,
2525
} from '@/app/workspace/[workspaceId]/home/types'
26+
import { useFolders } from '@/hooks/queries/folders'
2627
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
2728
import { useTablesList } from '@/hooks/queries/tables'
2829
import {
@@ -57,15 +58,17 @@ function useResourceNameLookup(workspaceId: string): Map<string, string> {
5758
const { data: tables = [] } = useTablesList(workspaceId)
5859
const { data: files = [] } = useWorkspaceFiles(workspaceId)
5960
const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId)
61+
const { data: folders = [] } = useFolders(workspaceId)
6062

6163
return useMemo(() => {
6264
const map = new Map<string, string>()
6365
for (const w of workflows) map.set(`workflow:${w.id}`, w.name)
6466
for (const t of tables) map.set(`table:${t.id}`, t.name)
6567
for (const f of files) map.set(`file:${f.id}`, f.name)
6668
for (const kb of knowledgeBases ?? []) map.set(`knowledgebase:${kb.id}`, kb.name)
69+
for (const folder of folders) map.set(`folder:${folder.id}`, folder.name)
6770
return map
68-
}, [workflows, tables, files, knowledgeBases])
71+
}, [workflows, tables, files, knowledgeBases, folders])
6972
}
7073

7174
interface ResourceTabsProps {

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export function mapResourceToContext(resource: MothershipResource): ChatContext
8787
return { kind: 'table', tableId: resource.id, label: resource.title }
8888
case 'file':
8989
return { kind: 'file', fileId: resource.id, label: resource.title }
90+
case 'folder':
91+
return { kind: 'folder', folderId: resource.id, label: resource.title }
9092
default:
9193
return { kind: 'docs', label: resource.title }
9294
}

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type React from 'react'
44
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
55
import { useParams } from 'next/navigation'
6-
import { Database, Table as TableIcon } from '@/components/emcn/icons'
6+
import { Database, Folder as FolderIcon, Table as TableIcon } from '@/components/emcn/icons'
77
import { getDocumentIcon } from '@/components/icons/document-icons'
88
import { useSession } from '@/lib/auth/auth-client'
99
import { cn } from '@/lib/core/utils/cn'
@@ -175,6 +175,7 @@ export function UserInput({
175175
if (ctx.kind === 'knowledge' && ctx.knowledgeId) keys.add(`knowledgebase:${ctx.knowledgeId}`)
176176
if (ctx.kind === 'table' && ctx.tableId) keys.add(`table:${ctx.tableId}`)
177177
if (ctx.kind === 'file' && ctx.fileId) keys.add(`file:${ctx.fileId}`)
178+
if (ctx.kind === 'folder' && ctx.folderId) keys.add(`folder:${ctx.folderId}`)
178179
}
179180
return keys
180181
}, [contextManagement.selectedContexts])
@@ -663,6 +664,9 @@ export function UserInput({
663664
mentionIconNode = <FileDocIcon className={iconClasses} />
664665
break
665666
}
667+
case 'folder':
668+
mentionIconNode = <FolderIcon className={iconClasses} />
669+
break
666670
}
667671
}
668672

apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useMemo } from 'react'
44
import { useParams } from 'next/navigation'
5-
import { Database, Table as TableIcon } from '@/components/emcn/icons'
5+
import { Database, Folder as FolderIcon, Table as TableIcon } from '@/components/emcn/icons'
66
import { getDocumentIcon } from '@/components/icons/document-icons'
77
import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
88
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -81,6 +81,9 @@ function MentionHighlight({ context }: { context: ChatMessageContext }) {
8181
icon = <FileDocIcon className={iconClasses} />
8282
break
8383
}
84+
case 'folder':
85+
icon = <FolderIcon className={iconClasses} />
86+
break
8487
}
8588

8689
return (

0 commit comments

Comments
 (0)