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 1bffe22e4..c39b3d206 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 @@ -19,6 +19,7 @@ import { import { getBaseUrl } from '@/lib/core/utils/urls' import { getInputFormatExample as getInputFormatExampleUtil } from '@/lib/workflows/operations/deployment-utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { runPreDeployChecks } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-predeploy-checks' import { CreateApiKeyModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components' import { startsWithUuid } from '@/executor/constants' import { useA2AAgentByWorkflow } from '@/hooks/queries/a2a/agents' @@ -38,6 +39,7 @@ import { useWorkspaceSettings } from '@/hooks/queries/workspace' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsModalStore } from '@/stores/modals/settings/store' 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' import { A2aDeploy } from './components/a2a/a2a' @@ -335,6 +337,20 @@ export function DeployModal({ setDeployError(null) setDeployWarnings([]) + const { blocks, edges, loops, parallels } = useWorkflowStore.getState() + const liveBlocks = mergeSubblockState(blocks, workflowId) + const checkResult = runPreDeployChecks({ + blocks: liveBlocks, + edges, + loops, + parallels, + workflowId, + }) + if (!checkResult.passed) { + setDeployError(checkResult.error || 'Pre-deploy validation failed') + return + } + try { const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false }) if (result.warnings && result.warnings.length > 0) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts index 4a7036cd7..1f2a350d8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts @@ -2,9 +2,6 @@ import { useCallback, useState } from 'react' import { createLogger } from '@sim/logger' import { useNotificationStore } from '@/stores/notifications' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { mergeSubblockState } from '@/stores/workflows/utils' -import { useWorkflowStore } from '@/stores/workflows/workflow/store' -import { runPreDeployChecks } from './use-predeploy-checks' const logger = createLogger('useDeployment') @@ -25,36 +22,16 @@ export function useDeployment({ const [isDeploying, setIsDeploying] = useState(false) const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus) const addNotification = useNotificationStore((state) => state.addNotification) - const blocks = useWorkflowStore((state) => state.blocks) - const edges = useWorkflowStore((state) => state.edges) - const loops = useWorkflowStore((state) => state.loops) - const parallels = useWorkflowStore((state) => state.parallels) /** * Handle deploy button click * First deploy: calls API to deploy, then opens modal on success - * Redeploy: validates client-side, then opens modal if valid + * Already deployed: opens modal directly (validation happens on Update in modal) */ const handleDeployClick = useCallback(async () => { if (!workflowId) return { success: false, shouldOpenModal: false } if (isDeployed) { - const liveBlocks = mergeSubblockState(blocks, workflowId) - const checkResult = runPreDeployChecks({ - blocks: liveBlocks, - edges, - loops, - parallels, - workflowId, - }) - if (!checkResult.passed) { - addNotification({ - level: 'error', - message: checkResult.error || 'Pre-deploy validation failed', - workflowId, - }) - return { success: false, shouldOpenModal: false } - } return { success: true, shouldOpenModal: true } } @@ -101,17 +78,7 @@ export function useDeployment({ } finally { setIsDeploying(false) } - }, [ - workflowId, - isDeployed, - blocks, - edges, - loops, - parallels, - refetchDeployedState, - setDeploymentStatus, - addNotification, - ]) + }, [workflowId, isDeployed, refetchDeployedState, setDeploymentStatus, addNotification]) return { isDeploying, diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 5ede7fd02..d334a5580 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -62,6 +62,45 @@ const shouldSkipEntry = (output: any): boolean => { return false } +interface NotifyBlockErrorParams { + error: unknown + blockName: string + workflowId?: string + logContext: Record +} + +/** + * Sends an error notification for a block failure if error notifications are enabled. + */ +const notifyBlockError = ({ error, blockName, workflowId, logContext }: NotifyBlockErrorParams) => { + const settings = getQueryClient().getQueryData(generalSettingsKeys.settings()) + const isErrorNotificationsEnabled = settings?.errorNotificationsEnabled ?? true + + if (!isErrorNotificationsEnabled) return + + try { + const errorMessage = String(error) + const displayName = blockName || 'Unknown Block' + const displayMessage = `${displayName}: ${errorMessage}` + const copilotMessage = `${errorMessage}\n\nError in ${displayName}.\n\nPlease fix this.` + + useNotificationStore.getState().addNotification({ + level: 'error', + message: displayMessage, + workflowId, + action: { + type: 'copilot', + message: copilotMessage, + }, + }) + } catch (notificationError) { + logger.error('Failed to create block error notification', { + ...logContext, + error: notificationError, + }) + } +} + export const useTerminalConsoleStore = create()( devtools( persist( @@ -154,35 +193,12 @@ export const useTerminalConsoleStore = create()( const newEntry = get().entries[0] if (newEntry?.error) { - const settings = getQueryClient().getQueryData( - generalSettingsKeys.settings() - ) - const isErrorNotificationsEnabled = settings?.errorNotificationsEnabled ?? true - - if (isErrorNotificationsEnabled) { - try { - const errorMessage = String(newEntry.error) - const blockName = newEntry.blockName || 'Unknown Block' - const displayMessage = `${blockName}: ${errorMessage}` - - const copilotMessage = `${errorMessage}\n\nError in ${blockName}.\n\nPlease fix this.` - - useNotificationStore.getState().addNotification({ - level: 'error', - message: displayMessage, - workflowId: entry.workflowId, - action: { - type: 'copilot', - message: copilotMessage, - }, - }) - } catch (notificationError) { - logger.error('Failed to create block error notification', { - entryId: newEntry.id, - error: notificationError, - }) - } - } + notifyBlockError({ + error: newEntry.error, + blockName: newEntry.blockName || 'Unknown Block', + workflowId: entry.workflowId, + logContext: { entryId: newEntry.id }, + }) } return newEntry @@ -376,6 +392,18 @@ export const useTerminalConsoleStore = create()( return { entries: updatedEntries } }) + + if (typeof update === 'object' && update.error) { + const matchingEntry = get().entries.find( + (e) => e.blockId === blockId && e.executionId === executionId + ) + notifyBlockError({ + error: update.error, + blockName: matchingEntry?.blockName || 'Unknown Block', + workflowId: matchingEntry?.workflowId, + logContext: { blockId }, + }) + } }, cancelRunningEntries: (workflowId: string) => {