diff --git a/sim/app/w/[id]/components/control-bar/components/notification-dropdown-item/notification-dropdown-item.tsx b/sim/app/w/[id]/components/control-bar/components/notification-dropdown-item/notification-dropdown-item.tsx
index 3f9a41a93..7fe56c91c 100644
--- a/sim/app/w/[id]/components/control-bar/components/notification-dropdown-item/notification-dropdown-item.tsx
+++ b/sim/app/w/[id]/components/control-bar/components/notification-dropdown-item/notification-dropdown-item.tsx
@@ -2,15 +2,10 @@ import { useEffect, useState } from 'react'
import { formatDistanceToNow } from 'date-fns'
import { AlertCircle, Copy, Rocket, Store, Terminal, X } from 'lucide-react'
import { ErrorIcon } from '@/components/icons'
-import { Button } from '@/components/ui/button'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
import { useNotificationStore } from '@/stores/notifications/store'
-import {
- NotificationOptions,
- NotificationStore,
- NotificationType,
-} from '@/stores/notifications/types'
+import { NotificationOptions, NotificationType } from '@/stores/notifications/types'
interface NotificationDropdownItemProps {
id: string
@@ -25,6 +20,7 @@ const NotificationIcon = {
console: Terminal,
api: Rocket,
marketplace: Store,
+ info: AlertCircle,
}
const NotificationColors = {
@@ -32,6 +28,7 @@ const NotificationColors = {
console: 'text-foreground',
api: 'text-[#7F2FFF]',
marketplace: 'text-foreground',
+ info: 'text-blue-500',
}
export function NotificationDropdownItem({
@@ -70,7 +67,9 @@ export function NotificationDropdownItem({
? 'API'
: type === 'marketplace'
? 'Marketplace'
- : 'Console'}
+ : type === 'info'
+ ? 'Info'
+ : 'Console'}
{timeAgo}
diff --git a/sim/app/w/[id]/components/control-bar/control-bar.tsx b/sim/app/w/[id]/components/control-bar/control-bar.tsx
index a1555c240..46b35b368 100644
--- a/sim/app/w/[id]/components/control-bar/control-bar.tsx
+++ b/sim/app/w/[id]/components/control-bar/control-bar.tsx
@@ -3,7 +3,20 @@
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { formatDistanceToNow } from 'date-fns'
-import { Bell, ChevronDown, History, Loader2, Play, Rocket, Store, Trash2 } from 'lucide-react'
+import {
+ Bell,
+ Bug,
+ ChevronDown,
+ History,
+ Loader2,
+ Play,
+ Rocket,
+ SkipForward,
+ StepForward,
+ Store,
+ Trash2,
+ X,
+} from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
@@ -26,7 +39,9 @@ import { Progress } from '@/components/ui/progress'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
+import { useExecutionStore } from '@/stores/execution/store'
import { useNotificationStore } from '@/stores/notifications/store'
+import { useGeneralStore } from '@/stores/settings/general/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
@@ -61,6 +76,11 @@ export function ControlBar() {
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow } = useWorkflowRegistry()
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
+ // Debug mode state
+ const { isDebugModeEnabled, toggleDebugMode } = useGeneralStore()
+ const { isDebugging, pendingBlocks, handleStepDebug, handleCancelDebug, handleResumeDebug } =
+ useWorkflowExecution()
+
// Local state
const [mounted, setMounted] = useState(false)
const [, forceUpdate] = useState({})
@@ -84,6 +104,7 @@ export function ControlBar() {
const [runCount, setRunCount] = useState(1)
const [completedRuns, setCompletedRuns] = useState(0)
const [isMultiRunning, setIsMultiRunning] = useState(false)
+ const [showRunProgress, setShowRunProgress] = useState(false)
// Get notifications for current workflow
const workflowNotifications = activeWorkflowId
@@ -304,6 +325,7 @@ export function ControlBar() {
// Reset state for a new batch of runs
setCompletedRuns(0)
setIsMultiRunning(true)
+ setShowRunProgress(runCount > 1)
try {
// Run the workflow multiple times sequentially
@@ -329,6 +351,10 @@ export function ControlBar() {
addNotification('error', 'Failed to complete all workflow runs', activeWorkflowId)
} finally {
setIsMultiRunning(false)
+ // Keep progress visible for a moment after completion
+ if (runCount > 1) {
+ setTimeout(() => setShowRunProgress(false), 2000)
+ }
}
}
@@ -572,11 +598,130 @@ export function ControlBar() {
)
+ /**
+ * Render debug mode controls
+ */
+ const renderDebugControls = () => {
+ // Display debug controls only when in debug mode and actively debugging
+ if (!isDebugModeEnabled || !isDebugging) return null
+
+ const pendingCount = pendingBlocks.length
+
+ return (
+
+
+ Debug Mode
+
+ {pendingCount} block{pendingCount !== 1 ? 's' : ''} pending
+
+
+
+
+
+
+ Step Forward
+
+
+
+
+
+ Resume Until End
+
+
+
+
+
+ Cancel Debugging
+
+
+ )
+ }
+
+ /**
+ * Render debug mode toggle button
+ */
+ const renderDebugModeToggle = () => {
+ const handleToggleDebugMode = () => {
+ // If turning off debug mode, make sure to clean up any debug state
+ if (isDebugModeEnabled) {
+ // Only clean up if we're not actively executing
+ if (!isExecuting) {
+ useExecutionStore.getState().setIsDebugging(false)
+ useExecutionStore.getState().setPendingBlocks([])
+ }
+ }
+ toggleDebugMode()
+ }
+
+ return (
+
+
+
+
+
+ {isDebugModeEnabled ? 'Disable Debug Mode' : 'Enable Debug Mode'}
+
+
+ )
+ }
+
/**
* Render run workflow button with multi-run dropdown
*/
const renderRunButton = () => (
+ {showRunProgress && (
+
+
+
+ {completedRuns}/{runCount} runs
+
+
+ )}
+
+ {/* Show how many blocks have been executed in debug mode if debugging */}
+ {isDebugging && (
+
+ )}
+
+ {renderDebugControls()}
+
{/* Main Run Button */}
- {/* Dropdown Trigger */}
-
-
-
-
-
- {RUN_COUNT_OPTIONS.map((count) => (
- setRunCount(count)}
- className={cn('justify-center cursor-pointer', runCount === count && 'bg-muted')}
+ {/* Dropdown Trigger - Only show when not in debug mode */}
+ {!isDebugModeEnabled && (
+
+
+
- ))}
-
-
+
+
+
+
+ {RUN_COUNT_OPTIONS.map((count) => (
+ setRunCount(count)}
+ className={cn('justify-center', runCount === count && 'bg-muted')}
+ >
+ {count}
+
+ ))}
+
+
+ )}
)
@@ -651,6 +808,7 @@ export function ControlBar() {
{renderDeleteButton()}
{renderHistoryDropdown()}
{renderNotificationsDropdown()}
+ {renderDebugModeToggle()}
{renderPublishButton()}
{renderDeployButton()}
{renderRunButton()}
diff --git a/sim/app/w/[id]/components/notifications/notifications.tsx b/sim/app/w/[id]/components/notifications/notifications.tsx
index ea4c961c5..ab6f5b2e7 100644
--- a/sim/app/w/[id]/components/notifications/notifications.tsx
+++ b/sim/app/w/[id]/components/notifications/notifications.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
-import { Rocket, Store, Terminal, X } from 'lucide-react'
+import { Info, Rocket, Store, Terminal, X } from 'lucide-react'
import { ErrorIcon } from '@/components/icons'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import {
@@ -68,6 +68,7 @@ const NotificationIcon = {
console: Terminal,
api: Rocket,
marketplace: Store,
+ info: Info,
}
// Color schemes for different notification types
@@ -79,6 +80,7 @@ const NotificationColors = {
api: 'border-border bg-background text-foreground dark:border-border dark:text-foreground dark:bg-background',
marketplace:
'border-border bg-background text-foreground dark:border-border dark:text-foreground dark:bg-background',
+ info: 'border-blue-500 bg-blue-50 text-blue-800 dark:border-border dark:bg-blue-900/20 dark:text-blue-400',
}
// API deployment status styling
@@ -373,6 +375,7 @@ function NotificationAlert({ notification, isFading, onHide }: NotificationAlert
'!text-red-500 mt-[-3px]': type === 'error',
'text-foreground mt-[-3px]': type === 'console',
'mt-[-4.5px] text-foreground ': type === 'marketplace',
+ '!text-blue-500 mt-[-3px]': type === 'info',
})}
/>
@@ -381,7 +384,13 @@ function NotificationAlert({ notification, isFading, onHide }: NotificationAlert
- {type === 'error' ? 'Error' : type === 'marketplace' ? 'Marketplace' : 'Console'}
+ {type === 'error'
+ ? 'Error'
+ : type === 'marketplace'
+ ? 'Marketplace'
+ : type === 'info'
+ ? 'Info'
+ : 'Console'}
{/* Close button for persistent notifications */}
diff --git a/sim/app/w/[id]/components/workflow-block/workflow-block.tsx b/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
index e6359eabd..884913d0e 100644
--- a/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
+++ b/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
@@ -20,11 +20,13 @@ interface WorkflowBlockProps {
type: string
config: BlockConfig
name: string
+ isActive?: boolean
+ isPending?: boolean
}
// Combine both interfaces into a single component
export function WorkflowBlock({ id, data }: NodeProps) {
- const { type, config, name } = data
+ const { type, config, name, isActive: dataIsActive, isPending } = data
// State management
const [isConnecting, setIsConnecting] = useState(false)
@@ -52,6 +54,7 @@ export function WorkflowBlock({ id, data }: NodeProps) {
// Execution store
const isActiveBlock = useExecutionStore((state) => state.activeBlockIds.has(id))
+ const isActive = dataIsActive || isActiveBlock
// Update node internals when handles change
useEffect(() => {
@@ -200,9 +203,17 @@ export function WorkflowBlock({ id, data }: NodeProps) {
'transition-ring transition-block-bg',
isWide ? 'w-[480px]' : 'w-[320px]',
!isEnabled && 'shadow-sm',
- isActiveBlock && 'ring-2 animate-pulse-ring'
+ isActive && 'ring-2 animate-pulse-ring ring-blue-500',
+ isPending && 'ring-2 ring-amber-500'
)}
>
+ {/* Show debug indicator for pending blocks */}
+ {isPending && (
+
+ Next Step
+
+ )}
+
diff --git a/sim/app/w/[id]/hooks/use-workflow-execution.ts b/sim/app/w/[id]/hooks/use-workflow-execution.ts
index 74eb53ec9..e9be3aaed 100644
--- a/sim/app/w/[id]/hooks/use-workflow-execution.ts
+++ b/sim/app/w/[id]/hooks/use-workflow-execution.ts
@@ -6,6 +6,7 @@ import { useConsoleStore } from '@/stores/console/store'
import { useExecutionStore } from '@/stores/execution/store'
import { useNotificationStore } from '@/stores/notifications/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
+import { useGeneralStore } from '@/stores/settings/general/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -21,7 +22,19 @@ export function useWorkflowExecution() {
const { addNotification } = useNotificationStore()
const { toggleConsole } = useConsoleStore()
const { getAllVariables } = useEnvironmentStore()
- const { isExecuting, setIsExecuting } = useExecutionStore()
+ const { isDebugModeEnabled } = useGeneralStore()
+ const {
+ isExecuting,
+ isDebugging,
+ pendingBlocks,
+ executor,
+ debugContext,
+ setIsExecuting,
+ setIsDebugging,
+ setPendingBlocks,
+ setExecutor,
+ setDebugContext,
+ } = useExecutionStore()
const [executionResult, setExecutionResult] = useState(null)
const persistLogs = async (executionId: string, result: ExecutionResult) => {
@@ -59,9 +72,14 @@ export function useWorkflowExecution() {
if (!activeWorkflowId) return
setIsExecuting(true)
+ // Set debug mode if it's enabled in settings
+ if (isDebugModeEnabled) {
+ setIsDebugging(true)
+ }
+
// Get the current console state directly from the store
const currentIsOpen = useConsoleStore.getState().isOpen
-
+
// Open console if it's not already open
if (!currentIsOpen) {
toggleConsole()
@@ -70,6 +88,9 @@ export function useWorkflowExecution() {
const executionId = uuidv4()
try {
+ // Clear any existing state
+ setDebugContext(null)
+
// Use the mergeSubblockState utility to get all block states
const mergedStates = mergeSubblockState(blocks)
const currentBlockStates = Object.entries(mergedStates).reduce(
@@ -96,23 +117,42 @@ export function useWorkflowExecution() {
{} as Record
)
- // Execute workflow
+ // Create serialized workflow
const workflow = new Serializer().serializeWorkflow(mergedStates, edges, loops)
- const executor = new Executor(workflow, currentBlockStates, envVarValues)
- const result = await executor.execute(activeWorkflowId)
- // Set result and show notification immediately
- setExecutionResult(result)
- addNotification(
- result.success ? 'console' : 'error',
- result.success
- ? 'Workflow completed successfully'
- : `Workflow execution failed: ${result.error}`,
- activeWorkflowId
- )
+ // Create executor and store in global state
+ const newExecutor = new Executor(workflow, currentBlockStates, envVarValues)
+ setExecutor(newExecutor)
- // Send the entire execution result to our API to be processed server-side
- await persistLogs(executionId, result)
+ // Execute workflow
+ const result = await newExecutor.execute(activeWorkflowId)
+
+ // If we're in debug mode, store the execution context for later steps
+ if (result.metadata?.isDebugSession && result.metadata.context) {
+ setDebugContext(result.metadata.context)
+
+ // Make sure to update pending blocks
+ if (result.metadata.pendingBlocks) {
+ setPendingBlocks(result.metadata.pendingBlocks)
+ }
+ } else {
+ // Normal execution completed
+ setExecutionResult(result)
+
+ // Show notification
+ addNotification(
+ result.success ? 'console' : 'error',
+ result.success
+ ? 'Workflow completed successfully'
+ : `Workflow execution failed: ${result.error}`,
+ activeWorkflowId
+ )
+
+ // In non-debug mode, persist logs
+ await persistLogs(executionId, result)
+ setIsExecuting(false)
+ setIsDebugging(false)
+ }
} catch (error: any) {
logger.error('Workflow Execution Error:', error)
@@ -184,12 +224,19 @@ export function useWorkflowExecution() {
notificationMessage += `: ${errorMessage}`
}
- addNotification('error', notificationMessage, activeWorkflowId)
+ // Safely show error notification
+ try {
+ addNotification('error', notificationMessage, activeWorkflowId)
+ } catch (notificationError) {
+ logger.error('Error showing error notification:', notificationError)
+ // Fallback console error
+ console.error('Workflow execution failed:', errorMessage)
+ }
// Also send the error result to the API
await persistLogs(executionId, errorResult)
- } finally {
setIsExecuting(false)
+ setIsDebugging(false)
}
}, [
activeWorkflowId,
@@ -200,7 +247,335 @@ export function useWorkflowExecution() {
toggleConsole,
getAllVariables,
setIsExecuting,
+ setIsDebugging,
+ isDebugModeEnabled,
])
- return { isExecuting, executionResult, handleRunWorkflow }
+ /**
+ * Handles stepping through workflow execution in debug mode
+ */
+ const handleStepDebug = useCallback(async () => {
+ // Log debug information
+ logger.info('Step Debug requested', {
+ hasExecutor: !!executor,
+ hasContext: !!debugContext,
+ pendingBlockCount: pendingBlocks.length,
+ })
+
+ if (!executor || !debugContext || pendingBlocks.length === 0) {
+ logger.error('Cannot step debug - missing required state', {
+ executor: !!executor,
+ debugContext: !!debugContext,
+ pendingBlocks: pendingBlocks.length,
+ })
+
+ // Show error notification
+ addNotification(
+ 'error',
+ 'Cannot step through debugging - missing execution state. Try restarting debug mode.',
+ activeWorkflowId || ''
+ )
+
+ // Reset debug state
+ setIsDebugging(false)
+ setIsExecuting(false)
+ return
+ }
+
+ try {
+ console.log('Executing debug step with blocks:', pendingBlocks)
+
+ // Execute the next step with the pending blocks
+ const result = await executor.continueExecution(pendingBlocks, debugContext)
+
+ console.log('Debug step execution result:', result)
+
+ // Save the new context in the store
+ if (result.metadata?.context) {
+ setDebugContext(result.metadata.context)
+ }
+
+ // Check if the debug session is complete
+ if (
+ !result.metadata?.isDebugSession ||
+ !result.metadata.pendingBlocks ||
+ result.metadata.pendingBlocks.length === 0
+ ) {
+ logger.info('Debug session complete')
+ // Debug session complete
+ setExecutionResult(result)
+
+ // Show completion notification
+ addNotification(
+ result.success ? 'console' : 'error',
+ result.success
+ ? 'Workflow completed successfully'
+ : `Workflow execution failed: ${result.error}`,
+ activeWorkflowId || ''
+ )
+
+ // Persist logs
+ await persistLogs(uuidv4(), result)
+
+ // Reset debug state
+ setIsExecuting(false)
+ setIsDebugging(false)
+ setDebugContext(null)
+ setExecutor(null)
+ setPendingBlocks([])
+ } else {
+ // Debug session continues - update UI with new pending blocks
+ logger.info('Debug step completed, next blocks pending', {
+ nextPendingBlocks: result.metadata.pendingBlocks.length,
+ })
+
+ // This is critical - ensure we update the pendingBlocks in the store
+ setPendingBlocks(result.metadata.pendingBlocks)
+ }
+ } catch (error: any) {
+ logger.error('Debug Step Error:', error)
+
+ const errorMessage = error instanceof Error ? error.message : String(error)
+
+ // Create error result
+ const errorResult = {
+ success: false,
+ output: { response: {} },
+ error: errorMessage,
+ logs: debugContext.blockLogs,
+ }
+
+ setExecutionResult(errorResult)
+
+ // Safely show error notification
+ try {
+ addNotification('error', `Debug step failed: ${errorMessage}`, activeWorkflowId || '')
+ } catch (notificationError) {
+ logger.error('Error showing step error notification:', notificationError)
+ console.error('Debug step failed:', errorMessage)
+ }
+
+ // Persist logs
+ await persistLogs(uuidv4(), errorResult)
+
+ // Reset debug state
+ setIsExecuting(false)
+ setIsDebugging(false)
+ setDebugContext(null)
+ setExecutor(null)
+ setPendingBlocks([])
+ }
+ }, [
+ executor,
+ debugContext,
+ pendingBlocks,
+ activeWorkflowId,
+ addNotification,
+ setIsExecuting,
+ setIsDebugging,
+ setPendingBlocks,
+ setDebugContext,
+ setExecutor,
+ ])
+
+ /**
+ * Handles resuming execution in debug mode until completion
+ */
+ const handleResumeDebug = useCallback(async () => {
+ // Log debug information
+ logger.info('Resume Debug requested', {
+ hasExecutor: !!executor,
+ hasContext: !!debugContext,
+ pendingBlockCount: pendingBlocks.length,
+ })
+
+ if (!executor || !debugContext || pendingBlocks.length === 0) {
+ logger.error('Cannot resume debug - missing required state', {
+ executor: !!executor,
+ debugContext: !!debugContext,
+ pendingBlocks: pendingBlocks.length,
+ })
+
+ // Show error notification
+ addNotification(
+ 'error',
+ 'Cannot resume debugging - missing execution state. Try restarting debug mode.',
+ activeWorkflowId || ''
+ )
+
+ // Reset debug state
+ setIsDebugging(false)
+ setIsExecuting(false)
+ return
+ }
+
+ try {
+ // Show a notification that we're resuming execution
+ try {
+ addNotification(
+ 'info',
+ 'Resuming workflow execution until completion',
+ activeWorkflowId || ''
+ )
+ } catch (notificationError) {
+ logger.error('Error showing resume notification:', notificationError)
+ console.info('Resuming workflow execution until completion')
+ }
+
+ let currentResult: ExecutionResult = {
+ success: true,
+ output: { response: {} },
+ logs: debugContext.blockLogs,
+ }
+
+ // Create copies to avoid mutation issues
+ let currentContext = { ...debugContext }
+ let currentPendingBlocks = [...pendingBlocks]
+
+ console.log('Starting resume execution with blocks:', currentPendingBlocks)
+
+ // Continue execution until there are no more pending blocks
+ let iterationCount = 0
+ const maxIterations = 100 // Safety to prevent infinite loops
+
+ while (currentPendingBlocks.length > 0 && iterationCount < maxIterations) {
+ logger.info(
+ `Resume iteration ${iterationCount + 1}, executing ${currentPendingBlocks.length} blocks`
+ )
+
+ currentResult = await executor.continueExecution(currentPendingBlocks, currentContext)
+
+ logger.info(`Resume iteration result:`, {
+ success: currentResult.success,
+ hasPendingBlocks: !!currentResult.metadata?.pendingBlocks,
+ pendingBlockCount: currentResult.metadata?.pendingBlocks?.length || 0,
+ })
+
+ // Update context for next iteration
+ if (currentResult.metadata?.context) {
+ currentContext = currentResult.metadata.context
+ } else {
+ logger.info('No context in result, ending resume')
+ break // No context means we're done
+ }
+
+ // Update pending blocks for next iteration
+ if (currentResult.metadata?.pendingBlocks) {
+ currentPendingBlocks = currentResult.metadata.pendingBlocks
+ } else {
+ logger.info('No pending blocks in result, ending resume')
+ break // No pending blocks means we're done
+ }
+
+ // If we don't have a debug session anymore, we're done
+ if (!currentResult.metadata?.isDebugSession) {
+ logger.info('Debug session ended, ending resume')
+ break
+ }
+
+ iterationCount++
+ }
+
+ if (iterationCount >= maxIterations) {
+ logger.warn('Resume execution reached maximum iteration limit')
+ }
+
+ logger.info('Resume execution complete', {
+ iterationCount,
+ success: currentResult.success,
+ })
+
+ // Final result is the last step's result
+ setExecutionResult(currentResult)
+
+ // Show completion notification
+ try {
+ addNotification(
+ currentResult.success ? 'console' : 'error',
+ currentResult.success
+ ? 'Workflow completed successfully'
+ : `Workflow execution failed: ${currentResult.error}`,
+ activeWorkflowId || ''
+ )
+ } catch (notificationError) {
+ logger.error('Error showing completion notification:', notificationError)
+ console.info('Workflow execution completed')
+ }
+
+ // Persist logs
+ await persistLogs(uuidv4(), currentResult)
+
+ // Reset debug state
+ setIsExecuting(false)
+ setIsDebugging(false)
+ setDebugContext(null)
+ setExecutor(null)
+ setPendingBlocks([])
+ } catch (error: any) {
+ logger.error('Debug Resume Error:', error)
+
+ const errorMessage = error instanceof Error ? error.message : String(error)
+
+ // Create error result
+ const errorResult = {
+ success: false,
+ output: { response: {} },
+ error: errorMessage,
+ logs: debugContext.blockLogs,
+ }
+
+ setExecutionResult(errorResult)
+
+ // Safely show error notification
+ try {
+ addNotification('error', `Resume execution failed: ${errorMessage}`, activeWorkflowId || '')
+ } catch (notificationError) {
+ logger.error('Error showing resume error notification:', notificationError)
+ console.error('Resume execution failed:', errorMessage)
+ }
+
+ // Persist logs
+ await persistLogs(uuidv4(), errorResult)
+
+ // Reset debug state
+ setIsExecuting(false)
+ setIsDebugging(false)
+ setDebugContext(null)
+ setExecutor(null)
+ setPendingBlocks([])
+ }
+ }, [
+ executor,
+ debugContext,
+ pendingBlocks,
+ activeWorkflowId,
+ addNotification,
+ setIsExecuting,
+ setIsDebugging,
+ setPendingBlocks,
+ setDebugContext,
+ setExecutor,
+ ])
+
+ /**
+ * Handles cancelling the current debugging session
+ */
+ const handleCancelDebug = useCallback(() => {
+ setIsExecuting(false)
+ setIsDebugging(false)
+ setDebugContext(null)
+ setExecutor(null)
+ setPendingBlocks([])
+ }, [setIsExecuting, setIsDebugging, setDebugContext, setExecutor, setPendingBlocks])
+
+ return {
+ isExecuting,
+ isDebugging,
+ pendingBlocks,
+ executionResult,
+ handleRunWorkflow,
+ handleStepDebug,
+ handleResumeDebug,
+ handleCancelDebug,
+ }
}
diff --git a/sim/app/w/[id]/workflow.tsx b/sim/app/w/[id]/workflow.tsx
index 64dc04f2d..89d54a482 100644
--- a/sim/app/w/[id]/workflow.tsx
+++ b/sim/app/w/[id]/workflow.tsx
@@ -12,6 +12,7 @@ import ReactFlow, {
} from 'reactflow'
import 'reactflow/dist/style.css'
import { createLogger } from '@/lib/logs/console-logger'
+import { useExecutionStore } from '@/stores/execution/store'
import { useNotificationStore } from '@/stores/notifications/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { initializeSyncManagers, isSyncInitialized } from '@/stores/sync-registry'
@@ -54,6 +55,10 @@ function WorkflowContent() {
const { setValue: setSubBlockValue } = useSubBlockStore()
const { markAllAsRead } = useNotificationStore()
+ // Execution and debug mode state
+ const { activeBlockIds, pendingBlocks } = useExecutionStore()
+ const { isDebugModeEnabled } = useGeneralStore()
+
// Initialize workflow
useEffect(() => {
if (typeof window !== 'undefined') {
@@ -219,6 +224,9 @@ function WorkflowContent() {
}
}
+ const isActive = activeBlockIds.has(block.id)
+ const isPending = isDebugModeEnabled && pendingBlocks.includes(block.id)
+
nodeArray.push({
id: block.id,
type: 'workflowBlock',
@@ -229,12 +237,14 @@ function WorkflowContent() {
type: block.type,
config: blockConfig,
name: block.name,
+ isActive,
+ isPending,
},
})
})
return nodeArray
- }, [blocks, loops])
+ }, [blocks, loops, activeBlockIds, pendingBlocks, isDebugModeEnabled])
// Update nodes
const onNodesChange = useCallback(
diff --git a/sim/executor/index.test.ts b/sim/executor/index.test.ts
index 24753ade1..16c9c97b1 100644
--- a/sim/executor/index.test.ts
+++ b/sim/executor/index.test.ts
@@ -10,9 +10,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { SerializedWorkflow } from '../serializer/types'
import { Executor } from './index'
-import { NormalizedBlockOutput } from './types'
-// Mock all dependencies
vi.mock('@/lib/logs/console-logger', () => ({
createLogger: () => ({
error: vi.fn(),
@@ -36,13 +34,23 @@ vi.mock('@/stores/execution/store', () => ({
setIsExecuting: vi.fn(),
reset: vi.fn(),
setActiveBlocks: vi.fn(),
+ setPendingBlocks: vi.fn(),
+ setIsDebugging: vi.fn(),
}),
},
}))
-// Mock all handler classes with a proper mock implementation
+vi.mock('@/stores/settings/general/store', () => ({
+ useGeneralStore: {
+ getState: () => ({
+ isDebugModeEnabled: true,
+ }),
+ },
+}))
+
+// Mock all handler classes
vi.mock('./handlers', () => {
- // Create a factory function that returns a handler implementation
+ // Factory function for handler mocks
const createHandler = (handlerName: string) => {
return vi.fn().mockImplementation(() => ({
canHandle: (block: any) => block.metadata?.id === handlerName || handlerName === 'generic',
@@ -86,7 +94,7 @@ vi.mock('./loops', () => ({
* Test Fixtures
*/
-// Create a minimal workflow with just a starter and one block
+// Create a minimal workflow
const createMinimalWorkflow = (): SerializedWorkflow => ({
version: '1.0',
blocks: [
@@ -415,6 +423,20 @@ describe('Executor', () => {
})
})
+ /**
+ * Debug mode tests
+ */
+ describe('debug mode', () => {
+ // Test that the executor can be put into debug mode
+ test('should detect debug mode from settings', () => {
+ const workflow = createMinimalWorkflow()
+ const executor = new Executor(workflow)
+ const isDebugging = (executor as any).isDebugging
+
+ expect(isDebugging).toBe(true)
+ })
+ })
+
/**
* Additional tests to improve coverage
*/
diff --git a/sim/executor/index.ts b/sim/executor/index.ts
index 1b9afbcba..0c6e2a562 100644
--- a/sim/executor/index.ts
+++ b/sim/executor/index.ts
@@ -1,6 +1,7 @@
import { createLogger } from '@/lib/logs/console-logger'
import { useConsoleStore } from '@/stores/console/store'
import { useExecutionStore } from '@/stores/execution/store'
+import { useGeneralStore } from '@/stores/settings/general/store'
import { BlockOutput } from '@/blocks/types'
import { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
import {
@@ -32,6 +33,7 @@ export class Executor {
private pathTracker: PathTracker
private blockHandlers: BlockHandler[]
private workflowInput: any
+ private isDebugging: boolean = false
constructor(
private workflow: SerializedWorkflow,
@@ -55,6 +57,8 @@ export class Executor {
new ApiBlockHandler(),
new GenericBlockHandler(),
]
+
+ this.isDebugging = useGeneralStore.getState().isDebugModeEnabled
}
/**
@@ -64,7 +68,7 @@ export class Executor {
* @returns Execution result containing output, logs, and metadata
*/
async execute(workflowId: string): Promise {
- const { setIsExecuting, reset } = useExecutionStore.getState()
+ const { setIsExecuting, setIsDebugging, setPendingBlocks, reset } = useExecutionStore.getState()
const startTime = new Date()
let finalOutput: NormalizedBlockOutput = { response: {} }
@@ -75,6 +79,10 @@ export class Executor {
try {
setIsExecuting(true)
+ if (this.isDebugging) {
+ setIsDebugging(true)
+ }
+
let hasMoreLayers = true
let iteration = 0
const maxIterations = 100 // Safety limit for infinite loops
@@ -82,18 +90,45 @@ export class Executor {
while (hasMoreLayers && iteration < maxIterations) {
const nextLayer = this.getNextExecutionLayer(context)
- if (nextLayer.length === 0) {
- hasMoreLayers = false
- } else {
- const outputs = await this.executeLayer(nextLayer, context)
+ if (this.isDebugging) {
+ // In debug mode, update the pending blocks and wait for user interaction
+ setPendingBlocks(nextLayer)
- if (outputs.length > 0) {
- finalOutput = outputs[outputs.length - 1]
- }
-
- const hasLoopReachedMaxIterations = await this.loopManager.processLoopIterations(context)
- if (hasLoopReachedMaxIterations) {
+ // If there are no more blocks, we're done
+ if (nextLayer.length === 0) {
hasMoreLayers = false
+ } else {
+ // Return early to wait for manual stepping
+ // The caller (useWorkflowExecution) will handle resumption
+ return {
+ success: true,
+ output: finalOutput,
+ metadata: {
+ duration: Date.now() - startTime.getTime(),
+ startTime: context.metadata.startTime!,
+ pendingBlocks: nextLayer,
+ isDebugSession: true,
+ context: context, // Include context for resumption
+ },
+ logs: context.blockLogs,
+ }
+ }
+ } else {
+ // Normal execution without debug mode
+ if (nextLayer.length === 0) {
+ hasMoreLayers = false
+ } else {
+ const outputs = await this.executeLayer(nextLayer, context)
+
+ if (outputs.length > 0) {
+ finalOutput = outputs[outputs.length - 1]
+ }
+
+ const hasLoopReachedMaxIterations =
+ await this.loopManager.processLoopIterations(context)
+ if (hasLoopReachedMaxIterations) {
+ hasMoreLayers = false
+ }
}
}
@@ -123,7 +158,78 @@ export class Executor {
logs: context.blockLogs,
}
} finally {
- reset()
+ if (!this.isDebugging) {
+ reset()
+ }
+ }
+ }
+
+ /**
+ * Continues execution in debug mode from the current state.
+ *
+ * @param blockIds - Block IDs to execute in this step
+ * @param context - The current execution context
+ * @returns Updated execution result
+ */
+ async continueExecution(blockIds: string[], context: ExecutionContext): Promise {
+ const { setPendingBlocks } = useExecutionStore.getState()
+ let finalOutput: NormalizedBlockOutput = { response: {} }
+
+ try {
+ // Execute the current layer - using the original context, not a clone
+ const outputs = await this.executeLayer(blockIds, context)
+
+ if (outputs.length > 0) {
+ finalOutput = outputs[outputs.length - 1]
+ }
+
+ await this.loopManager.processLoopIterations(context)
+ const nextLayer = this.getNextExecutionLayer(context)
+ setPendingBlocks(nextLayer)
+
+ // Check if we've completed execution
+ const isComplete = nextLayer.length === 0
+
+ if (isComplete) {
+ const endTime = new Date()
+ context.metadata.endTime = endTime.toISOString()
+
+ return {
+ success: true,
+ output: finalOutput,
+ metadata: {
+ duration: endTime.getTime() - new Date(context.metadata.startTime!).getTime(),
+ startTime: context.metadata.startTime!,
+ endTime: context.metadata.endTime!,
+ pendingBlocks: [],
+ isDebugSession: false,
+ },
+ logs: context.blockLogs,
+ }
+ }
+
+ // Return the updated state for the next step
+ return {
+ success: true,
+ output: finalOutput,
+ metadata: {
+ duration: Date.now() - new Date(context.metadata.startTime!).getTime(),
+ startTime: context.metadata.startTime!,
+ pendingBlocks: nextLayer,
+ isDebugSession: true,
+ context: context, // Return the same context object for continuity
+ },
+ logs: context.blockLogs,
+ }
+ } catch (error: any) {
+ console.error('Debug step execution failed:', this.sanitizeError(error))
+
+ return {
+ success: false,
+ output: finalOutput,
+ error: this.extractErrorMessage(error),
+ logs: context.blockLogs,
+ }
}
}
diff --git a/sim/executor/types.ts b/sim/executor/types.ts
index 1e40f1970..12d95219f 100644
--- a/sim/executor/types.ts
+++ b/sim/executor/types.ts
@@ -104,7 +104,10 @@ export interface ExecutionResult {
metadata?: {
duration: number // Total execution time in milliseconds
startTime: string // ISO timestamp when execution started
- endTime: string // ISO timestamp when execution completed
+ endTime?: string // ISO timestamp when execution completed
+ pendingBlocks?: string[] // Blocks pending execution in debug mode
+ isDebugSession?: boolean // Whether this is a debug session
+ context?: ExecutionContext // Execution context for resuming in debug mode
}
}
diff --git a/sim/stores/execution/store.ts b/sim/stores/execution/store.ts
index 04b6f5b93..1c16e7745 100644
--- a/sim/stores/execution/store.ts
+++ b/sim/stores/execution/store.ts
@@ -1,19 +1,33 @@
import { create } from 'zustand'
+import { Executor } from '@/executor'
+import { ExecutionContext } from '@/executor/types'
interface ExecutionState {
activeBlockIds: Set
isExecuting: boolean
+ isDebugging: boolean
+ pendingBlocks: string[]
+ executor: Executor | null
+ debugContext: ExecutionContext | null
}
interface ExecutionActions {
setActiveBlocks: (blockIds: Set) => void
setIsExecuting: (isExecuting: boolean) => void
+ setIsDebugging: (isDebugging: boolean) => void
+ setPendingBlocks: (blockIds: string[]) => void
+ setExecutor: (executor: Executor | null) => void
+ setDebugContext: (context: ExecutionContext | null) => void
reset: () => void
}
const initialState: ExecutionState = {
activeBlockIds: new Set(),
isExecuting: false,
+ isDebugging: false,
+ pendingBlocks: [],
+ executor: null,
+ debugContext: null,
}
export const useExecutionStore = create()((set) => ({
@@ -21,5 +35,9 @@ export const useExecutionStore = create()((se
setActiveBlocks: (blockIds) => set({ activeBlockIds: new Set(blockIds) }),
setIsExecuting: (isExecuting) => set({ isExecuting }),
+ setIsDebugging: (isDebugging) => set({ isDebugging }),
+ setPendingBlocks: (pendingBlocks) => set({ pendingBlocks }),
+ setExecutor: (executor) => set({ executor }),
+ setDebugContext: (debugContext) => set({ debugContext }),
reset: () => set(initialState),
}))
diff --git a/sim/stores/notifications/types.ts b/sim/stores/notifications/types.ts
index d94a17fd8..05f10dbac 100644
--- a/sim/stores/notifications/types.ts
+++ b/sim/stores/notifications/types.ts
@@ -1,4 +1,4 @@
-export type NotificationType = 'error' | 'console' | 'api' | 'marketplace'
+export type NotificationType = 'error' | 'console' | 'api' | 'marketplace' | 'info'
export interface Notification {
id: string