import { create } from 'zustand' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { type BlockRunStatus, defaultWorkflowExecutionState, deriveExecutionFlags, type EdgeRunStatus, type ExecutionActions, type ExecutionState, type ExecutionStatus, initialState, type WorkflowExecutionState, } from './types' /** * Returns the execution state for a workflow, creating a fresh default if absent. * * @remarks * When the workflow has no entry in the map, fresh `Set` and `Map` instances * are created so that callers never share mutable collections with * {@link defaultWorkflowExecutionState}. */ function getOrCreate( map: Map, workflowId: string ): WorkflowExecutionState { return ( map.get(workflowId) ?? { ...defaultWorkflowExecutionState, activeBlockIds: new Set(), lastRunPath: new Map(), lastRunEdges: new Map(), } ) } /** * Immutably updates a single workflow's execution state within the map. * * Creates a shallow copy of the outer map, merges the patch into the * target workflow's entry, and returns the new map. This ensures Zustand * detects the top-level reference change and notifies subscribers. */ function updatedMap( map: Map, workflowId: string, patch: Partial ): Map { const next = new Map(map) const current = getOrCreate(map, workflowId) next.set(workflowId, { ...current, ...patch }) return next } /** * Global Zustand store for per-workflow execution state. * * All execution state (running, debugging, block/edge highlights) is keyed * by workflow ID so users can run multiple workflows concurrently, each * with independent visual feedback. */ export const useExecutionStore = create()((set, get) => ({ ...initialState, getWorkflowExecution: (workflowId) => { return getOrCreate(get().workflowExecutions, workflowId) }, setActiveBlocks: (workflowId, blockIds) => { set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { activeBlockIds: new Set(blockIds), }), }) }, setPendingBlocks: (workflowId, pendingBlocks) => { set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { pendingBlocks }), }) }, setStatus: (workflowId, status, options) => { const patch: Partial = { status, ...deriveExecutionFlags(status), } if (options?.clearRunPath) { patch.lastRunPath = new Map() patch.lastRunEdges = new Map() } set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, patch), }) }, setIsExecuting: (workflowId, isExecuting) => { const current = getOrCreate(get().workflowExecutions, workflowId) const nextStatus: ExecutionStatus = isExecuting ? current.status === 'debugging' ? 'debugging' : 'running' : 'idle' get().setStatus(workflowId, nextStatus, { clearRunPath: isExecuting }) }, setIsDebugging: (workflowId, isDebugging) => { const current = getOrCreate(get().workflowExecutions, workflowId) const nextStatus: ExecutionStatus = isDebugging ? 'debugging' : current.status === 'debugging' ? 'running' : current.status get().setStatus(workflowId, nextStatus) }, setExecutor: (workflowId, executor) => { set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { executor }), }) }, setDebugContext: (workflowId, debugContext) => { set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { debugContext }), }) }, setBlockRunStatus: (workflowId, blockId, status) => { const current = getOrCreate(get().workflowExecutions, workflowId) const newRunPath = new Map(current.lastRunPath) newRunPath.set(blockId, status) set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { lastRunPath: newRunPath, }), }) }, setEdgeRunStatus: (workflowId, edgeId, status) => { const current = getOrCreate(get().workflowExecutions, workflowId) const newRunEdges = new Map(current.lastRunEdges) newRunEdges.set(edgeId, status) set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { lastRunEdges: newRunEdges, }), }) }, setCurrentExecutionId: (workflowId, executionId) => { set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { currentExecutionId: executionId, }), }) }, getCurrentExecutionId: (workflowId) => { return getOrCreate(get().workflowExecutions, workflowId).currentExecutionId }, clearRunPath: (workflowId) => { set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { lastRunPath: new Map(), lastRunEdges: new Map(), }), }) }, reset: () => set(initialState), setLastExecutionSnapshot: (workflowId, snapshot) => { const newSnapshots = new Map(get().lastExecutionSnapshots) newSnapshots.set(workflowId, snapshot) set({ lastExecutionSnapshots: newSnapshots }) }, getLastExecutionSnapshot: (workflowId) => { return get().lastExecutionSnapshots.get(workflowId) }, clearLastExecutionSnapshot: (workflowId) => { const newSnapshots = new Map(get().lastExecutionSnapshots) newSnapshots.delete(workflowId) set({ lastExecutionSnapshots: newSnapshots }) }, })) /** * Convenience hook that returns the execution state for the currently active workflow. */ export function useCurrentWorkflowExecution(): WorkflowExecutionState { const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) return useExecutionStore((state) => { if (!activeWorkflowId) return defaultWorkflowExecutionState return state.workflowExecutions.get(activeWorkflowId) ?? defaultWorkflowExecutionState }) } /** * Returns whether the active workflow is currently executing. * More granular than useCurrentWorkflowExecution — only re-renders when * the isExecuting boolean changes, not on every iteration update during * parallel-loop runs. */ export function useIsCurrentWorkflowExecuting(): boolean { const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) return useExecutionStore((state) => { if (!activeWorkflowId) return false return state.workflowExecutions.get(activeWorkflowId)?.isExecuting ?? false }) } /** * Returns whether a specific block is currently active (executing) in the current workflow. * More granular than useCurrentWorkflowExecution — only re-renders when * the boolean result changes for this specific block. */ export function useIsBlockActive(blockId: string): boolean { const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) return useExecutionStore((state) => { if (!activeWorkflowId) return false return state.workflowExecutions.get(activeWorkflowId)?.activeBlockIds.has(blockId) ?? false }) } /** * Returns the last run path (block statuses) for the current workflow. * More granular than useCurrentWorkflowExecution — only re-renders when * the lastRunPath map reference changes. */ export function useLastRunPath(): Map { const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) return useExecutionStore((state) => { if (!activeWorkflowId) return defaultWorkflowExecutionState.lastRunPath return ( state.workflowExecutions.get(activeWorkflowId)?.lastRunPath ?? defaultWorkflowExecutionState.lastRunPath ) }) } /** * Returns the last run edges (edge statuses) for the current workflow. * More granular than useCurrentWorkflowExecution — only re-renders when * the lastRunEdges map reference changes. */ export function useLastRunEdges(): Map { const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) return useExecutionStore((state) => { if (!activeWorkflowId) return defaultWorkflowExecutionState.lastRunEdges return ( state.workflowExecutions.get(activeWorkflowId)?.lastRunEdges ?? defaultWorkflowExecutionState.lastRunEdges ) }) }