Skip to content

Commit a091149

Browse files
committed
improvement(mothership): worklfow resource
1 parent 64cedfc commit a091149

File tree

11 files changed

+3525
-3353
lines changed

11 files changed

+3525
-3353
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { ResourceContent } from './resource-content'
2+
export { ResourceTabs } from './resource-tabs'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ResourceContent } from './resource-content'
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use client'
2+
3+
import { lazy, Suspense, useMemo } from 'react'
4+
import { Skeleton } from '@/components/emcn'
5+
import { FileViewer } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
6+
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
7+
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components'
8+
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
9+
10+
const Workflow = lazy(() => import('@/app/workspace/[workspaceId]/w/[workflowId]/workflow'))
11+
12+
const LOADING_SKELETON = (
13+
<div className='flex h-full flex-col gap-[8px] p-[24px]'>
14+
<Skeleton className='h-[16px] w-[60%]' />
15+
<Skeleton className='h-[16px] w-[80%]' />
16+
<Skeleton className='h-[16px] w-[40%]' />
17+
</div>
18+
)
19+
20+
interface ResourceContentProps {
21+
workspaceId: string
22+
resource: MothershipResource
23+
}
24+
25+
/**
26+
* Renders the content for the currently active mothership resource.
27+
* Handles table, file, and workflow resource types with appropriate
28+
* embedded rendering for each.
29+
*/
30+
export function ResourceContent({ workspaceId, resource }: ResourceContentProps) {
31+
switch (resource.type) {
32+
case 'table':
33+
return <Table key={resource.id} workspaceId={workspaceId} tableId={resource.id} embedded />
34+
35+
case 'file':
36+
return <EmbeddedFile key={resource.id} workspaceId={workspaceId} fileId={resource.id} />
37+
38+
case 'workflow':
39+
return (
40+
<Suspense fallback={LOADING_SKELETON}>
41+
<Workflow key={resource.id} workspaceId={workspaceId} workflowId={resource.id} embedded />
42+
</Suspense>
43+
)
44+
45+
default:
46+
return null
47+
}
48+
}
49+
50+
interface EmbeddedFileProps {
51+
workspaceId: string
52+
fileId: string
53+
}
54+
55+
function EmbeddedFile({ workspaceId, fileId }: EmbeddedFileProps) {
56+
const { data: files = [], isLoading } = useWorkspaceFiles(workspaceId)
57+
const file = useMemo(() => files.find((f) => f.id === fileId), [files, fileId])
58+
59+
if (isLoading) return LOADING_SKELETON
60+
61+
if (!file) {
62+
return (
63+
<div className='flex h-full items-center justify-center'>
64+
<span className='text-[13px] text-[var(--text-muted)]'>File not found</span>
65+
</div>
66+
)
67+
}
68+
69+
return (
70+
<div className='flex h-full flex-col overflow-hidden'>
71+
<FileViewer key={file.id} file={file} workspaceId={workspaceId} canEdit={true} />
72+
</div>
73+
)
74+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ResourceTabs } from './resource-tabs'
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client'
2+
3+
import { cn } from '@/lib/core/utils/cn'
4+
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
5+
6+
interface ResourceTabsProps {
7+
resources: MothershipResource[]
8+
activeId: string | null
9+
onSelect: (id: string) => void
10+
}
11+
12+
/**
13+
* Horizontal tab bar for switching between mothership resources.
14+
* Mirrors the role of ResourceHeader in the Resource abstraction.
15+
*/
16+
export function ResourceTabs({ resources, activeId, onSelect }: ResourceTabsProps) {
17+
return (
18+
<div className='flex shrink-0 gap-[2px] overflow-x-auto border-[var(--border)] border-b px-[12px]'>
19+
{resources.map((resource) => (
20+
<button
21+
key={resource.id}
22+
type='button'
23+
onClick={() => onSelect(resource.id)}
24+
className={cn(
25+
'shrink-0 cursor-pointer border-b-[2px] px-[12px] py-[10px] text-[13px] transition-colors',
26+
activeId === resource.id
27+
? 'border-[var(--text-primary)] font-medium text-[var(--text-primary)]'
28+
: 'border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
29+
)}
30+
>
31+
{resource.title}
32+
</button>
33+
))}
34+
</div>
35+
)
36+
}
Lines changed: 12 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
'use client'
22

