diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css
index 0c80aef072a..254534f2891 100644
--- a/apps/sim/app/_styles/globals.css
+++ b/apps/sim/app/_styles/globals.css
@@ -10,7 +10,7 @@
* @see stores/constants.ts for the source of truth
*/
:root {
- --sidebar-width: 248px; /* SIDEBAR_WIDTH.DEFAULT */
+ --sidebar-width: 0px; /* 0 outside workspace; blocking script always sets actual value on workspace pages */
--panel-width: 320px; /* PANEL_WIDTH.DEFAULT */
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
diff --git a/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts b/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts
index fe3246181d4..7fd68b4925e 100644
--- a/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts
+++ b/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts
@@ -304,7 +304,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
loops: {},
parallels: {},
isDeployed: true,
- deploymentStatuses: { production: 'deployed' },
},
}
@@ -349,7 +348,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
loops: {},
parallels: {},
isDeployed: true,
- deploymentStatuses: { production: 'deployed' },
lastSaved: 1640995200000,
},
},
@@ -370,7 +368,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
loops: {},
parallels: {},
isDeployed: true,
- deploymentStatuses: { production: 'deployed' },
lastSaved: 1640995200000,
}),
}
@@ -473,7 +470,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
edges: undefined,
loops: null,
parallels: undefined,
- deploymentStatuses: null,
},
}
@@ -508,7 +504,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
loops: {},
parallels: {},
isDeployed: false,
- deploymentStatuses: {},
lastSaved: 1640995200000,
})
})
@@ -768,10 +763,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
parallel1: { branches: ['branch1', 'branch2'] },
},
isDeployed: true,
- deploymentStatuses: {
- production: 'deployed',
- staging: 'pending',
- },
deployedAt: '2024-01-01T10:00:00.000Z',
},
}
@@ -816,10 +807,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
parallel1: { branches: ['branch1', 'branch2'] },
},
isDeployed: true,
- deploymentStatuses: {
- production: 'deployed',
- staging: 'pending',
- },
deployedAt: '2024-01-01T10:00:00.000Z',
lastSaved: 1640995200000,
})
diff --git a/apps/sim/app/api/copilot/checkpoints/revert/route.ts b/apps/sim/app/api/copilot/checkpoints/revert/route.ts
index 2edf7d2dec7..dd73477f5ec 100644
--- a/apps/sim/app/api/copilot/checkpoints/revert/route.ts
+++ b/apps/sim/app/api/copilot/checkpoints/revert/route.ts
@@ -82,7 +82,6 @@ export async function POST(request: NextRequest) {
loops: checkpointState?.loops || {},
parallels: checkpointState?.parallels || {},
isDeployed: checkpointState?.isDeployed || false,
- deploymentStatuses: checkpointState?.deploymentStatuses || {},
lastSaved: Date.now(),
...(checkpointState?.deployedAt &&
checkpointState.deployedAt !== null &&
diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
index 618a1de8f94..a209db29eb4 100644
--- a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
+++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
@@ -79,7 +79,6 @@ export async function POST(
loops: deployedState.loops || {},
parallels: deployedState.parallels || {},
lastSaved: Date.now(),
- deploymentStatuses: deployedState.deploymentStatuses || {},
})
if (!saveResult.success) {
diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts
index 95a6ee43dc7..3d74fe527fa 100644
--- a/apps/sim/app/api/workflows/[id]/route.ts
+++ b/apps/sim/app/api/workflows/[id]/route.ts
@@ -89,7 +89,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const finalWorkflowData = {
...workflowData,
state: {
- deploymentStatuses: {},
blocks: normalizedData.blocks,
edges: normalizedData.edges,
loops: normalizedData.loops,
@@ -115,7 +114,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const emptyWorkflowData = {
...workflowData,
state: {
- deploymentStatuses: {},
blocks: {},
edges: [],
loops: {},
diff --git a/apps/sim/app/api/workflows/[id]/variables/route.ts b/apps/sim/app/api/workflows/[id]/variables/route.ts
index 7b19a0320a9..1b4cd8ab3b9 100644
--- a/apps/sim/app/api/workflows/[id]/variables/route.ts
+++ b/apps/sim/app/api/workflows/[id]/variables/route.ts
@@ -8,7 +8,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
-import type { Variable } from '@/stores/panel/variables/types'
+import type { Variable } from '@/stores/variables/types'
const logger = createLogger('WorkflowVariablesAPI')
diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx
index 05fd18a7c0b..5863c4405d3 100644
--- a/apps/sim/app/layout.tsx
+++ b/apps/sim/app/layout.tsx
@@ -90,6 +90,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
}
// Sidebar width
+ var defaultSidebarWidth = '248px';
try {
var stored = localStorage.getItem('sidebar-state');
if (stored) {
@@ -108,11 +109,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
document.documentElement.style.setProperty('--sidebar-width', width + 'px');
} else if (width > maxSidebarWidth) {
document.documentElement.style.setProperty('--sidebar-width', maxSidebarWidth + 'px');
+ } else {
+ document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth);
}
}
+ } else {
+ document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth);
}
} catch (e) {
- // Fallback handled by CSS defaults
+ document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth);
}
// Panel width and active tab
diff --git a/apps/sim/app/templates/components/template-card.tsx b/apps/sim/app/templates/components/template-card.tsx
index 15fb0cec4cc..7a2f3d2e105 100644
--- a/apps/sim/app/templates/components/template-card.tsx
+++ b/apps/sim/app/templates/components/template-card.tsx
@@ -108,8 +108,6 @@ function normalizeWorkflowState(input?: any): WorkflowState | null {
lastUpdate: input.lastUpdate,
metadata: input.metadata,
variables: input.variables,
- deploymentStatuses: input.deploymentStatuses,
- needsRedeployment: input.needsRedeployment,
dragStartPosition: input.dragStartPosition ?? null,
}
diff --git a/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx
index 5f4e89edd09..7720c84fa3a 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx
@@ -16,7 +16,7 @@ import {
} from '@/components/emcn'
import { client, useSession } from '@/lib/auth/auth-client'
import type { OAuthReturnContext } from '@/lib/credentials/client-state'
-import { writeOAuthReturnContext } from '@/lib/credentials/client-state'
+import { ADD_CONNECTOR_SEARCH_PARAM, writeOAuthReturnContext } from '@/lib/credentials/client-state'
import {
getCanonicalScopesForProvider,
getProviderIdFromServiceId,
@@ -59,8 +59,8 @@ type OAuthModalConnectProps = OAuthModalBaseProps & {
workspaceId: string
credentialCount: number
} & (
- | { workflowId: string; knowledgeBaseId?: never }
- | { workflowId?: never; knowledgeBaseId: string }
+ | { workflowId: string; knowledgeBaseId?: never; connectorType?: never }
+ | { workflowId?: never; knowledgeBaseId: string; connectorType?: string }
)
interface OAuthModalReauthorizeProps extends OAuthModalBaseProps {
@@ -81,6 +81,7 @@ export function OAuthModal(props: OAuthModalProps) {
const workspaceId = isConnect ? props.workspaceId : ''
const workflowId = isConnect ? props.workflowId : undefined
const knowledgeBaseId = isConnect ? props.knowledgeBaseId : undefined
+ const connectorType = isConnect ? props.connectorType : undefined
const toolName = !isConnect ? props.toolName : ''
const requiredScopes = !isConnect ? (props.requiredScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES
const newScopes = !isConnect ? (props.newScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES
@@ -172,7 +173,7 @@ export function OAuthModal(props: OAuthModalProps) {
}
const returnContext: OAuthReturnContext = knowledgeBaseId
- ? { ...baseContext, origin: 'kb-connectors' as const, knowledgeBaseId }
+ ? { ...baseContext, origin: 'kb-connectors' as const, knowledgeBaseId, connectorType }
: { ...baseContext, origin: 'workflow' as const, workflowId: workflowId! }
writeOAuthReturnContext(returnContext)
@@ -205,7 +206,11 @@ export function OAuthModal(props: OAuthModalProps) {
return
}
- await client.oauth2.link({ providerId, callbackURL: window.location.href })
+ const callbackURL = new URL(window.location.href)
+ if (connectorType) {
+ callbackURL.searchParams.set(ADD_CONNECTOR_SEARCH_PARAM, connectorType)
+ }
+ await client.oauth2.link({ providerId, callbackURL: callbackURL.toString() })
handleClose()
} catch (err) {
logger.error('Failed to initiate OAuth connection', { error: err })
diff --git a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/loading.tsx b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/loading.tsx
deleted file mode 100644
index c64c3a39e27..00000000000
--- a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/loading.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Loader2 } from 'lucide-react'
-
-export default function FileViewLoading() {
- return (
-
-
-
- )
-}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
index 5fa965d9abe..88866a77000 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
@@ -1407,17 +1407,6 @@ export function useChat(
const output = tc.result?.output as Record | undefined
const deployedWorkflowId = (output?.workflowId as string) ?? undefined
if (deployedWorkflowId && typeof output?.isDeployed === 'boolean') {
- const isDeployed = output.isDeployed as boolean
- const serverDeployedAt = output.deployedAt
- ? new Date(output.deployedAt as string)
- : undefined
- useWorkflowRegistry
- .getState()
- .setDeploymentStatus(
- deployedWorkflowId,
- isDeployed,
- isDeployed ? (serverDeployedAt ?? new Date()) : undefined
- )
queryClient.invalidateQueries({
queryKey: deploymentKeys.info(deployedWorkflowId),
})
diff --git a/apps/sim/app/workspace/[workspaceId]/home/loading.tsx b/apps/sim/app/workspace/[workspaceId]/home/loading.tsx
deleted file mode 100644
index 98b656ef94e..00000000000
--- a/apps/sim/app/workspace/[workspaceId]/home/loading.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Skeleton } from '@/components/emcn'
-
-const SKELETON_LINE_COUNT = 4
-
-export default function HomeLoading() {
- return (
-
-
-
- {Array.from({ length: SKELETON_LINE_COUNT }).map((_, i) => (
-
- ))}
-
-
-
-
- )
-}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
index eb5823f9100..e81701bac4d 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { format } from 'date-fns'
import { AlertCircle, Loader2, Pencil, Plus, Tag, X } from 'lucide-react'
-import { useParams, useRouter } from 'next/navigation'
+import { useParams, usePathname, useRouter, useSearchParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import {
Badge,
@@ -25,6 +25,7 @@ import {
import { Database, DatabaseX } from '@/components/emcn/icons'
import { SearchHighlight } from '@/components/ui/search-highlight'
import { cn } from '@/lib/core/utils/cn'
+import { ADD_CONNECTOR_SEARCH_PARAM } from '@/lib/credentials/client-state'
import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants'
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
@@ -192,6 +193,10 @@ export function KnowledgeBase({
}: KnowledgeBaseProps) {
const params = useParams()
const workspaceId = propWorkspaceId || (params.workspaceId as string)
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const pathname = usePathname()
+ const addConnectorParam = searchParams.get(ADD_CONNECTOR_SEARCH_PARAM)
const posthog = usePostHog()
useEffect(() => {
@@ -278,7 +283,29 @@ export function KnowledgeBase({
const [contextMenuDocument, setContextMenuDocument] = useState(null)
const [showRenameModal, setShowRenameModal] = useState(false)
const [documentToRename, setDocumentToRename] = useState(null)
- const [showAddConnectorModal, setShowAddConnectorModal] = useState(false)
+ const showAddConnectorModal = addConnectorParam != null
+ const searchParamsRef = useRef(searchParams)
+ searchParamsRef.current = searchParams
+ const updateAddConnectorParam = useCallback(
+ (value: string | null) => {
+ const current = searchParamsRef.current
+ const currentValue = current.get(ADD_CONNECTOR_SEARCH_PARAM)
+ if (value === currentValue || (value === null && currentValue === null)) return
+ const next = new URLSearchParams(current.toString())
+ if (value === null) {
+ next.delete(ADD_CONNECTOR_SEARCH_PARAM)
+ } else {
+ next.set(ADD_CONNECTOR_SEARCH_PARAM, value)
+ }
+ const qs = next.toString()
+ router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false })
+ },
+ [pathname, router]
+ )
+ const setShowAddConnectorModal = useCallback(
+ (open: boolean) => updateAddConnectorParam(open ? '' : null),
+ [updateAddConnectorParam]
+ )
const {
isOpen: isContextMenuOpen,
@@ -340,8 +367,6 @@ export function KnowledgeBase({
prevHadSyncingRef.current = hasSyncingConnectors
}, [hasSyncingConnectors, refreshKnowledgeBase, refreshDocuments])
- const router = useRouter()
-
const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base'
const error = knowledgeBaseError || documentsError
@@ -1254,7 +1279,13 @@ export function KnowledgeBase({
/>
{showAddConnectorModal && (
-
+
)}
{documentToRename && (
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
index 223091360bc..ce218132643 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
@@ -44,14 +44,22 @@ const CONNECTOR_ENTRIES = Object.entries(CONNECTOR_REGISTRY)
interface AddConnectorModalProps {
open: boolean
onOpenChange: (open: boolean) => void
+ onConnectorTypeChange?: (connectorType: string | null) => void
knowledgeBaseId: string
+ initialConnectorType?: string | null
}
type Step = 'select-type' | 'configure'
-export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddConnectorModalProps) {
- const [step, setStep] = useState('select-type')
- const [selectedType, setSelectedType] = useState(null)
+export function AddConnectorModal({
+ open,
+ onOpenChange,
+ onConnectorTypeChange,
+ knowledgeBaseId,
+ initialConnectorType,
+}: AddConnectorModalProps) {
+ const [step, setStep] = useState(() => (initialConnectorType ? 'configure' : 'select-type'))
+ const [selectedType, setSelectedType] = useState(initialConnectorType ?? null)
const [sourceConfig, setSourceConfig] = useState>({})
const [syncInterval, setSyncInterval] = useState(1440)
const [selectedCredentialId, setSelectedCredentialId] = useState(null)
@@ -151,6 +159,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
setError(null)
setSearchTerm('')
setStep('configure')
+ onConnectorTypeChange?.(type)
}
const handleFieldChange = useCallback(
@@ -286,7 +295,10 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
@@ -565,6 +577,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
workspaceId={workspaceId}
knowledgeBaseId={knowledgeBaseId}
credentialCount={credentials.length}
+ connectorType={selectedType ?? undefined}
/>
)}
>
diff --git a/apps/sim/app/workspace/[workspaceId]/task/[taskId]/loading.tsx b/apps/sim/app/workspace/[workspaceId]/task/[taskId]/loading.tsx
deleted file mode 100644
index acc047d3a4b..00000000000
--- a/apps/sim/app/workspace/[workspaceId]/task/[taskId]/loading.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Loader2 } from 'lucide-react'
-
-export default function TaskLoading() {
- return (
-
- )
-}
diff --git a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx
index 8671a709271..a93cb915c69 100644
--- a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx
@@ -111,8 +111,6 @@ function normalizeWorkflowState(input?: any): WorkflowState | null {
lastUpdate: input.lastUpdate,
metadata: input.metadata,
variables: input.variables,
- deploymentStatuses: input.deploymentStatuses,
- needsRedeployment: input.needsRedeployment,
dragStartPosition: input.dragStartPosition ?? null,
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx
index f6ce4e95c2b..4133728b666 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx
@@ -40,7 +40,6 @@ import { useWorkflowMap } from '@/hooks/queries/workflows'
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
@@ -90,10 +89,7 @@ export function DeployModal({
const params = useParams()
const workspaceId = params?.workspaceId as string
const { navigateToSettings } = useSettingsNavigation()
- const deploymentStatus = useWorkflowRegistry((state) =>
- state.getWorkflowDeploymentStatus(workflowId)
- )
- const isDeployed = deploymentStatus?.isDeployed ?? isDeployedProp
+ const isDeployed = isDeployedProp
const { data: workflowMap = {} } = useWorkflowMap(workspaceId)
const workflowMetadata = workflowId ? workflowMap[workflowId] : undefined
const workflowWorkspaceId = workflowMetadata?.workspaceId ?? null
@@ -381,8 +377,6 @@ export function DeployModal({
invalidateDeploymentQueries(queryClient, workflowId)
- useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
-
if (chatSuccessTimeoutRef.current) {
clearTimeout(chatSuccessTimeoutRef.current)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx
index 78081e6eeac..24a0975325c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx
@@ -9,7 +9,7 @@ import {
useDeployment,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
-import { useDeployedWorkflowState } from '@/hooks/queries/deployments'
+import { useDeployedWorkflowState, useDeploymentInfo } from '@/hooks/queries/deployments'
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -25,10 +25,10 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
const isRegistryLoading = hydrationPhase === 'idle' || hydrationPhase === 'state-loading'
const { hasBlocks } = useCurrentWorkflow()
- const deploymentStatus = useWorkflowRegistry((state) =>
- state.getWorkflowDeploymentStatus(activeWorkflowId)
- )
- const isDeployed = deploymentStatus?.isDeployed || false
+ const { data: deploymentInfo } = useDeploymentInfo(activeWorkflowId, {
+ enabled: !isRegistryLoading,
+ })
+ const isDeployed = deploymentInfo?.isDeployed ?? false
const isDeployedStateEnabled = Boolean(activeWorkflowId) && isDeployed && !isRegistryLoading
const {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts
index 7187bf7fc95..41b394b8a77 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts
@@ -1,7 +1,7 @@
import { useMemo } from 'react'
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
-import { useVariablesStore } from '@/stores/panel/variables/store'
+import { useVariablesStore } from '@/stores/variables/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx
index 68256664a9c..620d38c2e83 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx
@@ -31,8 +31,8 @@ import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/
import { getBlock } from '@/blocks'
import type { BlockConfig } from '@/blocks/types'
import { normalizeName } from '@/executor/constants'
-import type { Variable } from '@/stores/panel'
-import { useVariablesStore } from '@/stores/panel'
+import { useVariablesStore } from '@/stores/variables/store'
+import type { Variable } from '@/stores/variables/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx
index 3cb55879312..c71a48f77bc 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx
@@ -19,8 +19,8 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
-import type { Variable } from '@/stores/panel'
-import { useVariablesStore } from '@/stores/panel'
+import { useVariablesStore } from '@/stores/variables/store'
+import type { Variable } from '@/stores/variables/types'
interface VariableAssignment {
id: string
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts
index 3986164f0fd..555496c899a 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts
@@ -5,8 +5,8 @@ import { useParams } from 'next/navigation'
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
import type { SubBlockConfig } from '@/blocks/types'
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
+import { usePersonalEnvironment } from '@/hooks/queries/environment'
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
-import { useEnvironmentStore } from '@/stores/settings/environment'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useDependsOnGate } from './use-depends-on-gate'
import { useSubBlockValue } from './use-sub-block-value'
@@ -32,7 +32,7 @@ export function useSelectorSetup(
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
const workflowId = (params?.workflowId as string) || activeWorkflowId || ''
- const envVariables = useEnvironmentStore((s) => s.variables)
+ const { data: envVariables = {} } = usePersonalEnvironment()
const { finalDisabled, dependencyValues, canonicalIndex } = useDependsOnGate(
blockId,
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
index 0f349d1b85f..4c5ecbbc570 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
@@ -63,7 +63,8 @@ import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useChatStore } from '@/stores/chat/store'
import { useNotificationStore } from '@/stores/notifications/store'
import type { ChatContext, PanelTab } from '@/stores/panel'
-import { usePanelStore, useVariablesStore as usePanelVariablesStore } from '@/stores/panel'
+import { usePanelStore } from '@/stores/panel'
+import { useVariablesModalStore } from '@/stores/variables/modal'
import { useVariablesStore } from '@/stores/variables/store'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils'
@@ -205,7 +206,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
setIsChatOpen: state.setIsChatOpen,
}))
)
- const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesStore(
+ const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesModalStore(
useShallow((state) => ({
isOpen: state.isOpen,
setIsOpen: state.setIsOpen,
@@ -410,6 +411,17 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
setHasHydrated(true)
}, [setHasHydrated])
+ useEffect(() => {
+ const handler = (e: Event) => {
+ const message = (e as CustomEvent<{ message: string }>).detail?.message
+ if (!message) return
+ setActiveTab('copilot')
+ copilotSendMessage(message)
+ }
+ window.addEventListener('mothership-send-message', handler)
+ return () => window.removeEventListener('mothership-send-message', handler)
+ }, [setActiveTab, copilotSendMessage])
+
/**
* Handles tab click events
*/
@@ -482,7 +494,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
throw new Error('No workflow state found')
}
- const workflowVariables = usePanelVariablesStore
+ const workflowVariables = useVariablesStore
.getState()
.getVariablesByWorkflowId(activeWorkflowId)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx
index 7c182e285c6..a5fb1615c45 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx
@@ -27,15 +27,15 @@ import {
usePreventZoom,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
-import { useVariablesStore as usePanelVariablesStore } from '@/stores/panel'
import {
getVariablesPosition,
MAX_VARIABLES_HEIGHT,
MAX_VARIABLES_WIDTH,
MIN_VARIABLES_HEIGHT,
MIN_VARIABLES_WIDTH,
- useVariablesStore,
-} from '@/stores/variables/store'
+ useVariablesModalStore,
+} from '@/stores/variables/modal'
+import { useVariablesStore } from '@/stores/variables/store'
import type { Variable } from '@/stores/variables/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -96,7 +96,7 @@ export function Variables() {
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
const { isOpen, position, width, height, setIsOpen, setPosition, setDimensions } =
- useVariablesStore(
+ useVariablesModalStore(
useShallow((s) => ({
isOpen: s.isOpen,
position: s.position,
@@ -108,7 +108,7 @@ export function Variables() {
}))
)
- const variables = usePanelVariablesStore((s) => s.variables)
+ const variables = useVariablesStore((s) => s.variables)
const { collaborativeUpdateVariable, collaborativeAddVariable, collaborativeDeleteVariable } =
useCollaborativeWorkflow()
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
index b7b04f38bb2..bb676157396 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
@@ -48,7 +48,7 @@ import { useSkills } from '@/hooks/queries/skills'
import { useTablesList } from '@/hooks/queries/tables'
import { useWorkflowMap } from '@/hooks/queries/workflows'
import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
-import { useVariablesStore } from '@/stores/panel'
+import { useVariablesStore } from '@/stores/variables/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { wouldCreateCycle } from '@/stores/workflows/workflow/utils'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow.ts
index a74573b07b0..85143170e5c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow.ts
@@ -2,7 +2,6 @@ import { useMemo } from 'react'
import type { Edge } from 'reactflow'
import { useShallow } from 'zustand/react/shallow'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
-import type { DeploymentStatus } from '@/stores/workflows/registry/types'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
@@ -16,8 +15,6 @@ export interface CurrentWorkflow {
loops: Record
parallels: Record
lastSaved?: number
- deploymentStatuses?: Record
- needsRedeployment?: boolean
// Mode information
isDiffMode: boolean
@@ -48,8 +45,6 @@ export function useCurrentWorkflow(): CurrentWorkflow {
loops: state.loops,
parallels: state.parallels,
lastSaved: state.lastSaved,
- deploymentStatuses: state.deploymentStatuses,
- needsRedeployment: state.needsRedeployment,
}))
)
@@ -78,8 +73,6 @@ export function useCurrentWorkflow(): CurrentWorkflow {
loops: activeWorkflow.loops || {},
parallels: activeWorkflow.parallels || {},
lastSaved: activeWorkflow.lastSaved,
- deploymentStatuses: activeWorkflow.deploymentStatuses,
- needsRedeployment: activeWorkflow.needsRedeployment,
// Mode information - update to reflect ready state
isDiffMode: hasActiveDiff && isShowingDiff,
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
index 2e8e4b8ce4a..8b9ca1fa0a9 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
@@ -36,8 +36,6 @@ import { useExecutionStream } from '@/hooks/use-execution-stream'
import { WorkflowValidationError } from '@/serializer'
import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution'
import { useNotificationStore } from '@/stores/notifications'
-import { useVariablesStore } from '@/stores/panel'
-import { useEnvironmentStore } from '@/stores/settings/environment'
import {
clearExecutionPointer,
consolePersistence,
@@ -45,6 +43,7 @@ import {
saveExecutionPointer,
useTerminalConsoleStore,
} from '@/stores/terminal'
+import { useVariablesStore } from '@/stores/variables/store'
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
@@ -120,7 +119,6 @@ export function useWorkflowExecution() {
}))
)
const hasHydrated = useTerminalConsoleStore((s) => s._hasHydrated)
- const getAllVariables = useEnvironmentStore((s) => s.getAllVariables)
const { getVariablesByWorkflowId, variables } = useVariablesStore(
useShallow((s) => ({
getVariablesByWorkflowId: s.getVariablesByWorkflowId,
@@ -744,7 +742,6 @@ export function useWorkflowExecution() {
activeWorkflowId,
currentWorkflow,
toggleConsole,
- getAllVariables,
getVariablesByWorkflowId,
setIsExecuting,
setIsDebugging,
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/loading.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/loading.tsx
deleted file mode 100644
index 6a65da73c7e..00000000000
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/loading.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Loader2 } from 'lucide-react'
-
-export default function WorkflowLoading() {
- return (
-
- )
-}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils.ts
index 3b4d5a73ee5..69a15ec7d67 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils.ts
@@ -124,8 +124,7 @@ export async function applyAutoLayoutAndUpdateStore(
try {
useWorkflowStore.getState().updateLastSaved()
- const { deploymentStatuses, needsRedeployment, dragStartPosition, ...stateToSave } =
- newWorkflowState
+ const { dragStartPosition, ...stateToSave } = newWorkflowState
const cleanedWorkflowState = {
...stateToSave,
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
index 57fd7a41706..0a8c196c60c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
@@ -85,7 +85,7 @@ import { useSearchModalStore } from '@/stores/modals/search/store'
import { useNotificationStore } from '@/stores/notifications'
import { usePanelEditorStore } from '@/stores/panel'
import { useUndoRedoStore } from '@/stores/undo-redo'
-import { useVariablesStore } from '@/stores/variables/store'
+import { useVariablesModalStore } from '@/stores/variables/modal'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils'
@@ -265,7 +265,7 @@ const WorkflowContent = React.memo(
const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance, {
embedded,
})
- const { emitCursorUpdate } = useSocket()
+ const { emitCursorUpdate, joinWorkflow, leaveWorkflow } = useSocket()
useDynamicHandleRefresh()
const workspaceId = propWorkspaceId || (params.workspaceId as string)
@@ -273,6 +273,14 @@ const WorkflowContent = React.memo(
const addNotification = useNotificationStore((state) => state.addNotification)
+ useEffect(() => {
+ if (!embedded || !workflowIdParam) return
+ joinWorkflow(workflowIdParam)
+ return () => {
+ leaveWorkflow()
+ }
+ }, [embedded, workflowIdParam, joinWorkflow, leaveWorkflow])
+
useOAuthReturnForWorkflow(workflowIdParam)
const {
@@ -337,7 +345,7 @@ const WorkflowContent = React.memo(
autoConnectRef.current = isAutoConnectEnabled
// Panel open states for context menu
- const isVariablesOpen = useVariablesStore((state) => state.isOpen)
+ const isVariablesOpen = useVariablesModalStore((state) => state.isOpen)
const isChatOpen = useChatStore((state) => state.isChatOpen)
const snapGrid: [number, number] = useMemo(
@@ -1374,7 +1382,7 @@ const WorkflowContent = React.memo(
}, [router, workspaceId, workflowIdParam])
const handleContextToggleVariables = useCallback(() => {
- const { isOpen, setIsOpen } = useVariablesStore.getState()
+ const { isOpen, setIsOpen } = useVariablesModalStore.getState()
setIsOpen(!isOpen)
}, [])
@@ -2144,12 +2152,9 @@ const WorkflowContent = React.memo(
const handleCanvasPointerMove = useCallback(
(event: React.PointerEvent) => {
- const target = event.currentTarget as HTMLElement
- const bounds = target.getBoundingClientRect()
-
const position = screenToFlowPosition({
- x: event.clientX - bounds.left,
- y: event.clientY - bounds.top,
+ x: event.clientX,
+ y: event.clientY,
})
emitCursorUpdate(position)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx
index f2c78c680c3..f6c0f9c5b5f 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx
@@ -13,7 +13,7 @@ import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils
import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
import { getBlock } from '@/blocks'
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
-import { useVariablesStore } from '@/stores/panel/variables/store'
+import { useVariablesStore } from '@/stores/variables/store'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
/** Execution status for blocks in preview mode */
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx
index 23c34bfdea4..51a9533e79d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx
@@ -343,7 +343,11 @@ export function SearchModal({
'-translate-x-1/2 fixed top-[15%] z-50 w-[500px] rounded-xl border-[4px] border-black/[0.06] bg-[var(--bg)] shadow-[0_24px_80px_-16px_rgba(0,0,0,0.15)] dark:border-white/[0.06] dark:shadow-[0_24px_80px_-16px_rgba(0,0,0,0.4)]',
open ? 'visible opacity-100' : 'invisible opacity-0'
)}
- style={{ left: 'calc(var(--sidebar-width) / 2 + 50%)' }}
+ style={{
+ left: isOnWorkflowPage
+ ? 'calc(50% + (var(--sidebar-width) - var(--panel-width)) / 2)'
+ : 'calc(var(--sidebar-width) / 2 + 50%)',
+ }}
>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/loading.tsx b/apps/sim/app/workspace/[workspaceId]/w/loading.tsx
deleted file mode 100644
index 9bb97126c6a..00000000000
--- a/apps/sim/app/workspace/[workspaceId]/w/loading.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Loader2 } from 'lucide-react'
-
-export default function WorkflowsLoading() {
- return (
-
- )
-}
diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx
index 6a27bd3a664..91a415df132 100644
--- a/apps/sim/app/workspace/providers/socket-provider.tsx
+++ b/apps/sim/app/workspace/providers/socket-provider.tsx
@@ -90,6 +90,7 @@ interface SocketContextType {
onSelectionUpdate: (handler: (data: any) => void) => void
onWorkflowDeleted: (handler: (data: any) => void) => void
onWorkflowReverted: (handler: (data: any) => void) => void
+ onWorkflowUpdated: (handler: (data: any) => void) => void
onOperationConfirmed: (handler: (data: any) => void) => void
onOperationFailed: (handler: (data: any) => void) => void
}
@@ -118,6 +119,7 @@ const SocketContext = createContext
({
onSelectionUpdate: () => {},
onWorkflowDeleted: () => {},
onWorkflowReverted: () => {},
+ onWorkflowUpdated: () => {},
onOperationConfirmed: () => {},
onOperationFailed: () => {},
})
@@ -155,6 +157,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
selectionUpdate?: (data: any) => void
workflowDeleted?: (data: any) => void
workflowReverted?: (data: any) => void
+ workflowUpdated?: (data: any) => void
operationConfirmed?: (data: any) => void
operationFailed?: (data: any) => void
}>({})
@@ -334,7 +337,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
socketInstance.on('join-workflow-success', ({ workflowId, presenceUsers }) => {
isRejoiningRef.current = false
// Ignore stale success responses from previous navigation
- if (workflowId !== urlWorkflowIdRef.current) {
+ if (urlWorkflowIdRef.current && workflowId !== urlWorkflowIdRef.current) {
logger.debug(`Ignoring stale join-workflow-success for ${workflowId}`)
return
}
@@ -382,6 +385,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
eventHandlers.current.workflowReverted?.(data)
})
+ socketInstance.on('workflow-updated', (data) => {
+ logger.info(`Workflow ${data.workflowId} has been updated externally`)
+ eventHandlers.current.workflowUpdated?.(data)
+ })
+
const rehydrateWorkflowStores = async (workflowId: string, workflowState: any) => {
const [
{ useOperationQueueStore },
@@ -424,7 +432,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
loops: workflowState.loops || {},
parallels: workflowState.parallels || {},
lastSaved: workflowState.lastSaved || Date.now(),
- deploymentStatuses: workflowState.deploymentStatuses || {},
})
useSubBlockStore.setState((state: any) => ({
@@ -804,6 +811,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
eventHandlers.current.workflowReverted = handler
}, [])
+ const onWorkflowUpdated = useCallback((handler: (data: any) => void) => {
+ eventHandlers.current.workflowUpdated = handler
+ }, [])
+
const onOperationConfirmed = useCallback((handler: (data: any) => void) => {
eventHandlers.current.operationConfirmed = handler
}, [])
@@ -837,6 +848,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
onSelectionUpdate,
onWorkflowDeleted,
onWorkflowReverted,
+ onWorkflowUpdated,
onOperationConfirmed,
onOperationFailed,
}),
@@ -864,6 +876,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
onSelectionUpdate,
onWorkflowDeleted,
onWorkflowReverted,
+ onWorkflowUpdated,
onOperationConfirmed,
onOperationFailed,
]
diff --git a/apps/sim/components/emcn/components/modal/modal.tsx b/apps/sim/components/emcn/components/modal/modal.tsx
index 2932bb2d5c9..f9afb95b6d6 100644
--- a/apps/sim/components/emcn/components/modal/modal.tsx
+++ b/apps/sim/components/emcn/components/modal/modal.tsx
@@ -40,6 +40,7 @@ import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { X } from 'lucide-react'
+import { usePathname } from 'next/navigation'
import { cn } from '@/lib/core/utils/cn'
import { Button } from '../button/button'
@@ -50,13 +51,6 @@ import { Button } from '../button/button'
const ANIMATION_CLASSES =
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=open]:animate-in motion-reduce:animate-none'
-/**
- * Modal content animation classes.
- * We keep only the slide animations (no zoom) to stabilize positioning while avoiding scale effects.
- */
-const CONTENT_ANIMATION_CLASSES =
- 'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[50%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[50%] motion-reduce:animate-none'
-
/**
* Root modal component. Manages open state.
*/
@@ -145,6 +139,8 @@ const ModalContent = React.forwardRef<
ModalContentProps
>(({ className, children, showClose = true, size = 'md', style, ...props }, ref) => {
const [isInteractionReady, setIsInteractionReady] = React.useState(false)
+ const pathname = usePathname()
+ const isWorkflowPage = pathname?.includes('/w/') ?? false
React.useEffect(() => {
const timer = setTimeout(() => setIsInteractionReady(true), 100)
@@ -157,14 +153,15 @@ const ModalContent = React.forwardRef<
{
diff --git a/apps/sim/hooks/queries/deployments.ts b/apps/sim/hooks/queries/deployments.ts
index 62f5e970d3b..7bcf8be904d 100644
--- a/apps/sim/hooks/queries/deployments.ts
+++ b/apps/sim/hooks/queries/deployments.ts
@@ -5,7 +5,6 @@ import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tansta
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
import { fetchDeploymentVersionState } from '@/hooks/queries/utils/fetch-deployment-version-state'
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeploymentQueries')
@@ -321,7 +320,6 @@ interface DeployWorkflowResult {
*/
export function useDeployWorkflow() {
const queryClient = useQueryClient()
- const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
return useMutation({
mutationFn: async ({
@@ -351,18 +349,12 @@ export function useDeployWorkflow() {
warnings: data.warnings,
}
},
- onSuccess: (data, variables) => {
- logger.info('Workflow deployed successfully', { workflowId: variables.workflowId })
-
- setDeploymentStatus(
- variables.workflowId,
- data.isDeployed,
- data.deployedAt ? new Date(data.deployedAt) : undefined,
- data.apiKey
- )
-
- useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(variables.workflowId, false)
-
+ onSettled: (_data, error, variables) => {
+ if (error) {
+ logger.error('Failed to deploy workflow', { error })
+ } else {
+ logger.info('Workflow deployed successfully', { workflowId: variables.workflowId })
+ }
return Promise.all([
invalidateDeploymentQueries(queryClient, variables.workflowId),
queryClient.invalidateQueries({
@@ -370,9 +362,6 @@ export function useDeployWorkflow() {
}),
])
},
- onError: (error) => {
- logger.error('Failed to deploy workflow', { error })
- },
})
}
@@ -389,7 +378,6 @@ interface UndeployWorkflowVariables {
*/
export function useUndeployWorkflow() {
const queryClient = useQueryClient()
- const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
return useMutation({
mutationFn: async ({ workflowId }: UndeployWorkflowVariables): Promise => {
@@ -402,11 +390,12 @@ export function useUndeployWorkflow() {
throw new Error(errorData.error || 'Failed to undeploy workflow')
}
},
- onSuccess: (_, variables) => {
- logger.info('Workflow undeployed successfully', { workflowId: variables.workflowId })
-
- setDeploymentStatus(variables.workflowId, false)
-
+ onSettled: (_data, error, variables) => {
+ if (error) {
+ logger.error('Failed to undeploy workflow', { error })
+ } else {
+ logger.info('Workflow undeployed successfully', { workflowId: variables.workflowId })
+ }
return Promise.all([
invalidateDeploymentQueries(queryClient, variables.workflowId),
queryClient.invalidateQueries({
@@ -414,9 +403,6 @@ export function useUndeployWorkflow() {
}),
])
},
- onError: (error) => {
- logger.error('Failed to undeploy workflow', { error })
- },
})
}
@@ -613,7 +599,6 @@ interface ActivateVersionResult {
*/
export function useActivateDeploymentVersion() {
const queryClient = useQueryClient()
- const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
return useMutation({
mutationFn: async ({
@@ -663,20 +648,13 @@ export function useActivateDeploymentVersion() {
)
}
},
- onSuccess: (data, variables) => {
- logger.info('Deployment version activated', {
- workflowId: variables.workflowId,
- version: variables.version,
- })
-
- setDeploymentStatus(
- variables.workflowId,
- true,
- data.deployedAt ? new Date(data.deployedAt) : undefined,
- data.apiKey
- )
- },
- onSettled: (_, __, variables) => {
+ onSettled: (_data, error, variables) => {
+ if (!error) {
+ logger.info('Deployment version activated', {
+ workflowId: variables.workflowId,
+ version: variables.version,
+ })
+ }
return invalidateDeploymentQueries(queryClient, variables.workflowId)
},
})
diff --git a/apps/sim/hooks/queries/environment.ts b/apps/sim/hooks/queries/environment.ts
index 01f6f6b9929..6d9c2dab94a 100644
--- a/apps/sim/hooks/queries/environment.ts
+++ b/apps/sim/hooks/queries/environment.ts
@@ -1,4 +1,3 @@
-import { useEffect } from 'react'
import { createLogger } from '@sim/logger'
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type { WorkspaceEnvironmentData } from '@/lib/environment/api'
@@ -6,7 +5,6 @@ import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/envir
import { workspaceCredentialKeys } from '@/hooks/queries/credentials'
import { API_ENDPOINTS } from '@/stores/constants'
import type { EnvironmentVariable } from '@/stores/settings/environment'
-import { useEnvironmentStore } from '@/stores/settings/environment'
export type { WorkspaceEnvironmentData } from '@/lib/environment/api'
export type { EnvironmentVariable } from '@/stores/settings/environment'
@@ -22,29 +20,16 @@ export const environmentKeys = {
workspace: (workspaceId: string) => [...environmentKeys.all, 'workspace', workspaceId] as const,
}
-/**
- * Environment Variable Types
- */
/**
* Hook to fetch personal environment variables
*/
export function usePersonalEnvironment() {
- const setVariables = useEnvironmentStore((state) => state.setVariables)
-
- const query = useQuery({
+ return useQuery({
queryKey: environmentKeys.personal(),
queryFn: ({ signal }) => fetchPersonalEnvironment(signal),
staleTime: 60 * 1000, // 1 minute
placeholderData: keepPreviousData,
})
-
- useEffect(() => {
- if (query.data) {
- setVariables(query.data)
- }
- }, [query.data, setVariables])
-
- return query
}
/**
diff --git a/apps/sim/hooks/queries/utils/custom-tool-cache.test.ts b/apps/sim/hooks/queries/utils/custom-tool-cache.test.ts
deleted file mode 100644
index c4b7fd54021..00000000000
--- a/apps/sim/hooks/queries/utils/custom-tool-cache.test.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * @vitest-environment node
- */
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const { getQueryDataMock } = vi.hoisted(() => ({
- getQueryDataMock: vi.fn(),
-}))
-
-vi.mock('@/app/_shell/providers/get-query-client', () => ({
- getQueryClient: vi.fn(() => ({
- getQueryData: getQueryDataMock,
- })),
-}))
-
-import { getCustomTool, getCustomTools } from '@/hooks/queries/utils/custom-tool-cache'
-
-describe('custom tool cache helpers', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- it('reads workspace-scoped custom tools from the cache', () => {
- const tools = [{ id: 'tool-1', title: 'Weather', schema: {}, code: '', workspaceId: 'ws-1' }]
- getQueryDataMock.mockReturnValue(tools)
-
- expect(getCustomTools('ws-1')).toBe(tools)
- expect(getQueryDataMock).toHaveBeenCalledWith(['customTools', 'list', 'ws-1'])
- })
-
- it('resolves custom tools by id or title', () => {
- getQueryDataMock.mockReturnValue([
- { id: 'tool-1', title: 'Weather', schema: {}, code: '', workspaceId: 'ws-1' },
- ])
-
- expect(getCustomTool('tool-1', 'ws-1')?.title).toBe('Weather')
- expect(getCustomTool('Weather', 'ws-1')?.id).toBe('tool-1')
- })
-})
diff --git a/apps/sim/hooks/queries/utils/custom-tool-cache.ts b/apps/sim/hooks/queries/utils/custom-tool-cache.ts
deleted file mode 100644
index 407911ddb37..00000000000
--- a/apps/sim/hooks/queries/utils/custom-tool-cache.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { getQueryClient } from '@/app/_shell/providers/get-query-client'
-import type { CustomToolDefinition } from '@/hooks/queries/custom-tools'
-import { customToolsKeys } from '@/hooks/queries/utils/custom-tool-keys'
-
-/**
- * Reads custom tools for a workspace directly from the React Query cache.
- */
-export function getCustomTools(workspaceId: string): CustomToolDefinition[] {
- return (
- getQueryClient().getQueryData(customToolsKeys.list(workspaceId)) ?? []
- )
-}
-
-/**
- * Resolves a custom tool from the cache by id or title.
- */
-export function getCustomTool(
- identifier: string,
- workspaceId: string
-): CustomToolDefinition | undefined {
- const tools = getCustomTools(workspaceId)
- return tools.find((tool) => tool.id === identifier || tool.title === identifier)
-}
diff --git a/apps/sim/hooks/selectors/use-selector-query.ts b/apps/sim/hooks/selectors/use-selector-query.ts
index 0323c7f5e7d..a4444b762aa 100644
--- a/apps/sim/hooks/selectors/use-selector-query.ts
+++ b/apps/sim/hooks/selectors/use-selector-query.ts
@@ -1,9 +1,9 @@
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
+import { usePersonalEnvironment } from '@/hooks/queries/environment'
import { getSelectorDefinition, mergeOption } from '@/hooks/selectors/registry'
import type { SelectorKey, SelectorOption, SelectorQueryArgs } from '@/hooks/selectors/types'
-import { useEnvironmentStore } from '@/stores/settings/environment'
interface SelectorHookArgs extends Omit {
search?: string
@@ -31,7 +31,7 @@ export function useSelectorOptionDetail(
key: SelectorKey,
args: SelectorHookArgs & { detailId?: string }
) {
- const envVariables = useEnvironmentStore((s) => s.variables)
+ const { data: envVariables = {} } = usePersonalEnvironment()
const definition = getSelectorDefinition(key)
const resolvedDetailId = useMemo(() => {
diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts
index 4093bed8b20..60c36eeb645 100644
--- a/apps/sim/hooks/use-collaborative-workflow.ts
+++ b/apps/sim/hooks/use-collaborative-workflow.ts
@@ -19,8 +19,9 @@ import {
} from '@/socket/constants'
import { useNotificationStore } from '@/stores/notifications'
import { registerEmitFunctions, useOperationQueue } from '@/stores/operation-queue/store'
-import { usePanelEditorStore, useVariablesStore } from '@/stores/panel'
+import { usePanelEditorStore } from '@/stores/panel'
import { useCodeUndoRedoStore, useUndoRedoStore } from '@/stores/undo-redo'
+import { useVariablesStore } from '@/stores/variables/store'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -122,6 +123,7 @@ export function useCollaborativeWorkflow() {
onVariableUpdate,
onWorkflowDeleted,
onWorkflowReverted,
+ onWorkflowUpdated,
onOperationConfirmed,
onOperationFailed,
} = useSocket()
@@ -536,82 +538,99 @@ export function useCollaborativeWorkflow() {
}
}
+ const reloadWorkflowFromApi = async (workflowId: string, reason: string): Promise => {
+ const response = await fetch(`/api/workflows/${workflowId}`)
+ if (!response.ok) {
+ logger.error(`Failed to fetch workflow data after ${reason}: ${response.statusText}`)
+ return false
+ }
+
+ const responseData = await response.json()
+ const workflowData = responseData.data
+
+ if (!workflowData?.state) {
+ logger.error(`No state found in workflow data after ${reason}`, { workflowData })
+ return false
+ }
+
+ isApplyingRemoteChange.current = true
+ try {
+ useWorkflowStore.getState().replaceWorkflowState({
+ blocks: workflowData.state.blocks || {},
+ edges: workflowData.state.edges || [],
+ loops: workflowData.state.loops || {},
+ parallels: workflowData.state.parallels || {},
+ lastSaved: workflowData.state.lastSaved || Date.now(),
+ })
+
+ const subblockValues: Record> = {}
+ Object.entries(workflowData.state.blocks || {}).forEach(([blockId, block]) => {
+ const blockState = block as any
+ subblockValues[blockId] = {}
+ Object.entries(blockState.subBlocks || {}).forEach(([subblockId, subblock]) => {
+ subblockValues[blockId][subblockId] = (subblock as any).value
+ })
+ })
+
+ useSubBlockStore.setState((state: any) => ({
+ workflowValues: {
+ ...state.workflowValues,
+ [workflowId]: subblockValues,
+ },
+ }))
+
+ const graph = {
+ blocksById: workflowData.state.blocks || {},
+ edgesById: Object.fromEntries(
+ (workflowData.state.edges || []).map((e: any) => [e.id, e])
+ ),
+ }
+
+ const undoRedoStore = useUndoRedoStore.getState()
+ const stackKeys = Object.keys(undoRedoStore.stacks)
+ stackKeys.forEach((key) => {
+ const [wfId, userId] = key.split(':')
+ if (wfId === workflowId) {
+ undoRedoStore.pruneInvalidEntries(wfId, userId, graph)
+ }
+ })
+
+ logger.info(`Successfully reloaded workflow state after ${reason}`, { workflowId })
+ return true
+ } finally {
+ isApplyingRemoteChange.current = false
+ }
+ }
+
const handleWorkflowReverted = async (data: any) => {
const { workflowId } = data
logger.info(`Workflow ${workflowId} has been reverted to deployed state`)
- // If the reverted workflow is the currently active one, reload the workflow state
- if (activeWorkflowId === workflowId) {
- logger.info(`Currently active workflow ${workflowId} was reverted, reloading state`)
-
- try {
- // Fetch the updated workflow state from the server (which loads from normalized tables)
- const response = await fetch(`/api/workflows/${workflowId}`)
- if (response.ok) {
- const responseData = await response.json()
- const workflowData = responseData.data
-
- if (workflowData?.state) {
- // Update the workflow store with the reverted state
- isApplyingRemoteChange.current = true
- try {
- // Update the main workflow state using the API response
- useWorkflowStore.getState().replaceWorkflowState({
- blocks: workflowData.state.blocks || {},
- edges: workflowData.state.edges || [],
- loops: workflowData.state.loops || {},
- parallels: workflowData.state.parallels || {},
- lastSaved: workflowData.state.lastSaved || Date.now(),
- deploymentStatuses: workflowData.state.deploymentStatuses || {},
- })
+ if (activeWorkflowId !== workflowId) return
- // Update subblock store with reverted values
- const subblockValues: Record> = {}
- Object.entries(workflowData.state.blocks || {}).forEach(([blockId, block]) => {
- const blockState = block as any
- subblockValues[blockId] = {}
- Object.entries(blockState.subBlocks || {}).forEach(([subblockId, subblock]) => {
- subblockValues[blockId][subblockId] = (subblock as any).value
- })
- })
+ try {
+ await reloadWorkflowFromApi(workflowId, 'revert')
+ } catch (error) {
+ logger.error('Error reloading workflow state after revert:', error)
+ }
+ }
- // Update subblock store for this workflow
- useSubBlockStore.setState((state: any) => ({
- workflowValues: {
- ...state.workflowValues,
- [workflowId]: subblockValues,
- },
- }))
+ const handleWorkflowUpdated = async (data: any) => {
+ const { workflowId } = data
+ logger.info(`Workflow ${workflowId} has been updated externally`)
- logger.info(`Successfully loaded reverted workflow state for ${workflowId}`)
+ if (activeWorkflowId !== workflowId) return
- const graph = {
- blocksById: workflowData.state.blocks || {},
- edgesById: Object.fromEntries(
- (workflowData.state.edges || []).map((e: any) => [e.id, e])
- ),
- }
+ const { hasActiveDiff } = useWorkflowDiffStore.getState()
+ if (hasActiveDiff) {
+ logger.info('Skipping workflow-updated: active diff in progress', { workflowId })
+ return
+ }
- const undoRedoStore = useUndoRedoStore.getState()
- const stackKeys = Object.keys(undoRedoStore.stacks)
- stackKeys.forEach((key) => {
- const [wfId, userId] = key.split(':')
- if (wfId === workflowId) {
- undoRedoStore.pruneInvalidEntries(wfId, userId, graph)
- }
- })
- } finally {
- isApplyingRemoteChange.current = false
- }
- } else {
- logger.error('No state found in workflow data after revert', { workflowData })
- }
- } else {
- logger.error(`Failed to fetch workflow data after revert: ${response.statusText}`)
- }
- } catch (error) {
- logger.error('Error reloading workflow state after revert:', error)
- }
+ try {
+ await reloadWorkflowFromApi(workflowId, 'external update')
+ } catch (error) {
+ logger.error('Error reloading workflow state after external update:', error)
}
}
@@ -633,6 +652,7 @@ export function useCollaborativeWorkflow() {
onVariableUpdate(handleVariableUpdate)
onWorkflowDeleted(handleWorkflowDeleted)
onWorkflowReverted(handleWorkflowReverted)
+ onWorkflowUpdated(handleWorkflowUpdated)
onOperationConfirmed(handleOperationConfirmed)
onOperationFailed(handleOperationFailed)
}, [
@@ -641,6 +661,7 @@ export function useCollaborativeWorkflow() {
onVariableUpdate,
onWorkflowDeleted,
onWorkflowReverted,
+ onWorkflowUpdated,
onOperationConfirmed,
onOperationFailed,
activeWorkflowId,
diff --git a/apps/sim/hooks/use-oauth-return.ts b/apps/sim/hooks/use-oauth-return.ts
index ad64918b877..bc260e901e5 100644
--- a/apps/sim/hooks/use-oauth-return.ts
+++ b/apps/sim/hooks/use-oauth-return.ts
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { toast } from '@/components/emcn'
import {
+ ADD_CONNECTOR_SEARCH_PARAM,
consumeOAuthReturnContext,
type OAuthReturnContext,
readOAuthReturnContext,
@@ -98,7 +99,11 @@ export function useOAuthReturnRouter() {
try {
sessionStorage.removeItem(SETTINGS_RETURN_URL_KEY)
} catch {}
- router.replace(`/workspace/${workspaceId}/knowledge/${ctx.knowledgeBaseId}`)
+ const kbUrl = `/workspace/${workspaceId}/knowledge/${ctx.knowledgeBaseId}`
+ const connectorParam = ctx.connectorType
+ ? `?${ADD_CONNECTOR_SEARCH_PARAM}=${encodeURIComponent(ctx.connectorType)}`
+ : ''
+ router.replace(`${kbUrl}${connectorParam}`)
return
}
}, [router, workspaceId])
diff --git a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts
index 9a9cb88ca8a..cad83d1ec4c 100644
--- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts
+++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts
@@ -40,7 +40,11 @@ import {
XCircle,
Zap,
} from 'lucide-react'
-import { getCustomTool } from '@/hooks/queries/utils/custom-tool-cache'
+import { getQueryClient } from '@/app/_shell/providers/get-query-client'
+import type { CustomToolDefinition } from '@/hooks/queries/custom-tools'
+import type { WorkflowDeploymentInfo } from '@/hooks/queries/deployments'
+import { deploymentKeys } from '@/hooks/queries/deployments'
+import { customToolsKeys } from '@/hooks/queries/utils/custom-tool-keys'
import { getWorkflowById } from '@/hooks/queries/utils/workflow-cache'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -445,7 +449,8 @@ const META_deploy_api: ToolMetadata = {
const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
const isAlreadyDeployed = workflowId
- ? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed
+ ? (getQueryClient().getQueryData(deploymentKeys.info(workflowId))
+ ?.isDeployed ?? false)
: false
let actionText = action
@@ -1053,7 +1058,11 @@ const META_manage_custom_tool: ToolMetadata = {
let toolName = params?.schema?.function?.name
if (!toolName && params?.toolId && workspaceId) {
try {
- const tool = getCustomTool(params.toolId, workspaceId)
+ const tools =
+ getQueryClient().getQueryData(
+ customToolsKeys.list(workspaceId)
+ ) ?? []
+ const tool = tools.find((t) => t.id === params.toolId || t.title === params.toolId)
toolName = tool?.schema?.function?.name
} catch {
// Ignore errors accessing cache
diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts
index eb0a0f23ed6..7be066d2989 100644
--- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts
+++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts
@@ -7,6 +7,7 @@ import {
type BaseServerTool,
type ServerToolContext,
} from '@/lib/copilot/tools/server/base-tool'
+import { env } from '@/lib/core/config/env'
import { applyTargetedLayout, getTargetedLayoutImpact } from '@/lib/workflows/autolayout'
import {
DEFAULT_HORIZONTAL_SPACING,
@@ -287,6 +288,18 @@ export const editWorkflowServerTool: BaseServerTool
logger.info('Workflow state persisted to database', { workflowId })
+ const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002'
+ fetch(`${socketUrl}/api/workflow-updated`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-api-key': env.INTERNAL_API_SECRET,
+ },
+ body: JSON.stringify({ workflowId }),
+ }).catch((error) => {
+ logger.warn('Failed to notify socket server of workflow update', { workflowId, error })
+ })
+
const sanitizationWarnings = validation.warnings.length > 0 ? validation.warnings : undefined
return {
diff --git a/apps/sim/lib/credentials/client-state.ts b/apps/sim/lib/credentials/client-state.ts
index a7216677c4c..fcaf88fa121 100644
--- a/apps/sim/lib/credentials/client-state.ts
+++ b/apps/sim/lib/credentials/client-state.ts
@@ -91,6 +91,8 @@ export function clearPendingCredentialCreateRequest() {
window.sessionStorage.removeItem(PENDING_CREDENTIAL_CREATE_REQUEST_KEY)
}
+export const ADD_CONNECTOR_SEARCH_PARAM = 'addConnector' as const
+
const OAUTH_RETURN_CONTEXT_KEY = 'sim.oauth-return-context'
export type OAuthReturnOrigin = 'workflow' | 'integrations' | 'kb-connectors'
@@ -116,6 +118,7 @@ interface OAuthReturnIntegrations extends OAuthReturnBase {
interface OAuthReturnKBConnectors extends OAuthReturnBase {
origin: 'kb-connectors'
knowledgeBaseId: string
+ connectorType?: string
}
export type OAuthReturnContext =
diff --git a/apps/sim/lib/logs/types.ts b/apps/sim/lib/logs/types.ts
index b9402583704..20f568ab41c 100644
--- a/apps/sim/lib/logs/types.ts
+++ b/apps/sim/lib/logs/types.ts
@@ -2,10 +2,9 @@ import type { Edge } from 'reactflow'
import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types'
import type { ParentIteration, SerializableExecutionState } from '@/executor/execution/types'
import type { BlockLog, NormalizedBlockOutput } from '@/executor/types'
-import type { DeploymentStatus } from '@/stores/workflows/registry/types'
import type { Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
-export type { WorkflowState, Loop, Parallel, DeploymentStatus }
+export type { WorkflowState, Loop, Parallel }
export type WorkflowEdge = Edge
export type { NormalizedBlockOutput, BlockLog }
diff --git a/apps/sim/lib/workflows/defaults.ts b/apps/sim/lib/workflows/defaults.ts
index 7de0c058a7d..b0b6f4de2e3 100644
--- a/apps/sim/lib/workflows/defaults.ts
+++ b/apps/sim/lib/workflows/defaults.ts
@@ -123,8 +123,6 @@ export function buildDefaultWorkflowArtifacts(): DefaultWorkflowArtifacts {
loops: {},
parallels: {},
lastSaved: Date.now(),
- deploymentStatuses: {},
- needsRedeployment: false,
}
return {
diff --git a/apps/sim/lib/workflows/persistence/duplicate.ts b/apps/sim/lib/workflows/persistence/duplicate.ts
index 1895068ab4d..bbc07a5ad15 100644
--- a/apps/sim/lib/workflows/persistence/duplicate.ts
+++ b/apps/sim/lib/workflows/persistence/duplicate.ts
@@ -14,7 +14,7 @@ import {
deduplicateWorkflowName,
} from '@/lib/workflows/utils'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
-import type { Variable } from '@/stores/panel/variables/types'
+import type { Variable } from '@/stores/variables/types'
import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types'
const logger = createLogger('WorkflowDuplicateHelper')
diff --git a/apps/sim/lib/workflows/persistence/utils.test.ts b/apps/sim/lib/workflows/persistence/utils.test.ts
index ef4c00fab84..e5e02b74a6d 100644
--- a/apps/sim/lib/workflows/persistence/utils.test.ts
+++ b/apps/sim/lib/workflows/persistence/utils.test.ts
@@ -985,7 +985,6 @@ describe('Database Helpers', () => {
edges: loadedState!.edges,
loops: {},
parallels: {},
- deploymentStatuses: {},
}
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
diff --git a/apps/sim/lib/workflows/variables/variable-manager.ts b/apps/sim/lib/workflows/variables/variable-manager.ts
index 7807d466c0e..61e5dfaee39 100644
--- a/apps/sim/lib/workflows/variables/variable-manager.ts
+++ b/apps/sim/lib/workflows/variables/variable-manager.ts
@@ -1,4 +1,4 @@
-import type { VariableType } from '@/stores/panel/variables/types'
+import type { VariableType } from '@/stores/variables/types'
/**
* Central manager for handling all variable-related operations.
diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts
index 60cfa177e6b..3e8eeeb99bb 100644
--- a/apps/sim/socket/database/operations.ts
+++ b/apps/sim/socket/database/operations.ts
@@ -186,7 +186,6 @@ export async function getWorkflowState(workflowId: string) {
if (normalizedData) {
const finalState = {
- deploymentStatuses: {},
hasActiveWebhook: false,
blocks: normalizedData.blocks,
edges: normalizedData.edges,
diff --git a/apps/sim/stores/index.ts b/apps/sim/stores/index.ts
index d1bbbc9b227..46535c23a6c 100644
--- a/apps/sim/stores/index.ts
+++ b/apps/sim/stores/index.ts
@@ -1,10 +1,9 @@
'use client'
-import { useEffect } from 'react'
import { createLogger } from '@sim/logger'
+import { getQueryClient } from '@/app/_shell/providers/get-query-client'
+import { environmentKeys } from '@/hooks/queries/environment'
import { useExecutionStore } from '@/stores/execution'
-import { useVariablesStore } from '@/stores/panel'
-import { useEnvironmentStore } from '@/stores/settings/environment'
import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -12,198 +11,13 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('Stores')
-// Track initialization state
-let isInitializing = false
-let appFullyInitialized = false
-let dataInitialized = false // Flag for actual data loading completion
-
-/**
- * Initialize the application state and sync system
- * localStorage persistence has been removed - relies on DB and Zustand stores only
- */
-async function initializeApplication(): Promise {
- if (typeof window === 'undefined' || isInitializing) return
-
- isInitializing = true
- appFullyInitialized = false
-
- // Track initialization start time
- const initStartTime = Date.now()
-
- try {
- // Load environment variables directly from DB
- await useEnvironmentStore.getState().loadEnvironmentVariables()
-
- // Mark data as initialized only after sync managers have loaded data from DB
- dataInitialized = true
-
- // Log initialization timing information
- const initDuration = Date.now() - initStartTime
- logger.info(`Application initialization completed in ${initDuration}ms`)
-
- // Mark application as fully initialized
- appFullyInitialized = true
- } catch (error) {
- logger.error('Error during application initialization:', { error })
- // Still mark as initialized to prevent being stuck in initializing state
- appFullyInitialized = true
- // But don't mark data as initialized on error
- dataInitialized = false
- } finally {
- isInitializing = false
- }
-}
-
-/**
- * Checks if application is fully initialized
- */
-export function isAppInitialized(): boolean {
- return appFullyInitialized
-}
-
-/**
- * Checks if data has been loaded from the database
- * This should be checked before any sync operations
- */
-export function isDataInitialized(): boolean {
- return dataInitialized
-}
-
-/**
- * Handle application cleanup before unload
- */
-function handleBeforeUnload(event: BeforeUnloadEvent): void {
- // Check if we're on an authentication page and skip confirmation if we are
- if (typeof window !== 'undefined') {
- const path = window.location.pathname
- // Skip confirmation for auth-related pages
- if (
- path === '/login' ||
- path === '/signup' ||
- path === '/reset-password' ||
- path === '/verify'
- ) {
- return
- }
- }
-
- // Standard beforeunload pattern
- event.preventDefault()
- event.returnValue = ''
-}
-
-/**
- * Clean up sync system
- */
-function cleanupApplication(): void {
- window.removeEventListener('beforeunload', handleBeforeUnload)
- // Note: No sync managers to dispose - Socket.IO handles cleanup
-}
-
-/**
- * Clear all user data when signing out
- * localStorage persistence has been removed
- */
-export async function clearUserData(): Promise {
- if (typeof window === 'undefined') return
-
- try {
- // Note: No sync managers to dispose - Socket.IO handles cleanup
-
- // Reset all stores to their initial state
- resetAllStores()
-
- // Clear localStorage except for essential app settings (minimal usage)
- const keysToKeep = ['next-favicon', 'theme']
- const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
- keysToRemove.forEach((key) => localStorage.removeItem(key))
-
- // Reset application initialization state
- appFullyInitialized = false
- dataInitialized = false
-
- logger.info('User data cleared successfully')
- } catch (error) {
- logger.error('Error clearing user data:', { error })
- }
-}
-
-/**
- * Hook to manage application lifecycle
- */
-export function useAppInitialization() {
- useEffect(() => {
- // Use Promise to handle async initialization
- initializeApplication()
-
- return () => {
- cleanupApplication()
- }
- }, [])
-}
-
-/**
- * Hook to reinitialize the application after successful login
- * Use this in the login success handler or post-login page
- */
-export function useLoginInitialization() {
- useEffect(() => {
- reinitializeAfterLogin()
- }, [])
-}
-
/**
- * Reinitialize the application after login
- * This ensures we load fresh data from the database for the new user
+ * Reset all Zustand stores and React Query caches to initial state.
*/
-export async function reinitializeAfterLogin(): Promise {
- if (typeof window === 'undefined') return
-
- try {
- // Reset application initialization state
- appFullyInitialized = false
- dataInitialized = false
-
- // Note: No sync managers to dispose - Socket.IO handles cleanup
-
- // Clean existing state to avoid stale data
- resetAllStores()
-
- // Reset initialization flags to force a fresh load
- isInitializing = false
-
- // Reinitialize the application
- await initializeApplication()
-
- logger.info('Application reinitialized after login')
- } catch (error) {
- logger.error('Error reinitializing application:', { error })
- }
-}
-
-// Initialize immediately when imported on client
-if (typeof window !== 'undefined') {
- initializeApplication()
-}
-
-// Export all stores
-export {
- useWorkflowStore,
- useWorkflowRegistry,
- useEnvironmentStore,
- useExecutionStore,
- useTerminalConsoleStore,
- useVariablesStore,
- useSubBlockStore,
-}
-
-// Helper function to reset all stores
export const resetAllStores = () => {
- // Reset all stores to initial state
useWorkflowRegistry.setState({
activeWorkflowId: null,
error: null,
- deploymentStatuses: {},
hydration: {
phase: 'idle',
workspaceId: null,
@@ -214,7 +28,7 @@ export const resetAllStores = () => {
})
useWorkflowStore.getState().clear()
useSubBlockStore.getState().clear()
- useEnvironmentStore.getState().reset()
+ getQueryClient().removeQueries({ queryKey: environmentKeys.all })
useExecutionStore.getState().reset()
useTerminalConsoleStore.setState({
workflowEntries: {},
@@ -223,21 +37,24 @@ export const resetAllStores = () => {
isOpen: false,
})
consolePersistence.persist()
- // Custom tools are managed by React Query cache, not a Zustand store
- // Variables store has no tracking to reset; registry hydrates
}
-// Helper function to log all store states
-export const logAllStores = () => {
- const state = {
- workflow: useWorkflowStore.getState(),
- workflowRegistry: useWorkflowRegistry.getState(),
- environment: useEnvironmentStore.getState(),
- execution: useExecutionStore.getState(),
- console: useTerminalConsoleStore.getState(),
- subBlock: useSubBlockStore.getState(),
- variables: useVariablesStore.getState(),
- }
+/**
+ * Clear all user data when signing out.
+ */
+export async function clearUserData(): Promise {
+ if (typeof window === 'undefined') return
- return state
+ try {
+ resetAllStores()
+
+ // Clear localStorage except for essential app settings
+ const keysToKeep = ['next-favicon', 'theme']
+ const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
+ keysToRemove.forEach((key) => localStorage.removeItem(key))
+
+ logger.info('User data cleared successfully')
+ } catch (error) {
+ logger.error('Error clearing user data:', { error })
+ }
}
diff --git a/apps/sim/stores/panel/index.ts b/apps/sim/stores/panel/index.ts
index 13ec5f1eec3..cf2c541411f 100644
--- a/apps/sim/stores/panel/index.ts
+++ b/apps/sim/stores/panel/index.ts
@@ -9,6 +9,3 @@ export { usePanelStore } from './store'
// Toolbar
export { useToolbarStore } from './toolbar'
export type { ChatContext, PanelState, PanelTab } from './types'
-export type { Variable, VariablesStore, VariableType } from './variables'
-// Variables
-export { useVariablesStore } from './variables'
diff --git a/apps/sim/stores/panel/variables/index.ts b/apps/sim/stores/panel/variables/index.ts
deleted file mode 100644
index 6a34a434565..00000000000
--- a/apps/sim/stores/panel/variables/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { useVariablesStore } from './store'
-export type { Variable, VariablesStore, VariableType } from './types'
diff --git a/apps/sim/stores/panel/variables/store.ts b/apps/sim/stores/panel/variables/store.ts
deleted file mode 100644
index e9a7db871c5..00000000000
--- a/apps/sim/stores/panel/variables/store.ts
+++ /dev/null
@@ -1,290 +0,0 @@
-import { createLogger } from '@sim/logger'
-import JSON5 from 'json5'
-import { create } from 'zustand'
-import { devtools } from 'zustand/middleware'
-import { normalizeName } from '@/executor/constants'
-import { useOperationQueueStore } from '@/stores/operation-queue/store'
-import type { Variable, VariablesStore } from '@/stores/panel/variables/types'
-import { useSubBlockStore } from '@/stores/workflows/subblock/store'
-
-const logger = createLogger('VariablesStore')
-
-function validateVariable(variable: Variable): string | undefined {
- try {
- switch (variable.type) {
- case 'number':
- if (Number.isNaN(Number(variable.value))) {
- return 'Not a valid number'
- }
- break
- case 'boolean':
- if (!/^(true|false)$/i.test(String(variable.value).trim())) {
- return 'Expected "true" or "false"'
- }
- break
- case 'object':
- try {
- const valueToEvaluate = String(variable.value).trim()
-
- if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) {
- return 'Not a valid object format'
- }
-
- const parsed = JSON5.parse(valueToEvaluate)
-
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
- return 'Not a valid object'
- }
-
- return undefined
- } catch (e) {
- logger.error('Object parsing error:', e)
- return 'Invalid object syntax'
- }
- case 'array':
- try {
- const parsed = JSON5.parse(String(variable.value))
- if (!Array.isArray(parsed)) {
- return 'Not a valid array'
- }
- } catch {
- return 'Invalid array syntax'
- }
- break
- }
- return undefined
- } catch (e) {
- return e instanceof Error ? e.message : 'Invalid format'
- }
-}
-
-function migrateStringToPlain(variable: Variable): Variable {
- if (variable.type !== 'string') {
- return variable
- }
-
- const updated = {
- ...variable,
- type: 'plain' as const,
- }
-
- return updated
-}
-
-export const useVariablesStore = create()(
- devtools((set, get) => ({
- variables: {},
- isLoading: false,
- error: null,
- isEditing: null,
-
- async loadForWorkflow(workflowId) {
- try {
- set({ isLoading: true, error: null })
- const res = await fetch(`/api/workflows/${workflowId}/variables`, { method: 'GET' })
- if (!res.ok) {
- const text = await res.text().catch(() => '')
- throw new Error(text || `Failed to load variables: ${res.statusText}`)
- }
- const data = await res.json()
- const variables = (data?.data as Record) || {}
- set((state) => {
- const withoutWorkflow = Object.fromEntries(
- Object.entries(state.variables).filter(
- (entry): entry is [string, Variable] => entry[1].workflowId !== workflowId
- )
- )
- return {
- variables: { ...withoutWorkflow, ...variables },
- isLoading: false,
- error: null,
- }
- })
- } catch (e) {
- const message = e instanceof Error ? e.message : 'Unknown error'
- set({ isLoading: false, error: message })
- }
- },
-
- addVariable: (variable, providedId?: string) => {
- const id = providedId || crypto.randomUUID()
-
- const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
-
- if (!variable.name || /^variable\d+$/.test(variable.name)) {
- const existingNumbers = workflowVariables
- .map((v) => {
- const match = v.name.match(/^variable(\d+)$/)
- return match ? Number.parseInt(match[1]) : 0
- })
- .filter((n) => !Number.isNaN(n))
-
- const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
-
- variable.name = `variable${nextNumber}`
- }
-
- let uniqueName = variable.name
- let nameIndex = 1
-
- while (workflowVariables.some((v) => v.name === uniqueName)) {
- uniqueName = `${variable.name} (${nameIndex})`
- nameIndex++
- }
-
- if (variable.type === 'string') {
- variable.type = 'plain'
- }
-
- const newVariable: Variable = {
- id,
- workflowId: variable.workflowId,
- name: uniqueName,
- type: variable.type,
- value: variable.value || '',
- validationError: undefined,
- }
-
- const validationError = validateVariable(newVariable)
- if (validationError) {
- newVariable.validationError = validationError
- }
-
- set((state) => ({
- variables: {
- ...state.variables,
- [id]: newVariable,
- },
- }))
-
- return id
- },
-
- updateVariable: (id, update) => {
- set((state) => {
- if (!state.variables[id]) return state
-
- if (update.name !== undefined) {
- const oldVariable = state.variables[id]
- const oldVariableName = oldVariable.name
- const newName = update.name.trim()
-
- if (!newName) {
- update = { ...update }
- update.name = undefined
- } else if (newName !== oldVariableName) {
- const subBlockStore = useSubBlockStore.getState()
- const targetWorkflowId = oldVariable.workflowId
-
- if (targetWorkflowId) {
- const workflowValues = subBlockStore.workflowValues[targetWorkflowId] || {}
- const updatedWorkflowValues = { ...workflowValues }
- const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> =
- []
-
- const oldVarName = normalizeName(oldVariableName)
- const newVarName = normalizeName(newName)
- const regex = new RegExp(``, 'gi')
-
- const updateReferences = (value: any, pattern: RegExp, replacement: string): any => {
- if (typeof value === 'string') {
- return pattern.test(value) ? value.replace(pattern, replacement) : value
- }
-
- if (Array.isArray(value)) {
- return value.map((item) => updateReferences(item, pattern, replacement))
- }
-
- if (value !== null && typeof value === 'object') {
- const result = { ...value }
- for (const key in result) {
- result[key] = updateReferences(result[key], pattern, replacement)
- }
- return result
- }
-
- return value
- }
-
- Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
- Object.entries(blockValues as Record).forEach(
- ([subBlockId, value]) => {
- const updatedValue = updateReferences(value, regex, ``)
-
- if (JSON.stringify(updatedValue) !== JSON.stringify(value)) {
- if (!updatedWorkflowValues[blockId]) {
- updatedWorkflowValues[blockId] = { ...workflowValues[blockId] }
- }
- updatedWorkflowValues[blockId][subBlockId] = updatedValue
- changedSubBlocks.push({ blockId, subBlockId, value: updatedValue })
- }
- }
- )
- })
-
- // Update local state
- useSubBlockStore.setState({
- workflowValues: {
- ...subBlockStore.workflowValues,
- [targetWorkflowId]: updatedWorkflowValues,
- },
- })
-
- // Queue operations for persistence via socket
- const operationQueue = useOperationQueueStore.getState()
-
- for (const { blockId, subBlockId, value } of changedSubBlocks) {
- operationQueue.addToQueue({
- id: crypto.randomUUID(),
- operation: {
- operation: 'subblock-update',
- target: 'subblock',
- payload: { blockId, subblockId: subBlockId, value },
- },
- workflowId: targetWorkflowId,
- userId: 'system',
- })
- }
- }
- }
- }
-
- if (update.type === 'string') {
- update = { ...update, type: 'plain' }
- }
-
- const updatedVariable: Variable = {
- ...state.variables[id],
- ...update,
- validationError: undefined,
- }
-
- if (update.type || update.value !== undefined) {
- updatedVariable.validationError = validateVariable(updatedVariable)
- }
-
- const updated = {
- ...state.variables,
- [id]: updatedVariable,
- }
-
- return { variables: updated }
- })
- },
-
- deleteVariable: (id) => {
- set((state) => {
- if (!state.variables[id]) return state
-
- const workflowId = state.variables[id].workflowId
- const { [id]: _, ...rest } = state.variables
-
- return { variables: rest }
- })
- },
-
- getVariablesByWorkflowId: (workflowId) => {
- return Object.values(get().variables).filter((variable) => variable.workflowId === workflowId)
- },
- }))
-)
diff --git a/apps/sim/stores/panel/variables/types.ts b/apps/sim/stores/panel/variables/types.ts
deleted file mode 100644
index c0f7d06d150..00000000000
--- a/apps/sim/stores/panel/variables/types.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Variable types supported in the application
- * Note: 'string' is deprecated - use 'plain' for text values instead
- */
-export type VariableType = 'plain' | 'number' | 'boolean' | 'object' | 'array' | 'string'
-
-/**
- * Represents a workflow variable with workflow-specific naming
- * Variable names must be unique within each workflow
- */
-export interface Variable {
- id: string
- workflowId: string
- name: string // Must be unique per workflow
- type: VariableType
- value: unknown
- validationError?: string // Tracks format validation errors
-}
-
-export interface VariablesStore {
- variables: Record
- isLoading: boolean
- error: string | null
- isEditing: string | null
-
- /**
- * Loads variables for a specific workflow from the API and hydrates the store.
- */
- loadForWorkflow: (workflowId: string) => Promise
-
- /**
- * Adds a new variable with automatic name uniqueness validation
- * If a variable with the same name exists, it will be suffixed with a number
- * Optionally accepts a predetermined ID for collaborative operations
- */
- addVariable: (variable: Omit, providedId?: string) => string
-
- /**
- * Updates a variable, ensuring name remains unique within the workflow
- * If an updated name conflicts with existing ones, a numbered suffix is added
- */
- updateVariable: (id: string, update: Partial>) => void
-
- deleteVariable: (id: string) => void
-
- /**
- * Returns all variables for a specific workflow
- */
- getVariablesByWorkflowId: (workflowId: string) => Variable[]
-}
diff --git a/apps/sim/stores/settings/environment/index.ts b/apps/sim/stores/settings/environment/index.ts
index 0b13cd29f71..01e93a50d17 100644
--- a/apps/sim/stores/settings/environment/index.ts
+++ b/apps/sim/stores/settings/environment/index.ts
@@ -1,7 +1 @@
-export { useEnvironmentStore } from './store'
-export type {
- CachedWorkspaceEnvData,
- EnvironmentState,
- EnvironmentStore,
- EnvironmentVariable,
-} from './types'
+export type { CachedWorkspaceEnvData, EnvironmentState, EnvironmentVariable } from './types'
diff --git a/apps/sim/stores/settings/environment/store.ts b/apps/sim/stores/settings/environment/store.ts
deleted file mode 100644
index 99bb11c18fa..00000000000
--- a/apps/sim/stores/settings/environment/store.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { createLogger } from '@sim/logger'
-import { create } from 'zustand'
-import { fetchPersonalEnvironment } from '@/lib/environment/api'
-import type { EnvironmentStore, EnvironmentVariable } from './types'
-
-const logger = createLogger('EnvironmentStore')
-
-export const useEnvironmentStore = create()((set, get) => ({
- variables: {},
- isLoading: false,
- error: null,
-
- loadEnvironmentVariables: async () => {
- try {
- set({ isLoading: true, error: null })
- const data = await fetchPersonalEnvironment()
- set({ variables: data, isLoading: false })
- } catch (error) {
- logger.error('Error loading environment variables:', { error })
- set({
- error: error instanceof Error ? error.message : 'Unknown error',
- isLoading: false,
- })
- throw error
- }
- },
-
- setVariables: (variables: Record) => {
- set({ variables })
- },
-
- getAllVariables: () => {
- return get().variables
- },
-
- reset: () => {
- set({
- variables: {},
- isLoading: false,
- error: null,
- })
- },
-}))
diff --git a/apps/sim/stores/settings/environment/types.ts b/apps/sim/stores/settings/environment/types.ts
index 7e9a575a8dd..8dbb67caf77 100644
--- a/apps/sim/stores/settings/environment/types.ts
+++ b/apps/sim/stores/settings/environment/types.ts
@@ -15,10 +15,3 @@ export interface EnvironmentState {
isLoading: boolean
error: string | null
}
-
-export interface EnvironmentStore extends EnvironmentState {
- loadEnvironmentVariables: () => Promise
- setVariables: (variables: Record) => void
- getAllVariables: () => Record
- reset: () => void
-}
diff --git a/apps/sim/stores/variables/index.ts b/apps/sim/stores/variables/index.ts
new file mode 100644
index 00000000000..d64ccb216a5
--- /dev/null
+++ b/apps/sim/stores/variables/index.ts
@@ -0,0 +1,11 @@
+export {
+ getDefaultVariablesDimensions,
+ getVariablesPosition,
+ MAX_VARIABLES_HEIGHT,
+ MAX_VARIABLES_WIDTH,
+ MIN_VARIABLES_HEIGHT,
+ MIN_VARIABLES_WIDTH,
+ useVariablesModalStore,
+} from './modal'
+export { useVariablesStore } from './store'
+export type { Variable, VariablesStore, VariableType } from './types'
diff --git a/apps/sim/stores/variables/modal.ts b/apps/sim/stores/variables/modal.ts
new file mode 100644
index 00000000000..be8ff2915b7
--- /dev/null
+++ b/apps/sim/stores/variables/modal.ts
@@ -0,0 +1,145 @@
+import { create } from 'zustand'
+import { devtools, persist } from 'zustand/middleware'
+import type {
+ VariablesDimensions,
+ VariablesModalStore,
+ VariablesPosition,
+} from '@/stores/variables/types'
+
+/**
+ * Floating variables modal default dimensions.
+ * Slightly larger than the chat modal for more comfortable editing.
+ */
+const DEFAULT_WIDTH = 320
+const DEFAULT_HEIGHT = 320
+
+/**
+ * Minimum and maximum modal dimensions.
+ */
+export const MIN_VARIABLES_WIDTH = DEFAULT_WIDTH
+export const MIN_VARIABLES_HEIGHT = DEFAULT_HEIGHT
+export const MAX_VARIABLES_WIDTH = 500
+export const MAX_VARIABLES_HEIGHT = 600
+
+/** Inset gap between the viewport edge and the content window */
+const CONTENT_WINDOW_GAP = 8
+
+/**
+ * Compute a center-biased default position, factoring in current layout chrome
+ * (sidebar, right panel, terminal) and content window inset.
+ */
+const calculateDefaultPosition = (): VariablesPosition => {
+ if (typeof window === 'undefined') {
+ return { x: 100, y: 100 }
+ }
+
+ const sidebarWidth = Number.parseInt(
+ getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
+ )
+ const panelWidth = Number.parseInt(
+ getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
+ )
+ const terminalHeight = Number.parseInt(
+ getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
+ )
+
+ const availableWidth = window.innerWidth - sidebarWidth - CONTENT_WINDOW_GAP - panelWidth
+ const availableHeight = window.innerHeight - CONTENT_WINDOW_GAP * 2 - terminalHeight
+ const x = sidebarWidth + (availableWidth - DEFAULT_WIDTH) / 2
+ const y = CONTENT_WINDOW_GAP + (availableHeight - DEFAULT_HEIGHT) / 2
+ return { x, y }
+}
+
+/**
+ * Constrain a position to the visible canvas, considering layout chrome.
+ */
+const constrainPosition = (
+ position: VariablesPosition,
+ width: number = DEFAULT_WIDTH,
+ height: number = DEFAULT_HEIGHT
+): VariablesPosition => {
+ if (typeof window === 'undefined') return position
+
+ const sidebarWidth = Number.parseInt(
+ getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
+ )
+ const panelWidth = Number.parseInt(
+ getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
+ )
+ const terminalHeight = Number.parseInt(
+ getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
+ )
+
+ const minX = sidebarWidth
+ const maxX = window.innerWidth - CONTENT_WINDOW_GAP - panelWidth - width
+ const minY = CONTENT_WINDOW_GAP
+ const maxY = window.innerHeight - CONTENT_WINDOW_GAP - terminalHeight - height
+
+ return {
+ x: Math.max(minX, Math.min(maxX, position.x)),
+ y: Math.max(minY, Math.min(maxY, position.y)),
+ }
+}
+
+/**
+ * Return a valid, constrained position. If the stored one is off-bounds due to
+ * layout changes, prefer a fresh default center position.
+ */
+export const getVariablesPosition = (
+ stored: VariablesPosition | null,
+ width: number = DEFAULT_WIDTH,
+ height: number = DEFAULT_HEIGHT
+): VariablesPosition => {
+ if (!stored) return calculateDefaultPosition()
+ const constrained = constrainPosition(stored, width, height)
+ const deltaX = Math.abs(constrained.x - stored.x)
+ const deltaY = Math.abs(constrained.y - stored.y)
+ if (deltaX > 100 || deltaY > 100) return calculateDefaultPosition()
+ return constrained
+}
+
+/**
+ * UI-only store for the floating variables modal.
+ * Variable data lives in the variables data store (`@/stores/variables/store`).
+ */
+export const useVariablesModalStore = create()(
+ devtools(
+ persist(
+ (set) => ({
+ isOpen: false,
+ position: null,
+ width: DEFAULT_WIDTH,
+ height: DEFAULT_HEIGHT,
+
+ setIsOpen: (open) => set({ isOpen: open }),
+ setPosition: (position) => set({ position }),
+ setDimensions: (dimensions) =>
+ set({
+ width: Math.max(MIN_VARIABLES_WIDTH, Math.min(MAX_VARIABLES_WIDTH, dimensions.width)),
+ height: Math.max(
+ MIN_VARIABLES_HEIGHT,
+ Math.min(MAX_VARIABLES_HEIGHT, dimensions.height)
+ ),
+ }),
+ resetPosition: () => set({ position: null }),
+ }),
+ {
+ name: 'variables-modal-store',
+ partialize: (state) => ({
+ position: state.position,
+ width: state.width,
+ height: state.height,
+ }),
+ }
+ ),
+ { name: 'variables-modal-store' }
+ )
+)
+
+/**
+ * Get default floating variables modal dimensions.
+ */
+export const getDefaultVariablesDimensions = (): VariablesDimensions => ({
+ width: DEFAULT_WIDTH,
+ height: DEFAULT_HEIGHT,
+})
diff --git a/apps/sim/stores/variables/store.ts b/apps/sim/stores/variables/store.ts
index 497db6550bb..0980f2ae6b3 100644
--- a/apps/sim/stores/variables/store.ts
+++ b/apps/sim/stores/variables/store.ts
@@ -1,145 +1,47 @@
import { createLogger } from '@sim/logger'
import JSON5 from 'json5'
-import { v4 as uuidv4 } from 'uuid'
import { create } from 'zustand'
-import { devtools, persist } from 'zustand/middleware'
+import { devtools } from 'zustand/middleware'
import { normalizeName } from '@/executor/constants'
-import type {
- Variable,
- VariablesDimensions,
- VariablesPosition,
- VariablesStore,
- VariableType,
-} from '@/stores/variables/types'
-import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
+import { useOperationQueueStore } from '@/stores/operation-queue/store'
+import type { Variable, VariablesStore } from '@/stores/variables/types'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
-const logger = createLogger('VariablesModalStore')
-
-/**
- * Floating variables modal default dimensions.
- * Slightly larger than the chat modal for more comfortable editing.
- */
-const DEFAULT_WIDTH = 320
-const DEFAULT_HEIGHT = 320
-
-/**
- * Minimum and maximum modal dimensions.
- * Kept in sync with the chat modal experience.
- */
-export const MIN_VARIABLES_WIDTH = DEFAULT_WIDTH
-export const MIN_VARIABLES_HEIGHT = DEFAULT_HEIGHT
-export const MAX_VARIABLES_WIDTH = 500
-export const MAX_VARIABLES_HEIGHT = 600
-
-/** Inset gap between the viewport edge and the content window */
-const CONTENT_WINDOW_GAP = 8
-
-/**
- * Compute a center-biased default position, factoring in current layout chrome
- * (sidebar, right panel, terminal) and content window inset.
- */
-const calculateDefaultPosition = (): VariablesPosition => {
- if (typeof window === 'undefined') {
- return { x: 100, y: 100 }
- }
-
- const sidebarWidth = Number.parseInt(
- getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
- )
- const panelWidth = Number.parseInt(
- getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
- )
- const terminalHeight = Number.parseInt(
- getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
- )
-
- const availableWidth = window.innerWidth - sidebarWidth - CONTENT_WINDOW_GAP - panelWidth
- const availableHeight = window.innerHeight - CONTENT_WINDOW_GAP * 2 - terminalHeight
- const x = sidebarWidth + (availableWidth - DEFAULT_WIDTH) / 2
- const y = CONTENT_WINDOW_GAP + (availableHeight - DEFAULT_HEIGHT) / 2
- return { x, y }
-}
+const logger = createLogger('VariablesStore')
-/**
- * Constrain a position to the visible canvas, considering layout chrome.
- */
-const constrainPosition = (
- position: VariablesPosition,
- width: number = DEFAULT_WIDTH,
- height: number = DEFAULT_HEIGHT
-): VariablesPosition => {
- if (typeof window === 'undefined') return position
-
- const sidebarWidth = Number.parseInt(
- getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
- )
- const panelWidth = Number.parseInt(
- getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
- )
- const terminalHeight = Number.parseInt(
- getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
- )
-
- const minX = sidebarWidth
- const maxX = window.innerWidth - CONTENT_WINDOW_GAP - panelWidth - width
- const minY = CONTENT_WINDOW_GAP
- const maxY = window.innerHeight - CONTENT_WINDOW_GAP - terminalHeight - height
-
- return {
- x: Math.max(minX, Math.min(maxX, position.x)),
- y: Math.max(minY, Math.min(maxY, position.y)),
- }
-}
-
-/**
- * Return a valid, constrained position. If the stored one is off-bounds due to
- * layout changes, prefer a fresh default center position.
- */
-export const getVariablesPosition = (
- stored: VariablesPosition | null,
- width: number = DEFAULT_WIDTH,
- height: number = DEFAULT_HEIGHT
-): VariablesPosition => {
- if (!stored) return calculateDefaultPosition()
- const constrained = constrainPosition(stored, width, height)
- const deltaX = Math.abs(constrained.x - stored.x)
- const deltaY = Math.abs(constrained.y - stored.y)
- if (deltaX > 100 || deltaY > 100) return calculateDefaultPosition()
- return constrained
-}
-
-/**
- * Validate a variable's value given its type. Returns an error message or undefined.
- */
function validateVariable(variable: Variable): string | undefined {
try {
switch (variable.type) {
- case 'number': {
- return Number.isNaN(Number(variable.value)) ? 'Not a valid number' : undefined
- }
- case 'boolean': {
- return !/^(true|false)$/i.test(String(variable.value).trim())
- ? 'Expected "true" or "false"'
- : undefined
- }
- case 'object': {
+ case 'number':
+ if (Number.isNaN(Number(variable.value))) {
+ return 'Not a valid number'
+ }
+ break
+ case 'boolean':
+ if (!/^(true|false)$/i.test(String(variable.value).trim())) {
+ return 'Expected "true" or "false"'
+ }
+ break
+ case 'object':
try {
const valueToEvaluate = String(variable.value).trim()
+
if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) {
return 'Not a valid object format'
}
+
const parsed = JSON5.parse(valueToEvaluate)
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
return 'Not a valid object'
}
+
return undefined
} catch (e) {
logger.error('Object parsing error:', e)
return 'Invalid object syntax'
}
- }
- case 'array': {
+ case 'array':
try {
const parsed = JSON5.parse(String(variable.value))
if (!Array.isArray(parsed)) {
@@ -148,257 +50,199 @@ function validateVariable(variable: Variable): string | undefined {
} catch {
return 'Invalid array syntax'
}
- return undefined
- }
- default:
- return undefined
+ break
}
+ return undefined
} catch (e) {
return e instanceof Error ? e.message : 'Invalid format'
}
}
-/**
- * Migrate deprecated type 'string' -> 'plain'.
- */
-function migrateStringToPlain(variable: Variable): Variable {
- if (variable.type !== 'string') return variable
- return { ...variable, type: 'plain' as const }
-}
-
-/**
- * Floating Variables modal + Variables data store.
- */
export const useVariablesStore = create()(
- devtools(
- persist(
- (set, get) => ({
- // UI
- isOpen: false,
- position: null,
- width: DEFAULT_WIDTH,
- height: DEFAULT_HEIGHT,
-
- setIsOpen: (open) => set({ isOpen: open }),
- setPosition: (position) => set({ position }),
- setDimensions: (dimensions) =>
- set({
- width: Math.max(MIN_VARIABLES_WIDTH, Math.min(MAX_VARIABLES_WIDTH, dimensions.width)),
- height: Math.max(
- MIN_VARIABLES_HEIGHT,
- Math.min(MAX_VARIABLES_HEIGHT, dimensions.height)
- ),
- }),
- resetPosition: () => set({ position: null }),
-
- // Data
- variables: {},
- isLoading: false,
- error: null,
-
- async loadForWorkflow(workflowId) {
- try {
- set({ isLoading: true, error: null })
- const res = await fetch(`/api/workflows/${workflowId}/variables`, { method: 'GET' })
- if (!res.ok) {
- const text = await res.text().catch(() => '')
- throw new Error(text || `Failed to load variables: ${res.statusText}`)
- }
- const data = await res.json()
- const variables = (data?.data as Record) || {}
- // Migrate any deprecated types and merge into store (remove other workflow entries)
- const migrated: Record = Object.fromEntries(
- Object.entries(variables).map(([id, v]) => [id, migrateStringToPlain(v)])
- )
- set((state) => {
- const withoutThisWorkflow = Object.fromEntries(
- Object.entries(state.variables).filter(
- (entry): entry is [string, Variable] => entry[1].workflowId !== workflowId
- )
- )
- return {
- variables: { ...withoutThisWorkflow, ...migrated },
- isLoading: false,
- error: null,
- }
- })
- } catch (e) {
- const message = e instanceof Error ? e.message : 'Unknown error'
- set({ isLoading: false, error: message })
- }
- },
-
- addVariable: (variable, providedId) => {
- const id = providedId || uuidv4()
- const state = get()
+ devtools((set, get) => ({
+ variables: {},
+ isLoading: false,
+ error: null,
+ isEditing: null,
+
+ addVariable: (variable, providedId?: string) => {
+ const id = providedId || crypto.randomUUID()
+
+ const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
+
+ if (!variable.name || /^variable\d+$/.test(variable.name)) {
+ const existingNumbers = workflowVariables
+ .map((v) => {
+ const match = v.name.match(/^variable(\d+)$/)
+ return match ? Number.parseInt(match[1]) : 0
+ })
+ .filter((n) => !Number.isNaN(n))
- const workflowVariables = state
- .getVariablesByWorkflowId(variable.workflowId)
- .map((v) => ({ id: v.id, name: v.name }))
+ const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
- // Default naming: variableN
- if (!variable.name || /^variable\d+$/.test(variable.name)) {
- const existingNumbers = workflowVariables
- .map((v) => {
- const match = v.name.match(/^variable(\d+)$/)
- return match ? Number.parseInt(match[1]) : 0
- })
- .filter((n) => !Number.isNaN(n))
- const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
- variable.name = `variable${nextNumber}`
- }
+ variable.name = `variable${nextNumber}`
+ }
- // Ensure uniqueness
- let uniqueName = variable.name
- let nameIndex = 1
- while (workflowVariables.some((v) => v.name === uniqueName)) {
- uniqueName = `${variable.name} (${nameIndex})`
- nameIndex++
- }
+ let uniqueName = variable.name
+ let nameIndex = 1
- if (variable.type === 'string') {
- variable.type = 'plain'
- }
+ while (workflowVariables.some((v) => v.name === uniqueName)) {
+ uniqueName = `${variable.name} (${nameIndex})`
+ nameIndex++
+ }
- const newVariable: Variable = {
- id,
- workflowId: variable.workflowId,
- name: uniqueName,
- type: variable.type,
- value: variable.value ?? '',
- validationError: undefined,
- }
+ if (variable.type === 'string') {
+ variable.type = 'plain'
+ }
- const validationError = validateVariable(newVariable)
- if (validationError) {
- newVariable.validationError = validationError
- }
+ const newVariable: Variable = {
+ id,
+ workflowId: variable.workflowId,
+ name: uniqueName,
+ type: variable.type,
+ value: variable.value || '',
+ validationError: undefined,
+ }
- set((state) => ({
- variables: {
- ...state.variables,
- [id]: newVariable,
- },
- }))
+ const validationError = validateVariable(newVariable)
+ if (validationError) {
+ newVariable.validationError = validationError
+ }
- return id
+ set((state) => ({
+ variables: {
+ ...state.variables,
+ [id]: newVariable,
},
+ }))
+
+ return id
+ },
+
+ updateVariable: (id, update) => {
+ set((state) => {
+ if (!state.variables[id]) return state
+
+ if (update.name !== undefined) {
+ const oldVariable = state.variables[id]
+ const oldVariableName = oldVariable.name
+ const newName = update.name.trim()
+
+ if (!newName) {
+ update = { ...update }
+ update.name = undefined
+ } else if (newName !== oldVariableName) {
+ const subBlockStore = useSubBlockStore.getState()
+ const targetWorkflowId = oldVariable.workflowId
+
+ if (targetWorkflowId) {
+ const workflowValues = subBlockStore.workflowValues[targetWorkflowId] || {}
+ const updatedWorkflowValues = { ...workflowValues }
+ const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> =
+ []
+
+ const oldVarName = normalizeName(oldVariableName)
+ const newVarName = normalizeName(newName)
+ const regex = new RegExp(``, 'gi')
+
+ const updateReferences = (value: any, pattern: RegExp, replacement: string): any => {
+ if (typeof value === 'string') {
+ return pattern.test(value) ? value.replace(pattern, replacement) : value
+ }
- updateVariable: (id, update) => {
- set((state) => {
- const existing = state.variables[id]
- if (!existing) return state
-
- // Handle name changes: keep references in sync across workflow values
- if (update.name !== undefined) {
- const oldVariableName = existing.name
- const newName = String(update.name).trim()
-
- if (!newName) {
- update = { ...update, name: undefined }
- } else if (newName !== oldVariableName) {
- const subBlockStore = useSubBlockStore.getState()
- const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
-
- if (activeWorkflowId) {
- const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {}
- const updatedWorkflowValues = { ...workflowValues }
-
- Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
- Object.entries(blockValues as Record).forEach(
- ([subBlockId, value]) => {
- const oldVarName = normalizeName(oldVariableName)
- const newVarName = normalizeName(newName)
- const regex = new RegExp(``, 'gi')
-
- updatedWorkflowValues[blockId][subBlockId] = updateReferences(
- value,
- regex,
- ``
- )
-
- function updateReferences(
- val: any,
- refRegex: RegExp,
- replacement: string
- ): any {
- if (typeof val === 'string') {
- return refRegex.test(val) ? val.replace(refRegex, replacement) : val
- }
- if (Array.isArray(val)) {
- return val.map((item) => updateReferences(item, refRegex, replacement))
- }
- if (val !== null && typeof val === 'object') {
- const result: Record = { ...val }
- for (const key in result) {
- result[key] = updateReferences(result[key], refRegex, replacement)
- }
- return result
- }
- return val
- }
- }
- )
- })
-
- useSubBlockStore.setState({
- workflowValues: {
- ...subBlockStore.workflowValues,
- [activeWorkflowId]: updatedWorkflowValues,
- },
- })
+ if (Array.isArray(value)) {
+ return value.map((item) => updateReferences(item, pattern, replacement))
}
+
+ if (value !== null && typeof value === 'object') {
+ const result = { ...value }
+ for (const key in result) {
+ result[key] = updateReferences(result[key], pattern, replacement)
+ }
+ return result
+ }
+
+ return value
}
- }
- // Handle deprecated -> new type migration
- if (update.type === 'string') {
- update = { ...update, type: 'plain' as VariableType }
- }
+ Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
+ Object.entries(blockValues as Record).forEach(
+ ([subBlockId, value]) => {
+ const updatedValue = updateReferences(value, regex, ``)
- const updated: Variable = {
- ...existing,
- ...update,
- validationError: undefined,
- }
+ if (JSON.stringify(updatedValue) !== JSON.stringify(value)) {
+ if (!updatedWorkflowValues[blockId]) {
+ updatedWorkflowValues[blockId] = { ...workflowValues[blockId] }
+ }
+ updatedWorkflowValues[blockId][subBlockId] = updatedValue
+ changedSubBlocks.push({ blockId, subBlockId, value: updatedValue })
+ }
+ }
+ )
+ })
- // Validate only when type or value changed
- if (update.type || update.value !== undefined) {
- updated.validationError = validateVariable(updated)
- }
+ // Update local state
+ useSubBlockStore.setState({
+ workflowValues: {
+ ...subBlockStore.workflowValues,
+ [targetWorkflowId]: updatedWorkflowValues,
+ },
+ })
- return {
- variables: {
- ...state.variables,
- [id]: updated,
- },
+ // Queue operations for persistence via socket
+ const operationQueue = useOperationQueueStore.getState()
+
+ for (const { blockId, subBlockId, value } of changedSubBlocks) {
+ operationQueue.addToQueue({
+ id: crypto.randomUUID(),
+ operation: {
+ operation: 'subblock-update',
+ target: 'subblock',
+ payload: { blockId, subblockId: subBlockId, value },
+ },
+ workflowId: targetWorkflowId,
+ userId: 'system',
+ })
+ }
}
- })
- },
+ }
+ }
- deleteVariable: (id) => {
- set((state) => {
- if (!state.variables[id]) return state
- const { [id]: _deleted, ...rest } = state.variables
- return { variables: rest }
- })
- },
+ if (update.type === 'string') {
+ update = { ...update, type: 'plain' }
+ }
- getVariablesByWorkflowId: (workflowId) => {
- return Object.values(get().variables).filter((v) => v.workflowId === workflowId)
- },
- }),
- {
- name: 'variables-modal-store',
- }
- )
- )
-)
+ const updatedVariable: Variable = {
+ ...state.variables[id],
+ ...update,
+ validationError: undefined,
+ }
-/**
- * Get default floating variables modal dimensions.
- */
-export const getDefaultVariablesDimensions = (): VariablesDimensions => ({
- width: DEFAULT_WIDTH,
- height: DEFAULT_HEIGHT,
-})
+ if (update.type || update.value !== undefined) {
+ updatedVariable.validationError = validateVariable(updatedVariable)
+ }
+
+ const updated = {
+ ...state.variables,
+ [id]: updatedVariable,
+ }
+
+ return { variables: updated }
+ })
+ },
+
+ deleteVariable: (id) => {
+ set((state) => {
+ if (!state.variables[id]) return state
+
+ const { [id]: _, ...rest } = state.variables
+
+ return { variables: rest }
+ })
+ },
+
+ getVariablesByWorkflowId: (workflowId) => {
+ return Object.values(get().variables).filter((variable) => variable.workflowId === workflowId)
+ },
+ }))
+)
diff --git a/apps/sim/stores/variables/types.ts b/apps/sim/stores/variables/types.ts
index 610192f49d8..a3b43e5be8b 100644
--- a/apps/sim/stores/variables/types.ts
+++ b/apps/sim/stores/variables/types.ts
@@ -1,19 +1,47 @@
/**
- * Variable types supported by the variables modal/editor.
- * Note: 'string' is deprecated. Use 'plain' for freeform text values instead.
+ * Variable types supported in the application
+ * Note: 'string' is deprecated - use 'plain' for text values instead
*/
export type VariableType = 'plain' | 'number' | 'boolean' | 'object' | 'array' | 'string'
/**
- * Workflow-scoped variable model.
+ * Represents a workflow variable with workflow-specific naming
+ * Variable names must be unique within each workflow
*/
export interface Variable {
id: string
workflowId: string
- name: string
+ name: string // Must be unique per workflow
type: VariableType
value: unknown
- validationError?: string
+ validationError?: string // Tracks format validation errors
+}
+
+export interface VariablesStore {
+ variables: Record
+ isLoading: boolean
+ error: string | null
+ isEditing: string | null
+
+ /**
+ * Adds a new variable with automatic name uniqueness validation
+ * If a variable with the same name exists, it will be suffixed with a number
+ * Optionally accepts a predetermined ID for collaborative operations
+ */
+ addVariable: (variable: Omit, providedId?: string) => string
+
+ /**
+ * Updates a variable, ensuring name remains unique within the workflow
+ * If an updated name conflicts with existing ones, a numbered suffix is added
+ */
+ updateVariable: (id: string, update: Partial>) => void
+
+ deleteVariable: (id: string) => void
+
+ /**
+ * Returns all variables for a specific workflow
+ */
+ getVariablesByWorkflowId: (workflowId: string) => Variable[]
}
/**
@@ -33,11 +61,10 @@ export interface VariablesDimensions {
}
/**
- * Public store interface for variables editor/modal.
- * Combines UI state of the floating modal and the variables data/actions.
+ * UI-only store interface for the floating variables modal.
+ * Variable data lives in the variables data store (`@/stores/variables/store`).
*/
-export interface VariablesStore {
- // UI State
+export interface VariablesModalStore {
isOpen: boolean
position: VariablesPosition | null
width: number
@@ -46,16 +73,4 @@ export interface VariablesStore {
setPosition: (position: VariablesPosition) => void
setDimensions: (dimensions: VariablesDimensions) => void
resetPosition: () => void
-
- // Data
- variables: Record
- isLoading: boolean
- error: string | null
-
- // Actions
- loadForWorkflow: (workflowId: string) => Promise
- addVariable: (variable: Omit, providedId?: string) => string
- updateVariable: (id: string, update: Partial>) => void
- deleteVariable: (id: string) => void
- getVariablesByWorkflowId: (workflowId: string) => Variable[]
}
diff --git a/apps/sim/stores/workflows/index.ts b/apps/sim/stores/workflows/index.ts
index e2fdf9a7c4a..1571a0e50f1 100644
--- a/apps/sim/stores/workflows/index.ts
+++ b/apps/sim/stores/workflows/index.ts
@@ -3,7 +3,7 @@ import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
-import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
+import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('Workflows')
@@ -30,9 +30,6 @@ export function getWorkflowWithValues(workflowId: string, workspaceId: string) {
return null
}
- // Get deployment status from registry
- const deploymentStatus = useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)
-
// Use the current state from the store (only available for active workflow)
const workflowState: WorkflowState = useWorkflowStore.getState().getWorkflowState()
@@ -52,110 +49,10 @@ export function getWorkflowWithValues(workflowId: string, workspaceId: string) {
loops: workflowState.loops,
parallels: workflowState.parallels,
lastSaved: workflowState.lastSaved,
- // Get deployment fields from registry for API compatibility
- isDeployed: deploymentStatus?.isDeployed || false,
- deployedAt: deploymentStatus?.deployedAt,
},
}
}
-/**
- * Get a specific block with its subblock values merged in
- * @param blockId ID of the block to retrieve
- * @returns The block with merged subblock values or null if not found
- */
-export function getBlockWithValues(blockId: string): BlockState | null {
- const workflowState = useWorkflowStore.getState()
- const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
-
- if (!activeWorkflowId || !workflowState.blocks[blockId]) return null
-
- const mergedBlocks = mergeSubblockState(workflowState.blocks, activeWorkflowId, blockId)
- return mergedBlocks[blockId] || null
-}
-
-/**
- * Get all workflows with their values merged
- * Note: Since localStorage has been removed, this only includes the active workflow state
- * @param workspaceId Workspace containing the workflow metadata
- * @returns An object containing workflows, with state only for the active workflow
- */
-export function getAllWorkflowsWithValues(workspaceId: string) {
- const workflows = getWorkflows(workspaceId)
- const result: Record<
- string,
- {
- id: string
- name: string
- description?: string
- color: string
- folderId?: string | null
- workspaceId?: string
- apiKey?: string
- state: WorkflowState & { isDeployed: boolean; deployedAt?: Date }
- }
- > = {}
- const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
- const currentState = useWorkflowStore.getState()
-
- // Only sync the active workflow to ensure we always send valid state data
- const activeMetadata = activeWorkflowId
- ? workflows.find((w) => w.id === activeWorkflowId)
- : undefined
- if (activeWorkflowId && activeMetadata) {
- const metadata = activeMetadata
-
- // Get deployment status from registry
- const deploymentStatus = useWorkflowRegistry
- .getState()
- .getWorkflowDeploymentStatus(activeWorkflowId)
-
- // Ensure state has all required fields for Zod validation
- const workflowState: WorkflowState = {
- ...useWorkflowStore.getState().getWorkflowState(),
- // Ensure fallback values for safer handling
- blocks: currentState.blocks || {},
- edges: currentState.edges || [],
- loops: currentState.loops || {},
- parallels: currentState.parallels || {},
- lastSaved: currentState.lastSaved || Date.now(),
- }
-
- // Merge the subblock values for this specific workflow
- const mergedBlocks = mergeSubblockState(workflowState.blocks, activeWorkflowId)
-
- // Include the API key in the state if it exists in the deployment status
- const apiKey = deploymentStatus?.apiKey
-
- result[activeWorkflowId] = {
- id: activeWorkflowId,
- name: metadata.name,
- description: metadata.description,
- color: metadata.color || '#3972F6',
- folderId: metadata.folderId,
- state: {
- blocks: mergedBlocks,
- edges: workflowState.edges,
- loops: workflowState.loops,
- parallels: workflowState.parallels,
- lastSaved: workflowState.lastSaved,
- // Get deployment fields from registry for API compatibility
- isDeployed: deploymentStatus?.isDeployed || false,
- deployedAt: deploymentStatus?.deployedAt,
- },
- // Include API key if available
- apiKey,
- }
-
- // Only include workspaceId if it's not null/undefined
- if (metadata.workspaceId) {
- result[activeWorkflowId].workspaceId = metadata.workspaceId
- }
- }
-
- return result
-}
-
export { useWorkflowRegistry } from '@/stores/workflows/registry/store'
export type { WorkflowMetadata } from '@/stores/workflows/registry/types'
export { useSubBlockStore } from '@/stores/workflows/subblock/store'
diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts
index 9ece2c4c18e..48a5b4a7a7c 100644
--- a/apps/sim/stores/workflows/registry/store.ts
+++ b/apps/sim/stores/workflows/registry/store.ts
@@ -3,14 +3,12 @@ import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
+import type { WorkflowDeploymentInfo } from '@/hooks/queries/deployments'
+import { deploymentKeys } from '@/hooks/queries/deployments'
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
-import { useVariablesStore } from '@/stores/panel/variables/store'
-import type { Variable } from '@/stores/panel/variables/types'
-import type {
- DeploymentStatus,
- HydrationState,
- WorkflowRegistry,
-} from '@/stores/workflows/registry/types'
+import { useVariablesStore } from '@/stores/variables/store'
+import type { Variable } from '@/stores/variables/types'
+import type { HydrationState, WorkflowRegistry } from '@/stores/workflows/registry/types'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { getUniqueBlockName, regenerateBlockIds } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -34,7 +32,6 @@ function resetWorkflowStores() {
edges: [],
loops: {},
parallels: {},
- deploymentStatuses: {},
lastSaved: Date.now(),
})
@@ -48,7 +45,6 @@ export const useWorkflowRegistry = create()(
(set, get) => ({
activeWorkflowId: null,
error: null,
- deploymentStatuses: {},
hydration: initialHydration,
clipboard: null,
pendingSelection: null,
@@ -61,7 +57,6 @@ export const useWorkflowRegistry = create()(
set({
activeWorkflowId: null,
- deploymentStatuses: {},
error: null,
hydration: {
phase: 'idle',
@@ -73,74 +68,6 @@ export const useWorkflowRegistry = create()(
})
},
- getWorkflowDeploymentStatus: (workflowId: string | null): DeploymentStatus | null => {
- if (!workflowId) {
- workflowId = get().activeWorkflowId
- if (!workflowId) return null
- }
-
- const { deploymentStatuses = {} } = get()
-
- if (deploymentStatuses[workflowId]) {
- return deploymentStatuses[workflowId]
- }
-
- return null
- },
-
- setDeploymentStatus: (
- workflowId: string | null,
- isDeployed: boolean,
- deployedAt?: Date,
- apiKey?: string
- ) => {
- if (!workflowId) {
- workflowId = get().activeWorkflowId
- if (!workflowId) return
- }
-
- set((state) => ({
- deploymentStatuses: {
- ...state.deploymentStatuses,
- [workflowId as string]: {
- isDeployed,
- deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
- apiKey,
- needsRedeployment: isDeployed
- ? false
- : (state.deploymentStatuses?.[workflowId as string]?.needsRedeployment ?? false),
- },
- },
- }))
- },
-
- setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => {
- if (!workflowId) {
- workflowId = get().activeWorkflowId
- if (!workflowId) return
- }
-
- set((state) => {
- const deploymentStatuses = state.deploymentStatuses || {}
- const currentStatus = deploymentStatuses[workflowId as string] || { isDeployed: false }
-
- return {
- deploymentStatuses: {
- ...deploymentStatuses,
- [workflowId as string]: {
- ...currentStatus,
- needsRedeployment,
- },
- },
- }
- })
-
- const { activeWorkflowId } = get()
- if (workflowId === activeWorkflowId) {
- useWorkflowStore.getState().setNeedsRedeploymentFlag(needsRedeployment)
- }
- },
-
loadWorkflowState: async (workflowId: string) => {
const workspaceId = get().hydration.workspaceId
if (!workspaceId) {
@@ -170,20 +97,19 @@ export const useWorkflowRegistry = create()(
}
const workflowData = (await response.json()).data
- const nextDeploymentStatuses =
- workflowData?.isDeployed || workflowData?.deployedAt
- ? {
- ...get().deploymentStatuses,
- [workflowId]: {
- isDeployed: workflowData.isDeployed || false,
- deployedAt: workflowData.deployedAt
- ? new Date(workflowData.deployedAt)
- : undefined,
- apiKey: workflowData.apiKey || undefined,
- needsRedeployment: false,
- },
- }
- : get().deploymentStatuses
+
+ if (workflowData?.isDeployed !== undefined) {
+ getQueryClient().setQueryData(
+ deploymentKeys.info(workflowId),
+ (prev) => ({
+ isDeployed: workflowData.isDeployed ?? false,
+ deployedAt: workflowData.deployedAt ?? null,
+ apiKey: workflowData.apiKey ?? prev?.apiKey ?? null,
+ needsRedeployment: prev?.needsRedeployment ?? false,
+ isPublicApi: prev?.isPublicApi ?? false,
+ })
+ )
+ }
let workflowState: WorkflowState
@@ -195,7 +121,6 @@ export const useWorkflowRegistry = create()(
loops: workflowData.state.loops || {},
parallels: workflowData.state.parallels || {},
lastSaved: Date.now(),
- deploymentStatuses: nextDeploymentStatuses,
}
} else {
workflowState = {
@@ -204,7 +129,6 @@ export const useWorkflowRegistry = create()(
edges: [],
loops: {},
parallels: {},
- deploymentStatuses: nextDeploymentStatuses,
lastSaved: Date.now(),
}
@@ -250,7 +174,6 @@ export const useWorkflowRegistry = create()(
set((state) => ({
activeWorkflowId: workflowId,
error: null,
- deploymentStatuses: nextDeploymentStatuses,
hydration: {
phase: 'ready',
workspaceId: state.hydration.workspaceId,
@@ -367,7 +290,6 @@ export const useWorkflowRegistry = create()(
set({
activeWorkflowId: null,
- deploymentStatuses: {},
error: null,
hydration: initialHydration,
clipboard: null,
diff --git a/apps/sim/stores/workflows/registry/types.ts b/apps/sim/stores/workflows/registry/types.ts
index f434967e413..96a2d324539 100644
--- a/apps/sim/stores/workflows/registry/types.ts
+++ b/apps/sim/stores/workflows/registry/types.ts
@@ -1,13 +1,6 @@
import type { Edge } from 'reactflow'
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
-export interface DeploymentStatus {
- isDeployed: boolean
- deployedAt?: Date
- apiKey?: string
- needsRedeployment?: boolean
-}
-
export interface ClipboardData {
blocks: Record
edges: Edge[]
@@ -45,7 +38,6 @@ export interface HydrationState {
export interface WorkflowRegistryState {
activeWorkflowId: string | null
error: string | null
- deploymentStatuses: Record
hydration: HydrationState
clipboard: ClipboardData | null
pendingSelection: string[] | null
@@ -57,14 +49,6 @@ export interface WorkflowRegistryActions {
switchToWorkspace: (id: string) => void
markWorkflowCreating: (workflowId: string) => void
markWorkflowCreated: (workflowId: string | null) => void
- getWorkflowDeploymentStatus: (workflowId: string | null) => DeploymentStatus | null
- setDeploymentStatus: (
- workflowId: string | null,
- isDeployed: boolean,
- deployedAt?: Date,
- apiKey?: string
- ) => void
- setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => void
copyBlocks: (blockIds: string[]) => void
preparePasteData: (positionOffset?: { x: number; y: number }) => {
blocks: Record
diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts
index de8a7b58126..e7900c5a8a2 100644
--- a/apps/sim/stores/workflows/workflow/store.ts
+++ b/apps/sim/stores/workflows/workflow/store.ts
@@ -107,8 +107,6 @@ const initialState = {
loops: {},
parallels: {},
lastSaved: undefined,
- deploymentStatuses: {},
- needsRedeployment: false,
}
export const useWorkflowStore = create()(
@@ -116,10 +114,6 @@ export const useWorkflowStore = create()(
(set, get) => ({
...initialState,
- setNeedsRedeploymentFlag: (needsRedeployment: boolean) => {
- set({ needsRedeployment })
- },
-
setCurrentWorkflowId: (currentWorkflowId) => {
set({ currentWorkflowId })
},
@@ -540,8 +534,6 @@ export const useWorkflowStore = create()(
loops: state.loops,
parallels: state.parallels,
lastSaved: state.lastSaved,
- deploymentStatuses: state.deploymentStatuses,
- needsRedeployment: state.needsRedeployment,
}
},
replaceWorkflowState: (
@@ -580,11 +572,6 @@ export const useWorkflowStore = create()(
edges: nextEdges,
loops: nextLoops,
parallels: nextParallels,
- deploymentStatuses: nextState.deploymentStatuses || state.deploymentStatuses,
- needsRedeployment:
- nextState.needsRedeployment !== undefined
- ? nextState.needsRedeployment
- : state.needsRedeployment,
lastSaved:
options?.updateLastSaved === true
? Date.now()
@@ -1141,67 +1128,6 @@ export const useWorkflowStore = create()(
}))
},
- revertToDeployedState: async (deployedState: WorkflowState) => {
- const activeWorkflowId = get().currentWorkflowId
-
- if (!activeWorkflowId) {
- logger.error('Cannot revert: no active workflow ID')
- return
- }
-
- const deploymentStatus = get().deploymentStatuses?.[activeWorkflowId]
-
- get().replaceWorkflowState({
- ...deployedState,
- needsRedeployment: false,
- deploymentStatuses: {
- ...get().deploymentStatuses,
- ...(deploymentStatus ? { [activeWorkflowId]: deploymentStatus } : {}),
- },
- })
-
- const values: Record> = {}
- Object.entries(deployedState.blocks).forEach(([blockId, block]) => {
- values[blockId] = {}
- Object.entries(block.subBlocks || {}).forEach(([subBlockId, subBlock]) => {
- values[blockId][subBlockId] = subBlock.value
- })
- })
-
- useSubBlockStore.setState({
- workflowValues: {
- ...useSubBlockStore.getState().workflowValues,
- [activeWorkflowId]: values,
- },
- })
-
- get().updateLastSaved()
-
- // Call API to persist the revert to normalized tables
- try {
- const response = await fetch(
- `/api/workflows/${activeWorkflowId}/deployments/active/revert`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- }
- )
-
- if (!response.ok) {
- const errorData = await response.json()
- logger.error('Failed to persist revert to deployed state:', errorData.error)
- // Don't throw error to avoid breaking the UI, but log it
- } else {
- logger.info('Successfully persisted revert to deployed state')
- }
- } catch (error) {
- logger.error('Error calling revert to deployed API:', error)
- // Don't throw error to avoid breaking the UI
- }
- },
-
toggleBlockAdvancedMode: (id: string) => {
const block = get().blocks[id]
if (!block) return
diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts
index 21f22ff5478..37d86847bcb 100644
--- a/apps/sim/stores/workflows/workflow/types.ts
+++ b/apps/sim/stores/workflows/workflow/types.ts
@@ -1,6 +1,5 @@
import type { Edge } from 'reactflow'
import type { OutputFieldDefinition, SubBlockType } from '@/blocks/types'
-import type { DeploymentStatus } from '@/stores/workflows/registry/types'
export const SUBFLOW_TYPES = {
LOOP: 'loop',
@@ -173,8 +172,6 @@ export interface WorkflowState {
exportedAt?: string
}
variables?: Record
- deploymentStatuses?: Record
- needsRedeployment?: boolean
dragStartPosition?: DragStartPosition | null
}
@@ -228,8 +225,6 @@ export interface WorkflowActions {
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => void
generateLoopBlocks: () => Record
generateParallelBlocks: () => Record
- setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
- revertToDeployedState: (deployedState: WorkflowState) => void
toggleBlockAdvancedMode: (id: string) => void
setDragStartPosition: (position: DragStartPosition | null) => void
getDragStartPosition: () => DragStartPosition | null
diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts
index 3ff33f1c9a0..e3ecf97fae2 100644
--- a/apps/sim/tools/index.test.ts
+++ b/apps/sim/tools/index.test.ts
@@ -25,6 +25,7 @@ const {
mockGetCustomToolById,
mockListCustomTools,
mockGetCustomToolByIdOrTitle,
+ mockGenerateInternalToken,
} = vi.hoisted(() => ({
mockIsHosted: { value: false },
mockEnv: { NEXT_PUBLIC_APP_URL: 'http://localhost:3000' } as Record,
@@ -38,6 +39,7 @@ const {
mockGetCustomToolById: vi.fn(),
mockListCustomTools: vi.fn(),
mockGetCustomToolByIdOrTitle: vi.fn(),
+ mockGenerateInternalToken: vi.fn(),
}))
// Mock feature flags
@@ -65,6 +67,10 @@ vi.mock('@/lib/api-key/byok', () => ({
getBYOKKey: (...args: unknown[]) => mockGetBYOKKey(...args),
}))
+vi.mock('@/lib/auth/internal', () => ({
+ generateInternalToken: (...args: unknown[]) => mockGenerateInternalToken(...args),
+}))
+
vi.mock('@/lib/billing/core/usage-log', () => ({}))
vi.mock('@/lib/core/rate-limiter/hosted-key', () => ({
@@ -193,8 +199,8 @@ vi.mock('@/tools/registry', () => {
return { tools: mockTools }
})
-// Mock custom tools - define mock data inside factory function
-vi.mock('@/hooks/queries/utils/custom-tool-cache', () => {
+// Mock query client for custom tool cache reads
+vi.mock('@/app/_shell/providers/get-query-client', () => {
const mockCustomTool = {
id: 'custom-tool-123',
title: 'Custom Weather Tool',
@@ -214,13 +220,12 @@ vi.mock('@/hooks/queries/utils/custom-tool-cache', () => {
},
}
return {
- getCustomTool: (toolId: string) => {
- if (toolId === 'custom-tool-123') {
- return mockCustomTool
- }
- return undefined
- },
- getCustomTools: () => [mockCustomTool],
+ getQueryClient: () => ({
+ getQueryData: (key: string[]) => {
+ if (key[0] === 'customTools') return [mockCustomTool]
+ return undefined
+ },
+ }),
}
})
@@ -1155,6 +1160,34 @@ describe('MCP Tool Execution', () => {
expect(result.timing).toBeDefined()
})
+ it('should embed userId in JWT when executionContext is undefined (agent block path)', async () => {
+ mockGenerateInternalToken.mockResolvedValue('test-token')
+
+ global.fetch = Object.assign(
+ vi.fn().mockImplementation(async () => ({
+ ok: true,
+ status: 200,
+ json: () =>
+ Promise.resolve({
+ success: true,
+ data: { output: { content: [{ type: 'text', text: 'OK' }] } },
+ }),
+ })),
+ { preconnect: vi.fn() }
+ ) as typeof fetch
+
+ await executeTool('mcp-123-test_tool', {
+ query: 'test',
+ _context: {
+ workspaceId: 'workspace-456',
+ workflowId: 'workflow-789',
+ userId: 'user-abc',
+ },
+ })
+
+ expect(mockGenerateInternalToken).toHaveBeenCalledWith('user-abc')
+ })
+
describe('Tool request retries', () => {
function makeJsonResponse(
status: number,
diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts
index 737dfa47bbd..e48e05baf2d 100644
--- a/apps/sim/tools/index.ts
+++ b/apps/sim/tools/index.ts
@@ -1552,11 +1552,13 @@ async function executeMcpTool(
const baseUrl = getInternalApiBaseUrl()
+ const mcpScope = resolveToolScope(params, executionContext)
+
const headers: Record = { 'Content-Type': 'application/json' }
if (typeof window === 'undefined') {
try {
- const internalToken = await generateInternalToken(executionContext?.userId)
+ const internalToken = await generateInternalToken(mcpScope.userId)
headers.Authorization = `Bearer ${internalToken}`
} catch (error) {
logger.error(`[${actualRequestId}] Failed to generate internal token:`, error)
@@ -1587,8 +1589,6 @@ async function executeMcpTool(
)
}
- const mcpScope = resolveToolScope(params, executionContext)
-
if (mcpScope.callChain && mcpScope.callChain.length > 0) {
headers[SIM_VIA_HEADER] = serializeCallChain(mcpScope.callChain)
}
diff --git a/apps/sim/tools/utils.test.ts b/apps/sim/tools/utils.test.ts
index 43a5531da91..9d8fa28f2f1 100644
--- a/apps/sim/tools/utils.test.ts
+++ b/apps/sim/tools/utils.test.ts
@@ -21,24 +21,23 @@ vi.mock('@/lib/core/security/input-validation.server', () => ({
secureFetchWithPinnedIP: vi.fn(),
}))
-vi.mock('@/stores/settings/environment', () => {
- const mockStore = {
- getAllVariables: vi.fn().mockReturnValue({
- API_KEY: { value: 'mock-api-key' },
- BASE_URL: { value: 'https://example.com' },
- }),
- }
-
- return {
- useEnvironmentStore: {
- getState: vi.fn().mockImplementation(() => mockStore),
- },
- }
-})
+const { mockGetQueryData } = vi.hoisted(() => ({
+ mockGetQueryData: vi.fn(),
+}))
+
+vi.mock('@/app/_shell/providers/get-query-client', () => ({
+ getQueryClient: () => ({
+ getQueryData: mockGetQueryData,
+ }),
+}))
const originalWindow = global.window
beforeEach(() => {
global.window = {} as any
+ mockGetQueryData.mockReturnValue({
+ API_KEY: { key: 'API_KEY', value: 'mock-api-key' },
+ BASE_URL: { key: 'BASE_URL', value: 'https://example.com' },
+ })
})
afterEach(() => {
@@ -651,15 +650,8 @@ describe('createParamSchema', () => {
})
describe('getClientEnvVars', () => {
- it.concurrent('should return environment variables from store in browser environment', () => {
- const mockStoreGetter = () => ({
- getAllVariables: () => ({
- API_KEY: { value: 'mock-api-key' },
- BASE_URL: { value: 'https://example.com' },
- }),
- })
-
- const result = getClientEnvVars(mockStoreGetter)
+ it('should return environment variables from React Query cache in browser environment', () => {
+ const result = getClientEnvVars()
expect(result).toEqual({
API_KEY: 'mock-api-key',
@@ -667,7 +659,7 @@ describe('getClientEnvVars', () => {
})
})
- it.concurrent('should return empty object in server environment', () => {
+ it('should return empty object in server environment', () => {
global.window = undefined as any
const result = getClientEnvVars()
@@ -677,7 +669,7 @@ describe('getClientEnvVars', () => {
})
describe('createCustomToolRequestBody', () => {
- it.concurrent('should create request body function for client-side execution', () => {
+ it('should create request body function for client-side execution', () => {
const customTool = {
code: 'return a + b',
schema: {
@@ -687,14 +679,7 @@ describe('createCustomToolRequestBody', () => {
},
}
- const mockStoreGetter = () => ({
- getAllVariables: () => ({
- API_KEY: { value: 'mock-api-key' },
- BASE_URL: { value: 'https://example.com' },
- }),
- })
-
- const bodyFn = createCustomToolRequestBody(customTool, true, undefined, mockStoreGetter)
+ const bodyFn = createCustomToolRequestBody(customTool, true)
const result = bodyFn({ a: 5, b: 3 })
expect(result).toEqual({
diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts
index 2f944c18bd4..534dc51797c 100644
--- a/apps/sim/tools/utils.ts
+++ b/apps/sim/tools/utils.ts
@@ -1,7 +1,9 @@
import { createLogger } from '@sim/logger'
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
+import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import type { CustomToolDefinition } from '@/hooks/queries/custom-tools'
-import { useEnvironmentStore } from '@/stores/settings/environment'
+import { environmentKeys } from '@/hooks/queries/environment'
+import type { EnvironmentVariable } from '@/stores/settings/environment'
import { tools } from '@/tools/registry'
import type { ToolConfig } from '@/tools/types'
@@ -215,20 +217,20 @@ export function createParamSchema(customTool: any): Record {
}
/**
- * Get environment variables from store (client-side only)
- * @param getStore Optional function to get the store (useful for testing)
+ * Get environment variables from React Query cache (client-side only)
*/
-export function getClientEnvVars(getStore?: () => any): Record {
+export function getClientEnvVars(): Record {
if (typeof window === 'undefined') return {}
try {
- // Allow injecting the store for testing
- const envStore = getStore ? getStore() : useEnvironmentStore.getState()
- const allEnvVars = envStore.getAllVariables()
+ const allEnvVars =
+ getQueryClient().getQueryData>(
+ environmentKeys.personal()
+ ) ?? {}
// Convert environment variables to a simple key-value object
return Object.entries(allEnvVars).reduce(
- (acc, [key, variable]: [string, any]) => {
+ (acc, [key, variable]) => {
acc[key] = variable.value
return acc
},
@@ -245,20 +247,14 @@ export function getClientEnvVars(getStore?: () => any): Record {
* @param customTool The custom tool configuration
* @param isClient Whether running on client side
* @param workflowId Optional workflow ID for server-side
- * @param getStore Optional function to get the store (useful for testing)
*/
-export function createCustomToolRequestBody(
- customTool: any,
- isClient = true,
- workflowId?: string,
- getStore?: () => any
-) {
+export function createCustomToolRequestBody(customTool: any, isClient = true, workflowId?: string) {
return (params: Record) => {
// Get environment variables - try multiple sources in order of preference:
// 1. envVars parameter (passed from provider/agent context)
// 2. Client-side store (if running in browser)
// 3. Empty object (fallback)
- const envVars = params.envVars || (isClient ? getClientEnvVars(getStore) : {})
+ const envVars = params.envVars || (isClient ? getClientEnvVars() : {})
// Get workflow variables from params (passed from execution context)
const workflowVariables = params.workflowVariables || {}