mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-15 01:47:59 -05:00
feat(debug): added debug mode & tests (#178)
This commit is contained in:
@@ -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'}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{timeAgo}</span>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<div className="flex items-center gap-2 ml-2 bg-muted rounded-md px-2 py-1">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-muted-foreground">Debug Mode</span>
|
||||
<span className="text-xs font-medium">
|
||||
{pendingCount} block{pendingCount !== 1 ? 's' : ''} pending
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleStepDebug}
|
||||
className="h-8 w-8 bg-background"
|
||||
disabled={pendingCount === 0}
|
||||
>
|
||||
<StepForward className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Step Forward</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleResumeDebug}
|
||||
className="h-8 w-8 bg-background"
|
||||
disabled={pendingCount === 0}
|
||||
>
|
||||
<SkipForward className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Resume Until End</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleCancelDebug}
|
||||
className="h-8 w-8 bg-background"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Cancel Debugging</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleToggleDebugMode}
|
||||
disabled={isExecuting || isMultiRunning}
|
||||
className={cn(isDebugModeEnabled && 'text-amber-500')}
|
||||
>
|
||||
<Bug className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle Debug Mode</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isDebugModeEnabled ? 'Disable Debug Mode' : 'Enable Debug Mode'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render run workflow button with multi-run dropdown
|
||||
*/
|
||||
const renderRunButton = () => (
|
||||
<div className="flex items-center">
|
||||
{showRunProgress && (
|
||||
<div className="mr-3 w-28">
|
||||
<Progress value={(completedRuns / runCount) * 100} className="h-2 bg-muted" />
|
||||
<p className="text-xs text-muted-foreground mt-1 text-center">
|
||||
{completedRuns}/{runCount} runs
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show how many blocks have been executed in debug mode if debugging */}
|
||||
{isDebugging && (
|
||||
<div className="mr-3 min-w-28 px-1 py-0.5 bg-muted rounded">
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<span className="font-medium">Debugging Mode</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderDebugControls()}
|
||||
|
||||
<div className="flex ml-1">
|
||||
{/* Main Run Button */}
|
||||
<Button
|
||||
@@ -588,52 +733,64 @@ export function ControlBar() {
|
||||
(isExecuting || isMultiRunning) &&
|
||||
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
|
||||
'disabled:opacity-50 disabled:hover:bg-[#7F2FFF] disabled:hover:shadow-none',
|
||||
'rounded-r-none border-r border-r-[#6420cc] py-1.5 px-3 h-10 text-sm rounded-l-sm'
|
||||
isDebugModeEnabled
|
||||
? 'rounded py-2 px-4 h-10'
|
||||
: 'rounded-r-none border-r border-r-[#6420cc] py-2 px-4 h-10'
|
||||
)}
|
||||
onClick={handleMultipleRuns}
|
||||
onClick={isDebugModeEnabled ? handleRunWorkflow : handleMultipleRuns}
|
||||
disabled={isExecuting || isMultiRunning}
|
||||
>
|
||||
<Play className={cn('!h-3 !w-3', 'fill-current stroke-current')} />
|
||||
{isDebugModeEnabled ? (
|
||||
<Bug className={cn('h-3.5 w-3.5 mr-1.5', 'fill-current stroke-current')} />
|
||||
) : (
|
||||
<Play className={cn('h-3.5 w-3.5', 'fill-current stroke-current')} />
|
||||
)}
|
||||
{isMultiRunning
|
||||
? `Running ${completedRuns}/${runCount}`
|
||||
: isExecuting
|
||||
? 'Running'
|
||||
: runCount === 1
|
||||
? 'Run'
|
||||
: `Run (${runCount})`}
|
||||
? isDebugging
|
||||
? 'Debugging'
|
||||
: 'Running'
|
||||
: isDebugModeEnabled
|
||||
? 'Debug'
|
||||
: runCount === 1
|
||||
? 'Run'
|
||||
: `Run (${runCount})`}
|
||||
</Button>
|
||||
|
||||
{/* Dropdown Trigger */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className={cn(
|
||||
'px-1.5 font-medium',
|
||||
'bg-[#7F2FFF] hover:bg-[#7028E6]',
|
||||
'shadow-[0_0_0_0_#7F2FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
|
||||
'text-white transition-all duration-200',
|
||||
(isExecuting || isMultiRunning) &&
|
||||
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
|
||||
'disabled:opacity-50 disabled:hover:bg-[#7F2FFF] disabled:hover:shadow-none',
|
||||
'rounded-l-none h-10 rounded-r-sm'
|
||||
)}
|
||||
disabled={isExecuting || isMultiRunning}
|
||||
>
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-20">
|
||||
{RUN_COUNT_OPTIONS.map((count) => (
|
||||
<DropdownMenuItem
|
||||
key={count}
|
||||
onClick={() => setRunCount(count)}
|
||||
className={cn('justify-center cursor-pointer', runCount === count && 'bg-muted')}
|
||||
{/* Dropdown Trigger - Only show when not in debug mode */}
|
||||
{!isDebugModeEnabled && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className={cn(
|
||||
'px-2 font-medium',
|
||||
'bg-[#7F2FFF] hover:bg-[#7028E6]',
|
||||
'shadow-[0_0_0_0_#7F2FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
|
||||
'text-white transition-all duration-200',
|
||||
(isExecuting || isMultiRunning) &&
|
||||
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
|
||||
'disabled:opacity-50 disabled:hover:bg-[#7F2FFF] disabled:hover:shadow-none',
|
||||
'rounded-l-none h-10'
|
||||
)}
|
||||
disabled={isExecuting || isMultiRunning}
|
||||
>
|
||||
{count}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-20">
|
||||
{RUN_COUNT_OPTIONS.map((count) => (
|
||||
<DropdownMenuItem
|
||||
key={count}
|
||||
onClick={() => setRunCount(count)}
|
||||
className={cn('justify-center', runCount === count && 'bg-muted')}
|
||||
>
|
||||
{count}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -651,6 +808,7 @@ export function ControlBar() {
|
||||
{renderDeleteButton()}
|
||||
{renderHistoryDropdown()}
|
||||
{renderNotificationsDropdown()}
|
||||
{renderDebugModeToggle()}
|
||||
{renderPublishButton()}
|
||||
{renderDeployButton()}
|
||||
{renderRunButton()}
|
||||
|
||||
@@ -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',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -381,7 +384,13 @@ function NotificationAlert({ notification, isFading, onHide }: NotificationAlert
|
||||
<div className="flex-1 space-y-2 mr-4">
|
||||
<AlertTitle className="flex items-center justify-between -mt-0.5">
|
||||
<span>
|
||||
{type === 'error' ? 'Error' : type === 'marketplace' ? 'Marketplace' : 'Console'}
|
||||
{type === 'error'
|
||||
? 'Error'
|
||||
: type === 'marketplace'
|
||||
? 'Marketplace'
|
||||
: type === 'info'
|
||||
? 'Info'
|
||||
: 'Console'}
|
||||
</span>
|
||||
|
||||
{/* Close button for persistent notifications */}
|
||||
|
||||
@@ -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<WorkflowBlockProps>) {
|
||||
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<WorkflowBlockProps>) {
|
||||
|
||||
// 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<WorkflowBlockProps>) {
|
||||
'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 && (
|
||||
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2 bg-amber-500 text-white text-xs px-2 py-0.5 rounded-t-md z-10">
|
||||
Next Step
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ActionBar blockId={id} blockType={type} />
|
||||
<ConnectionBlocks blockId={id} setIsConnecting={setIsConnecting} />
|
||||
|
||||
|
||||
@@ -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<ExecutionResult | null>(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<string, string>
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<ExecutionResult> {
|
||||
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<ExecutionResult> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
import { create } from 'zustand'
|
||||
import { Executor } from '@/executor'
|
||||
import { ExecutionContext } from '@/executor/types'
|
||||
|
||||
interface ExecutionState {
|
||||
activeBlockIds: Set<string>
|
||||
isExecuting: boolean
|
||||
isDebugging: boolean
|
||||
pendingBlocks: string[]
|
||||
executor: Executor | null
|
||||
debugContext: ExecutionContext | null
|
||||
}
|
||||
|
||||
interface ExecutionActions {
|
||||
setActiveBlocks: (blockIds: Set<string>) => 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<ExecutionState & ExecutionActions>()((set) => ({
|
||||
@@ -21,5 +35,9 @@ export const useExecutionStore = create<ExecutionState & ExecutionActions>()((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),
|
||||
}))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type NotificationType = 'error' | 'console' | 'api' | 'marketplace'
|
||||
export type NotificationType = 'error' | 'console' | 'api' | 'marketplace' | 'info'
|
||||
|
||||
export interface Notification {
|
||||
id: string
|
||||
|
||||
Reference in New Issue
Block a user