Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions apps/sim/app/academy/components/sandbox-canvas-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import type {
import { validateExercise } from '@/lib/academy/validation'
import { cn } from '@/lib/core/utils/cn'
import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { SandboxWorkspacePermissionsProvider } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import Workflow from '@/app/workspace/[workspaceId]/w/[workflowId]/workflow'
import { getBlock } from '@/blocks/registry'
import { workflowKeys } from '@/hooks/queries/workflows'
import { SandboxBlockConstraintsContext } from '@/hooks/use-sandbox-block-constraints'
import { useExecutionStore } from '@/stores/execution/store'
import { useTerminalConsoleStore } from '@/stores/terminal/console/store'
Expand Down Expand Up @@ -218,8 +220,13 @@ export function SandboxCanvasProvider({

useWorkflowStore.getState().replaceWorkflowState(workflowState)
useSubBlockStore.getState().initializeFromWorkflow(workflowId, workflowState.blocks)
useWorkflowRegistry.setState((state) => ({
workflows: { ...state.workflows, [workflowId]: syntheticMetadata },

const qc = getQueryClient()
const cacheKey = workflowKeys.list(SANDBOX_WORKSPACE_ID, 'active')
const cached = qc.getQueryData<WorkflowMetadata[]>(cacheKey) ?? []
qc.setQueryData(cacheKey, [...cached.filter((w) => w.id !== workflowId), syntheticMetadata])

useWorkflowRegistry.setState({
activeWorkflowId: workflowId,
hydration: {
phase: 'ready',
Expand All @@ -228,7 +235,7 @@ export function SandboxCanvasProvider({
requestId: null,
error: null,
},
}))
})

logger.info('Sandbox stores hydrated', { workflowId })
setIsReady(true)
Expand Down Expand Up @@ -262,17 +269,21 @@ export function SandboxCanvasProvider({
unsubWorkflow()
unsubSubBlock()
unsubExecution()
useWorkflowRegistry.setState((state) => {
const { [workflowId]: _removed, ...rest } = state.workflows
return {
workflows: rest,
activeWorkflowId: state.activeWorkflowId === workflowId ? null : state.activeWorkflowId,
hydration:
state.hydration.workflowId === workflowId
? { phase: 'idle', workspaceId: null, workflowId: null, requestId: null, error: null }
: state.hydration,
}
})
const cleanupQc = getQueryClient()
const cleanupKey = workflowKeys.list(SANDBOX_WORKSPACE_ID, 'active')
const cleanupCached = cleanupQc.getQueryData<WorkflowMetadata[]>(cleanupKey) ?? []
cleanupQc.setQueryData(
cleanupKey,
cleanupCached.filter((w) => w.id !== workflowId)
)

useWorkflowRegistry.setState((state) => ({
activeWorkflowId: state.activeWorkflowId === workflowId ? null : state.activeWorkflowId,
hydration:
state.hydration.workflowId === workflowId
? { phase: 'idle', workspaceId: null, workflowId: null, requestId: null, error: null }
: state.hydration,
}))
useWorkflowStore.setState({ blocks: {}, edges: [], loops: {}, parallels: {} })
useSubBlockStore.setState((state) => {
const { [workflowId]: _removed, ...rest } = state.workflowValues
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function useAvailableResources(
workspaceId: string,
existingKeys: Set<string>
): AvailableItemsByType[] {
const { data: workflows = [] } = useWorkflows(workspaceId, { syncRegistry: false })
const { data: workflows = [] } = useWorkflows(workspaceId)
const { data: tables = [] } = useTablesList(workspaceId)
const { data: files = [] } = useWorkspaceFiles(workspaceId)
const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components'
import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
import { useWorkflows } from '@/hooks/queries/workflows'
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useExecutionStore } from '@/stores/execution/store'
Expand Down Expand Up @@ -375,15 +376,16 @@ interface EmbeddedWorkflowProps {
}

function EmbeddedWorkflow({ workspaceId, workflowId }: EmbeddedWorkflowProps) {
const workflowExists = useWorkflowRegistry((state) => Boolean(state.workflows[workflowId]))
const isMetadataLoaded = useWorkflowRegistry(
(state) => state.hydration.phase !== 'idle' && state.hydration.phase !== 'metadata-loading'
const { data: workflowList, isPending: isWorkflowsPending } = useWorkflows(workspaceId)
const workflowExists = useMemo(
() => (workflowList ?? []).some((w) => w.id === workflowId),
[workflowList, workflowId]
)
const hasLoadError = useWorkflowRegistry(
(state) => state.hydration.phase === 'error' && state.hydration.workflowId === workflowId
)

if (!isMetadataLoaded) return LOADING_SKELETON
if (isWorkflowsPending) return LOADING_SKELETON

if (!workflowExists || hasLoadError) {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use client'

import type { ElementType, ReactNode } from 'react'
import { type ElementType, type ReactNode, useMemo } from 'react'
import type { QueryClient } from '@tanstack/react-query'
import { useParams } from 'next/navigation'
import {
Database,
File as FileIcon,
Expand All @@ -17,9 +18,9 @@ import type {
} from '@/app/workspace/[workspaceId]/home/types'
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
import { tableKeys } from '@/hooks/queries/tables'
import { workflowKeys } from '@/hooks/queries/workflows'
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
import { useWorkflows } from '@/hooks/queries/workflows'
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'

interface DropdownItemRenderProps {
item: { id: string; name: string; [key: string]: unknown }
Expand All @@ -34,7 +35,12 @@ export interface ResourceTypeConfig {
}

function WorkflowTabSquare({ workflowId, className }: { workflowId: string; className?: string }) {
const color = useWorkflowRegistry((state) => state.workflows[workflowId]?.color ?? '#888')
const { workspaceId } = useParams<{ workspaceId: string }>()
const { data: workflowList } = useWorkflows(workspaceId)
const color = useMemo(() => {
const wf = (workflowList ?? []).find((w) => w.id === workflowId)
return wf?.color ?? '#888'
}, [workflowList, workflowId])
return (
<div
className={cn('flex-shrink-0 rounded-[3px] border-[2px]', className)}
Expand Down Expand Up @@ -157,8 +163,8 @@ const RESOURCE_INVALIDATORS: Record<
qc.invalidateQueries({ queryKey: workspaceFilesKeys.contentFile(wId, id) })
qc.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() })
},
workflow: (qc, _wId) => {
qc.invalidateQueries({ queryKey: workflowKeys.lists() })
workflow: (qc, wId) => {
void invalidateWorkflowLists(qc, wId)
},
knowledgebase: (qc, _wId, id) => {
qc.invalidateQueries({ queryKey: knowledgeKeys.lists() })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const PREVIEW_MODE_LABELS: Record<PreviewMode, string> = {
* tabs always reflect the latest name even after a rename.
*/
function useResourceNameLookup(workspaceId: string): Map<string, string> {
const { data: workflows = [] } = useWorkflows(workspaceId, { syncRegistry: false })
const { data: workflows = [] } = useWorkflows(workspaceId)
const { data: tables = [] } = useTablesList(workspaceId)
const { data: files = [] } = useWorkspaceFiles(workspaceId)
const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ import {
computeMentionHighlightRanges,
extractContextTokens,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils'
import { useWorkflowMap } from '@/hooks/queries/workflows'
import type { ChatContext } from '@/stores/panel'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'

export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'

Expand Down Expand Up @@ -122,6 +122,7 @@ export function UserInput({
onContextAdd,
}: UserInputProps) {
const { workspaceId } = useParams<{ workspaceId: string }>()
const { data: workflowsById = {} } = useWorkflowMap(workspaceId)
const { data: session } = useSession()
const [value, setValue] = useState(defaultValue)
const overlayRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -617,7 +618,6 @@ export function UserInput({

const elements: React.ReactNode[] = []
let lastIndex = 0

for (let i = 0; i < ranges.length; i++) {
const range = ranges[i]

Expand All @@ -639,7 +639,7 @@ export function UserInput({
case 'workflow':
case 'current_workflow': {
const wfId = (matchingCtx as { workflowId: string }).workflowId
const wfColor = useWorkflowRegistry.getState().workflows[wfId]?.color ?? '#888'
const wfColor = workflowsById[wfId]?.color ?? '#888'
mentionIconNode = (
<div
className='absolute inset-0 m-auto h-[12px] w-[12px] rounded-[3px] border-[2px]'
Expand Down Expand Up @@ -691,7 +691,7 @@ export function UserInput({
}

return elements.length > 0 ? elements : <span>{'\u00A0'}</span>
}, [value, contextManagement.selectedContexts])
}, [value, contextManagement.selectedContexts, workflowsById])

return (
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client'

import { useMemo } from 'react'
import { useParams } from 'next/navigation'
import { Database, Table as TableIcon } from '@/components/emcn/icons'
import { getDocumentIcon } from '@/components/icons/document-icons'
import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflows } from '@/hooks/queries/workflows'

const USER_MESSAGE_CLASSES =
'whitespace-pre-wrap break-words [overflow-wrap:anywhere] font-[430] font-[family-name:var(--font-inter)] text-base text-[var(--text-primary)] leading-[23px] tracking-[0] antialiased'
Expand Down Expand Up @@ -44,12 +46,12 @@ function computeMentionRanges(text: string, contexts: ChatMessageContext[]): Men
}

function MentionHighlight({ context }: { context: ChatMessageContext }) {
const workflowColor = useWorkflowRegistry((state) => {
if (context.kind === 'workflow' || context.kind === 'current_workflow') {
return state.workflows[context.workflowId || '']?.color ?? null
}
return null
})
const { workspaceId } = useParams<{ workspaceId: string }>()
const { data: workflowList } = useWorkflows(workspaceId)
const workflowColor = useMemo(() => {
if (context.kind !== 'workflow' && context.kind !== 'current_workflow') return null
return (workflowList ?? []).find((w) => w.id === context.workflowId)?.color ?? null
}, [workflowList, context.kind, context.workflowId])

let icon: React.ReactNode = null
const iconClasses = 'h-[12px] w-[12px] flex-shrink-0 text-[var(--text-icon)]'
Expand Down
84 changes: 47 additions & 37 deletions apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,23 @@ import {
import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resource-types'
import { isWorkflowToolName } from '@/lib/copilot/workflow-tools'
import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { invalidateResourceQueries } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import type {
ChatMessage,
ChatMessageAttachment,
ContentBlock,
ContentBlockType,
FileAttachmentForApi,
GenericResourceData,
GenericResourceEntry,
MothershipResource,
MothershipResourceType,
QueuedMessage,
SSEPayload,
SSEPayloadData,
ToolCallStatus,
} from '@/app/workspace/[workspaceId]/home/types'
import { deploymentKeys } from '@/hooks/queries/deployments'
import {
fetchChatHistory,
Expand All @@ -34,29 +50,17 @@ import {
taskKeys,
useChatHistory,
} from '@/hooks/queries/tasks'
import { getFolderMap } from '@/hooks/queries/utils/folder-cache'
import { invalidateWorkflowSelectors } from '@/hooks/queries/utils/invalidate-workflow-lists'
import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order'
import { getWorkflowById, getWorkflows } from '@/hooks/queries/utils/workflow-cache'
import { workflowKeys } from '@/hooks/queries/workflows'
import { useExecutionStream } from '@/hooks/use-execution-stream'
import { useExecutionStore } from '@/stores/execution/store'
import { useFolderStore } from '@/stores/folders/store'
import type { ChatContext } from '@/stores/panel'
import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type {
ChatMessage,
ChatMessageAttachment,
ContentBlock,
ContentBlockType,
FileAttachmentForApi,
GenericResourceData,
GenericResourceEntry,
MothershipResource,
MothershipResourceType,
QueuedMessage,
SSEPayload,
SSEPayloadData,
ToolCallStatus,
} from '../types'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'

export interface UseChatReturn {
messages: ChatMessage[]
Expand Down Expand Up @@ -301,31 +305,37 @@ function getPayloadData(payload: SSEPayload): SSEPayloadData | undefined {
return typeof payload.data === 'object' ? payload.data : undefined
}

/** Adds a workflow to the registry with a top-insertion sort order if it doesn't already exist. */
/** Adds a workflow to the React Query cache with a top-insertion sort order if it doesn't already exist. */
function ensureWorkflowInRegistry(resourceId: string, title: string, workspaceId: string): boolean {
const registry = useWorkflowRegistry.getState()
if (registry.workflows[resourceId]) return false
const workflows = getWorkflows(workspaceId)
if (workflows.some((w) => w.id === resourceId)) return false
const sortOrder = getTopInsertionSortOrder(
registry.workflows,
useFolderStore.getState().folders,
Object.fromEntries(workflows.map((w) => [w.id, w])),
getFolderMap(workspaceId),
workspaceId,
null
)
useWorkflowRegistry.setState((state) => ({
workflows: {
...state.workflows,
[resourceId]: {
id: resourceId,
name: title,
lastModified: new Date(),
createdAt: new Date(),
color: getNextWorkflowColor(),
workspaceId,
folderId: null,
sortOrder,
},
},
}))
const newMetadata: WorkflowMetadata = {
id: resourceId,
name: title,
lastModified: new Date(),
createdAt: new Date(),
color: getNextWorkflowColor(),
workspaceId,
folderId: null,
sortOrder,
}
const queryClient = getQueryClient()
const key = workflowKeys.list(workspaceId, 'active')
queryClient.setQueryData<WorkflowMetadata[]>(key, (current) => {
const next = current ?? workflows
if (next.some((workflow) => workflow.id === resourceId)) {
return next
}

return [...next, newMetadata]
})
void invalidateWorkflowSelectors(queryClient, workspaceId)
return true
}

Expand Down Expand Up @@ -1253,7 +1263,7 @@ export function useChat(
? ((args as Record<string, unknown>).workflowId as string)
: useWorkflowRegistry.getState().activeWorkflowId
if (targetWorkflowId) {
const meta = useWorkflowRegistry.getState().workflows[targetWorkflowId]
const meta = getWorkflowById(workspaceId, targetWorkflowId)
const wasAdded = addResource({
type: 'workflow',
id: targetWorkflowId,
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/
import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'
import { SettingsLoader } from '@/app/workspace/[workspaceId]/providers/settings-loader'
import { WorkspacePermissionsProvider } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { WorkspaceScopeSync } from '@/app/workspace/[workspaceId]/providers/workspace-scope-sync'
import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'

export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
Expand All @@ -16,6 +17,7 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
<div className='flex h-screen w-full flex-col overflow-hidden bg-[var(--surface-1)]'>
<ImpersonationBanner />
<WorkspacePermissionsProvider>
<WorkspaceScopeSync />
<div className='flex min-h-0 flex-1'>
<div className='shrink-0' suppressHydrationWarning>
<Sidebar />
Expand Down
Loading
Loading