3-
import { useMemo } from 'react'
4-
import { Skeleton } from '@/components/emcn'
5-
import { cn } from '@/lib/core/utils/cn'
6-
import { FileViewer } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
73
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
8-
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components'
9-
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
4+
import { ResourceContent, ResourceTabs } from './components'
105

116
interface MothershipViewProps {
127
workspaceId: string
@@ -15,6 +10,11 @@ interface MothershipViewProps {
1510
onSelectResource: (id: string) => void
1611
}
1712

13+
/**
14+
* Split-pane view that renders embedded resources (tables, files, workflows)
15+
* alongside the chat conversation. Composes ResourceTabs for navigation
16+
* and ResourceContent for rendering the active resource.
17+
*/
1818
export function MothershipView({
1919
workspaceId,
2020
resources,
@@ -25,66 +25,14 @@ export function MothershipView({
2525

2626
return (
2727
<div className='flex h-full w-[50%] min-w-[400px] flex-col border-[var(--border)] border-l'>
28-
<div className='flex shrink-0 gap-[2px] overflow-x-auto border-[var(--border)] border-b px-[12px]'>
29-
{resources.map((r) => (
30-
<button
31-
key={r.id}
32-
type='button'
33-
onClick={() => onSelectResource(r.id)}
34-
className={cn(
35-
'shrink-0 cursor-pointer border-b-[2px] px-[12px] py-[10px] text-[13px] transition-colors',
36-
active?.id === r.id
37-
? 'border-[var(--text-primary)] font-medium text-[var(--text-primary)]'
38-
: 'border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
39-
)}
40-
>
41-
{r.title}
42-
</button>
43-
))}
44-
</div>
45-
28+
<ResourceTabs
29+
resources={resources}
30+
activeId={active?.id ?? null}
31+
onSelect={onSelectResource}
32+
/>
4633
<div className='min-h-0 flex-1 overflow-hidden'>
47-
{active?.type === 'table' && (
48-
<Table key={active.id} workspaceId={workspaceId} tableId={active.id} embedded />
49-
)}
50-
{active?.type === 'file' && (
51-
<EmbeddedFile key={active.id} workspaceId={workspaceId} fileId={active.id} />
52-
)}
34+
{active && <ResourceContent workspaceId={workspaceId} resource={active} />}
5335
</div>
5436
</div>
5537
)
5638
}
57-
58-
interface EmbeddedFileProps {
59-
workspaceId: string
60-
fileId: string
61-
}
62-
63-
function EmbeddedFile({ workspaceId, fileId }: EmbeddedFileProps) {
64-
const { data: files = [], isLoading } = useWorkspaceFiles(workspaceId)
65-
const file = useMemo(() => files.find((f) => f.id === fileId), [files, fileId])
66-
67-
if (isLoading) {
68-
return (
69-
<div className='flex h-full flex-col gap-[8px] p-[24px]'>
70-
<Skeleton className='h-[16px] w-[60%]' />
71-
<Skeleton className='h-[16px] w-[80%]' />
72-
<Skeleton className='h-[16px] w-[40%]' />
73-
</div>
74-
)
75-
}
76-
77-
if (!file) {
78-
return (
79-
<div className='flex h-full items-center justify-center'>
80-
<span className='text-[13px] text-[var(--text-muted)]'>File not found</span>
81-
</div>
82-
)
83-
}
84-
85-
return (
86-
<div className='flex h-full flex-col overflow-hidden'>
87-
<FileViewer key={file.id} file={file} workspaceId={workspaceId} canEdit={true} />
88-
</div>
89-
)
90-
}

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 34 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
useChatHistory,
1313
} from '@/hooks/queries/tasks'
1414
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
15+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1516
import type {
1617
ChatMessage,
1718
ContentBlock,
@@ -22,6 +23,12 @@ import type {
2223
ToolCallStatus,
2324
} from '../types'
2425
import { SUBAGENT_LABELS } from '../types'
26+
import {
27+
extractFileResource,
28+
extractTableResource,
29+
extractWorkflowResource,
30+
RESOURCE_TOOL_NAMES,
31+
} from '../utils'
2532

2633
export interface UseChatReturn {
2734
messages: ChatMessage[]
@@ -78,60 +85,6 @@ function getPayloadData(payload: SSEPayload): SSEPayloadData | undefined {
7885
return typeof payload.data === 'object' ? payload.data : undefined
7986
}
8087

81-
const RESOURCE_TOOL_NAMES = new Set(['user_table', 'workspace_file'])
82-
83-
function getResultData(parsed: SSEPayload): Record<string, unknown> | undefined {
84-
const topResult = parsed.result as Record<string, unknown> | undefined
85-
const nestedResult =
86-
typeof parsed.data === 'object' ? (parsed.data?.result as Record<string, unknown>) : undefined
87-
const result = topResult ?? nestedResult
88-
return result?.data as Record<string, unknown> | undefined
89-
}
90-
91-
function extractTableResource(
92-
parsed: SSEPayload,
93-
storedArgs: Record<string, unknown> | undefined,
94-
fallbackTableId: string | null
95-
): MothershipResource | null {
96-
const data = getResultData(parsed)
97-
const storedInnerArgs = storedArgs?.args as Record<string, unknown> | undefined
98-
99-
const table = data?.table as Record<string, unknown> | undefined
100-
if (table?.id) {
101-
return { type: 'table', id: table.id as string, title: (table.name as string) || 'Table' }
102-
}
103-
104-
const tableId =
105-
(data?.tableId as string) ?? storedInnerArgs?.tableId ?? storedArgs?.tableId ?? fallbackTableId
106-
const tableName = (data?.tableName as string) || (table?.name as string) || 'Table'
107-
if (tableId) return { type: 'table', id: tableId as string, title: tableName }
108-
109-
return null
110-
}
111-
112-
function extractFileResource(
113-
parsed: SSEPayload,
114-
storedArgs: Record<string, unknown> | undefined
115-
): MothershipResource | null {
116-
const data = getResultData(parsed)
117-
const storedInnerArgs = storedArgs?.args as Record<string, unknown> | undefined
118-
119-
const file = data?.file as Record<string, unknown> | undefined
120-
if (file?.id) {
121-
return { type: 'file', id: file.id as string, title: (file.name as string) || 'File' }
122-
}
123-
124-
const fileId = (data?.fileId as string) ?? (data?.id as string)
125-
const fileName =
126-
(data?.fileName as string) ||
127-
(data?.name as string) ||
128-
(storedInnerArgs?.fileName as string) ||
129-
'File'
130-
if (fileId && typeof fileId === 'string') return { type: 'file', id: fileId, title: fileName }
131-
132-
return null
133-
}
134-
13588
export function useChat(workspaceId: string, initialChatId?: string): UseChatReturn {
13689
const pathname = usePathname()
13790
const queryClient = useQueryClient()
@@ -211,6 +164,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
211164
const blocks: ContentBlock[] = []
212165
const toolMap = new Map<string, number>()
213166
let lastTableId: string | null = null
167+
let lastWorkflowId: string | null = null
214168

215169
const ensureTextBlock = (): ContentBlock => {
216170
const last = blocks[blocks.length - 1]
@@ -349,6 +303,32 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
349303
queryKey: workspaceFilesKeys.content(workspaceId, resource.id),
350304
})
351305
}
306+
} else if (toolName === 'create_workflow' || toolName === 'edit_workflow') {
307+
resource = extractWorkflowResource(parsed, lastWorkflowId)
308+
if (resource) {
309+
lastWorkflowId = resource.id
310+
const registry = useWorkflowRegistry.getState()
311+
if (!registry.workflows[resource.id]) {
312+
useWorkflowRegistry.setState((state) => ({
313+
workflows: {
314+
...state.workflows,
315+
[resource!.id]: {
316+
id: resource!.id,
317+
name: resource!.title,
318+
lastModified: new Date(),
319+
createdAt: new Date(),
320+
color: '#7F2FFF',
321+
workspaceId,
322+
folderId: null,
323+
sortOrder: 0,
324+
},
325+
},
326+
}))
327+
registry.setActiveWorkflow(resource.id)
328+
} else {
329+
registry.loadWorkflowState(resource.id)
330+
}
331+
}
352332
}
353333

354334
if (resource) addResource(resource)

apps/sim/app/workspace/[workspaceId]/home/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export interface SSEPayload {
6666
result?: Record<string, unknown>
6767
}
6868

69-
export type MothershipResourceType = 'table' | 'file'
69+
export type MothershipResourceType = 'table' | 'file' | 'workflow'
7070

7171
export interface MothershipResource {
7272
type: MothershipResourceType

0 commit comments

Comments
 (0)