From cbb98a08686168fe33d2f78b46d2e5734a4f63fb Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 14 Feb 2026 12:58:18 -0800 Subject: [PATCH 1/2] fix(executor): resolve block ID for parallel subflow active state --- .../hooks/use-workflow-execution.ts | 19 +++++++++++++- .../utils/workflow-execution-utils.ts | 25 ++++++++++++++++--- apps/sim/executor/execution/block-executor.ts | 4 +-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 1088f8c87f..fceade9a1b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -63,6 +63,7 @@ interface BlockEventHandlerConfig { executionIdRef: { current: string } workflowEdges: Array<{ id: string; target: string; sourceHandle?: string | null }> activeBlocksSet: Set + activeBlockRefCounts: Map accumulatedBlockLogs: BlockLog[] accumulatedBlockStates: Map executedBlockIds: Set @@ -309,6 +310,7 @@ export function useWorkflowExecution() { executionIdRef, workflowEdges, activeBlocksSet, + activeBlockRefCounts, accumulatedBlockLogs, accumulatedBlockStates, executedBlockIds, @@ -328,9 +330,18 @@ export function useWorkflowExecution() { const updateActiveBlocks = (blockId: string, isActive: boolean) => { if (!workflowId) return if (isActive) { + const count = activeBlockRefCounts.get(blockId) ?? 0 + activeBlockRefCounts.set(blockId, count + 1) activeBlocksSet.add(blockId) } else { - activeBlocksSet.delete(blockId) + const count = activeBlockRefCounts.get(blockId) ?? 1 + const next = count - 1 + if (next <= 0) { + activeBlockRefCounts.delete(blockId) + activeBlocksSet.delete(blockId) + } else { + activeBlockRefCounts.set(blockId, next) + } } setActiveBlocks(workflowId, new Set(activeBlocksSet)) } @@ -1280,6 +1291,7 @@ export function useWorkflowExecution() { } const activeBlocksSet = new Set() + const activeBlockRefCounts = new Map() const streamedContent = new Map() const accumulatedBlockLogs: BlockLog[] = [] const accumulatedBlockStates = new Map() @@ -1292,6 +1304,7 @@ export function useWorkflowExecution() { executionIdRef, workflowEdges, activeBlocksSet, + activeBlockRefCounts, accumulatedBlockLogs, accumulatedBlockStates, executedBlockIds, @@ -1902,6 +1915,7 @@ export function useWorkflowExecution() { const accumulatedBlockStates = new Map() const executedBlockIds = new Set() const activeBlocksSet = new Set() + const activeBlockRefCounts = new Map() try { const blockHandlers = buildBlockEventHandlers({ @@ -1909,6 +1923,7 @@ export function useWorkflowExecution() { executionIdRef, workflowEdges, activeBlocksSet, + activeBlockRefCounts, accumulatedBlockLogs, accumulatedBlockStates, executedBlockIds, @@ -2104,6 +2119,7 @@ export function useWorkflowExecution() { const workflowEdges = useWorkflowStore.getState().edges const activeBlocksSet = new Set() + const activeBlockRefCounts = new Map() const accumulatedBlockLogs: BlockLog[] = [] const accumulatedBlockStates = new Map() const executedBlockIds = new Set() @@ -2115,6 +2131,7 @@ export function useWorkflowExecution() { executionIdRef, workflowEdges, activeBlocksSet, + activeBlockRefCounts, accumulatedBlockLogs, accumulatedBlockStates, executedBlockIds, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts index c0e54ea437..bf5e2e531b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts @@ -39,6 +39,7 @@ export async function executeWorkflowWithFullLogging( const workflowEdges = useWorkflowStore.getState().edges const activeBlocksSet = new Set() + const activeBlockRefCounts = new Map() const payload: any = { input: options.workflowInput, @@ -103,6 +104,8 @@ export async function executeWorkflowWithFullLogging( switch (event.type) { case 'block:started': { + const startCount = activeBlockRefCounts.get(event.data.blockId) ?? 0 + activeBlockRefCounts.set(event.data.blockId, startCount + 1) activeBlocksSet.add(event.data.blockId) setActiveBlocks(wfId, new Set(activeBlocksSet)) @@ -115,8 +118,14 @@ export async function executeWorkflowWithFullLogging( break } - case 'block:completed': - activeBlocksSet.delete(event.data.blockId) + case 'block:completed': { + const completeCount = activeBlockRefCounts.get(event.data.blockId) ?? 1 + if (completeCount <= 1) { + activeBlockRefCounts.delete(event.data.blockId) + activeBlocksSet.delete(event.data.blockId) + } else { + activeBlockRefCounts.set(event.data.blockId, completeCount - 1) + } setActiveBlocks(wfId, new Set(activeBlocksSet)) setBlockRunStatus(wfId, event.data.blockId, 'success') @@ -144,9 +153,16 @@ export async function executeWorkflowWithFullLogging( options.onBlockComplete(event.data.blockId, event.data.output).catch(() => {}) } break + } - case 'block:error': - activeBlocksSet.delete(event.data.blockId) + case 'block:error': { + const errorCount = activeBlockRefCounts.get(event.data.blockId) ?? 1 + if (errorCount <= 1) { + activeBlockRefCounts.delete(event.data.blockId) + activeBlocksSet.delete(event.data.blockId) + } else { + activeBlockRefCounts.set(event.data.blockId, errorCount - 1) + } setActiveBlocks(wfId, new Set(activeBlocksSet)) setBlockRunStatus(wfId, event.data.blockId, 'error') @@ -171,6 +187,7 @@ export async function executeWorkflowWithFullLogging( iterationContainerId: event.data.iterationContainerId, }) break + } case 'execution:completed': executionResult = { diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 58143e5832..56b7c6a915 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -428,7 +428,7 @@ export class BlockExecutor { block: SerializedBlock, executionOrder: number ): void { - const blockId = node.id + const blockId = node.metadata?.originalBlockId ?? node.id const blockName = block.metadata?.name ?? blockId const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE @@ -456,7 +456,7 @@ export class BlockExecutor { executionOrder: number, endedAt: string ): void { - const blockId = node.id + const blockId = node.metadata?.originalBlockId ?? node.id const blockName = block.metadata?.name ?? blockId const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE From 42d09ef8cb4713588a30a00e8af66ffad31413a2 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 14 Feb 2026 13:37:07 -0800 Subject: [PATCH 2/2] feat(terminal): show child workflow blocks as nested expandable tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a workflow block executes a child workflow, the terminal console now shows each child block as a nested expandable entry under the parent — matching the existing loop/parallel subflow pattern. - Extract childTraceSpans from workflow block output into console entries - Extend buildEntryTree with recursive workflow node nesting - Add 'workflow' node type to EntryNodeRow with recursive rendering - Shared typed utility (extractChildWorkflowEntries) for both execution paths - Filter UI excludes synthetic child IDs; children follow parent visibility - CSV export and error notifications skip child workflow entries Co-Authored-By: Claude Opus 4.6 --- .../terminal/hooks/use-terminal-filters.ts | 35 ++++-- .../components/terminal/terminal.tsx | 118 ++++++++++++++++-- .../[workflowId]/components/terminal/utils.ts | 66 ++++++++-- .../hooks/use-workflow-execution.ts | 20 ++- .../utils/workflow-execution-utils.ts | 20 ++- apps/sim/stores/terminal/console/index.ts | 1 + apps/sim/stores/terminal/console/store.ts | 10 +- apps/sim/stores/terminal/console/types.ts | 2 + apps/sim/stores/terminal/console/utils.ts | 78 ++++++++++++ apps/sim/stores/terminal/index.ts | 2 +- 10 files changed, 318 insertions(+), 34 deletions(-) create mode 100644 apps/sim/stores/terminal/console/utils.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-terminal-filters.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-terminal-filters.ts index c712864cf3..0d788410b9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-terminal-filters.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-terminal-filters.ts @@ -88,21 +88,38 @@ export function useTerminalFilters() { let result = entries if (hasActiveFilters) { - result = entries.filter((entry) => { - // Block ID filter + // Determine which top-level entries pass the filters + const visibleBlockIds = new Set() + for (const entry of entries) { + if (entry.parentWorkflowBlockId) continue + + let passes = true if (filters.blockIds.size > 0 && !filters.blockIds.has(entry.blockId)) { - return false + passes = false } - - // Status filter - if (filters.statuses.size > 0) { + if (passes && filters.statuses.size > 0) { const isError = !!entry.error const hasStatus = isError ? filters.statuses.has('error') : filters.statuses.has('info') - if (!hasStatus) return false + if (!hasStatus) passes = false + } + if (passes) { + visibleBlockIds.add(entry.blockId) + } + } + + // Propagate visibility to child workflow entries (handles arbitrary nesting). + // Keep iterating until no new children are discovered. + let prevSize = 0 + while (visibleBlockIds.size !== prevSize) { + prevSize = visibleBlockIds.size + for (const entry of entries) { + if (entry.parentWorkflowBlockId && visibleBlockIds.has(entry.parentWorkflowBlockId)) { + visibleBlockIds.add(entry.blockId) + } } + } - return true - }) + result = entries.filter((entry) => visibleBlockIds.has(entry.blockId)) } // Sort by executionOrder (monotonically increasing integer from server) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 8b19a3a35a..6262d434cc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -339,7 +339,8 @@ const SubflowNodeRow = memo(function SubflowNodeRow({ }) /** - * Entry node component - dispatches to appropriate component based on node type + * Entry node component - dispatches to appropriate component based on node type. + * Handles recursive rendering for workflow nodes with arbitrarily nested children. */ const EntryNodeRow = memo(function EntryNodeRow({ node, @@ -380,6 +381,98 @@ const EntryNodeRow = memo(function EntryNodeRow({ ) } + if (nodeType === 'workflow') { + const { entry, children } = node + const BlockIcon = getBlockIcon(entry.blockType) + const hasError = Boolean(entry.error) || children.some((c) => c.entry.error) + const bgColor = getBlockColor(entry.blockType) + const nodeId = entry.id + const isExpanded = expandedNodes.has(nodeId) + const hasChildren = children.length > 0 + const isSelected = selectedEntryId === entry.id + const isRunning = Boolean(entry.isRunning) + const isCanceled = Boolean(entry.isCanceled) + + return ( +
+ {/* Workflow Block Header */} +
{ + e.stopPropagation() + if (hasChildren) { + onToggleNode(nodeId) + } + onSelectEntry(entry) + }} + > +
+
+ {BlockIcon && } +
+ + {entry.blockName} + + {hasChildren && ( + + )} +
+ + + +
+ + {/* Nested Child Workflow Blocks (recursive) */} + {isExpanded && hasChildren && ( +
+ {children.map((child) => ( + + ))} +
+ )} +
+ ) + } + // Regular block return ( { const blocksMap = new Map() allWorkflowEntries.forEach((entry) => { + // Skip child workflow entries — they use synthetic IDs and shouldn't appear in filters + if (entry.parentWorkflowBlockId) return if (!blocksMap.has(entry.blockId)) { blocksMap.set(entry.blockId, { blockId: entry.blockId, @@ -667,19 +762,22 @@ export const Terminal = memo(function Terminal() { const newestExec = executionGroups[0] - // Collect all node IDs that should be expanded (subflows and their iterations) + // Collect all expandable node IDs recursively (subflows, iterations, and workflow nodes) const nodeIdsToExpand: string[] = [] - for (const node of newestExec.entryTree) { - if (node.nodeType === 'subflow' && node.children.length > 0) { - nodeIdsToExpand.push(node.entry.id) - // Also expand all iteration children - for (const iterNode of node.children) { - if (iterNode.nodeType === 'iteration') { - nodeIdsToExpand.push(iterNode.entry.id) - } + const collectExpandableNodes = (nodes: EntryNode[]) => { + for (const node of nodes) { + if (node.children.length === 0) continue + if ( + node.nodeType === 'subflow' || + node.nodeType === 'iteration' || + node.nodeType === 'workflow' + ) { + nodeIdsToExpand.push(node.entry.id) + collectExpandableNodes(node.children) } } } + collectExpandableNodes(newestExec.entryTree) if (nodeIdsToExpand.length > 0) { setExpandedNodes((prev) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts index 0c285a7b96..1bb97af852 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts @@ -120,10 +120,10 @@ export function isSubflowBlockType(blockType: string): boolean { /** * Node type for the tree structure */ -export type EntryNodeType = 'block' | 'subflow' | 'iteration' +export type EntryNodeType = 'block' | 'subflow' | 'iteration' | 'workflow' /** - * Entry node for tree structure - represents a block, subflow, or iteration + * Entry node for tree structure - represents a block, subflow, iteration, or workflow */ export interface EntryNode { /** The console entry (for blocks) or synthetic entry (for subflows/iterations) */ @@ -175,12 +175,17 @@ interface IterationGroup { * Sorts by start time to ensure chronological order. */ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] { - // Separate regular blocks from iteration entries + // Separate regular blocks from iteration entries and child workflow entries const regularBlocks: ConsoleEntry[] = [] const iterationEntries: ConsoleEntry[] = [] + const childWorkflowEntries = new Map() for (const entry of entries) { - if (entry.iterationType && entry.iterationCurrent !== undefined) { + if (entry.parentWorkflowBlockId) { + const existing = childWorkflowEntries.get(entry.parentWorkflowBlockId) || [] + existing.push(entry) + childWorkflowEntries.set(entry.parentWorkflowBlockId, existing) + } else if (entry.iterationType && entry.iterationCurrent !== undefined) { iterationEntries.push(entry) } else { regularBlocks.push(entry) @@ -338,12 +343,53 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] { }) } - // Build nodes for regular blocks - const regularNodes: EntryNode[] = regularBlocks.map((entry) => ({ - entry, - children: [], - nodeType: 'block' as const, - })) + /** + * Recursively builds child nodes for workflow blocks. + * Handles multi-level nesting where a child workflow block itself has children. + */ + const buildWorkflowChildNodes = (parentBlockId: string): EntryNode[] => { + const childEntries = childWorkflowEntries.get(parentBlockId) + if (!childEntries || childEntries.length === 0) return [] + + childEntries.sort((a, b) => { + const aTime = new Date(a.startedAt || a.timestamp).getTime() + const bTime = new Date(b.startedAt || b.timestamp).getTime() + return aTime - bTime + }) + + return childEntries.map((child) => { + const nestedChildren = buildWorkflowChildNodes(child.blockId) + if (nestedChildren.length > 0) { + return { + entry: child, + children: nestedChildren, + nodeType: 'workflow' as const, + } + } + return { + entry: child, + children: [], + nodeType: 'block' as const, + } + }) + } + + // Build nodes for regular blocks, promoting workflow blocks with children to 'workflow' nodes + const regularNodes: EntryNode[] = regularBlocks.map((entry) => { + const childNodes = buildWorkflowChildNodes(entry.blockId) + if (childNodes.length > 0) { + return { + entry, + children: childNodes, + nodeType: 'workflow' as const, + } + } + return { + entry, + children: [], + nodeType: 'block' as const, + } + }) // Combine all nodes and sort by executionOrder ascending (oldest first, top-down) const allNodes = [...subflowNodes, ...regularNodes] diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index fceade9a1b..f79833c841 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -38,7 +38,11 @@ import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/executi import { useNotificationStore } from '@/stores/notifications' import { useVariablesStore } from '@/stores/panel' import { useEnvironmentStore } from '@/stores/settings/environment' -import { useTerminalConsoleStore } from '@/stores/terminal' +import { + extractChildWorkflowEntries, + hasChildTraceSpans, + useTerminalConsoleStore, +} from '@/stores/terminal' import { useWorkflowDiffStore } from '@/stores/workflow-diff' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { mergeSubblockState } from '@/stores/workflows/utils' @@ -517,6 +521,20 @@ export function useWorkflowExecution() { addConsoleEntry(data, data.output as NormalizedBlockOutput) } + // Extract child workflow trace spans into separate console entries + if (data.blockType === 'workflow' && hasChildTraceSpans(data.output)) { + const childEntries = extractChildWorkflowEntries({ + parentBlockId: data.blockId, + executionId: executionIdRef.current, + executionOrder: data.executionOrder, + workflowId: workflowId!, + childTraceSpans: data.output.childTraceSpans, + }) + for (const entry of childEntries) { + addConsole(entry) + } + } + if (onBlockCompleteCallback) { onBlockCompleteCallback(data.blockId, data.output).catch((error) => { logger.error('Error in onBlockComplete callback:', error) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts index bf5e2e531b..dbada8da05 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts @@ -1,7 +1,11 @@ import { v4 as uuidv4 } from 'uuid' import type { ExecutionResult, StreamingExecution } from '@/executor/types' import { useExecutionStore } from '@/stores/execution' -import { useTerminalConsoleStore } from '@/stores/terminal' +import { + extractChildWorkflowEntries, + hasChildTraceSpans, + useTerminalConsoleStore, +} from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -149,6 +153,20 @@ export async function executeWorkflowWithFullLogging( iterationContainerId: event.data.iterationContainerId, }) + // Extract child workflow trace spans into separate console entries + if (event.data.blockType === 'workflow' && hasChildTraceSpans(event.data.output)) { + const childEntries = extractChildWorkflowEntries({ + parentBlockId: event.data.blockId, + executionId, + executionOrder: event.data.executionOrder, + workflowId: activeWorkflowId, + childTraceSpans: event.data.output.childTraceSpans, + }) + for (const entry of childEntries) { + addConsole(entry) + } + } + if (options.onBlockComplete) { options.onBlockComplete(event.data.blockId, event.data.output).catch(() => {}) } diff --git a/apps/sim/stores/terminal/console/index.ts b/apps/sim/stores/terminal/console/index.ts index d2b6679543..40bb3a8fcd 100644 --- a/apps/sim/stores/terminal/console/index.ts +++ b/apps/sim/stores/terminal/console/index.ts @@ -1,3 +1,4 @@ export { indexedDBStorage } from './storage' export { useTerminalConsoleStore } from './store' export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types' +export { extractChildWorkflowEntries, hasChildTraceSpans } from './utils' diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 9fddbf3efd..caa2bff1fb 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -224,7 +224,11 @@ export const useTerminalConsoleStore = create()( const newEntry = get().entries[0] - if (newEntry?.error && newEntry.blockType !== 'cancelled') { + if ( + newEntry?.error && + newEntry.blockType !== 'cancelled' && + !newEntry.parentWorkflowBlockId + ) { notifyBlockError({ error: newEntry.error, blockName: newEntry.blockName || 'Unknown Block', @@ -249,7 +253,9 @@ export const useTerminalConsoleStore = create()( })), exportConsoleCSV: (workflowId: string) => { - const entries = get().entries.filter((entry) => entry.workflowId === workflowId) + const entries = get().entries.filter( + (entry) => entry.workflowId === workflowId && !entry.parentWorkflowBlockId + ) if (entries.length === 0) { return diff --git a/apps/sim/stores/terminal/console/types.ts b/apps/sim/stores/terminal/console/types.ts index e057854d8c..6283538a0f 100644 --- a/apps/sim/stores/terminal/console/types.ts +++ b/apps/sim/stores/terminal/console/types.ts @@ -22,6 +22,7 @@ export interface ConsoleEntry { iterationTotal?: number iterationType?: SubflowType iterationContainerId?: string + parentWorkflowBlockId?: string isRunning?: boolean isCanceled?: boolean } @@ -44,6 +45,7 @@ export interface ConsoleUpdate { iterationTotal?: number iterationType?: SubflowType iterationContainerId?: string + parentWorkflowBlockId?: string } export interface ConsoleStore { diff --git a/apps/sim/stores/terminal/console/utils.ts b/apps/sim/stores/terminal/console/utils.ts new file mode 100644 index 0000000000..a18b5f13d3 --- /dev/null +++ b/apps/sim/stores/terminal/console/utils.ts @@ -0,0 +1,78 @@ +import type { TraceSpan } from '@/lib/logs/types' +import type { ConsoleEntry } from '@/stores/terminal/console/types' + +/** + * Parameters for extracting child workflow entries from trace spans + */ +interface ExtractChildWorkflowEntriesParams { + parentBlockId: string + executionId: string + executionOrder: number + workflowId: string + childTraceSpans: TraceSpan[] +} + +/** + * Extracts child workflow trace spans into console entry payloads. + * Handles recursive nesting for multi-level child workflows by flattening + * nested children with a parent block ID chain. + */ +export function extractChildWorkflowEntries( + params: ExtractChildWorkflowEntriesParams +): Omit[] { + const { parentBlockId, executionId, executionOrder, workflowId, childTraceSpans } = params + const entries: Omit[] = [] + + for (const span of childTraceSpans) { + if (!span.blockId) continue + + const childBlockId = `child-${parentBlockId}-${span.blockId}` + + entries.push({ + blockId: childBlockId, + blockName: span.name || 'Unknown Block', + blockType: span.type || 'unknown', + parentWorkflowBlockId: parentBlockId, + input: span.input || {}, + output: (span.output || {}) as ConsoleEntry['output'], + durationMs: span.duration, + startedAt: span.startTime, + endedAt: span.endTime, + success: span.status !== 'error', + error: + span.status === 'error' + ? (span.output?.error as string) || `${span.name || 'Block'} failed` + : undefined, + executionId, + executionOrder, + workflowId, + }) + + // Recursively extract nested child workflow spans + if (span.children && span.children.length > 0 && span.type === 'workflow') { + const nestedEntries = extractChildWorkflowEntries({ + parentBlockId: childBlockId, + executionId, + executionOrder, + workflowId, + childTraceSpans: span.children, + }) + entries.push(...nestedEntries) + } + } + + return entries +} + +/** + * Checks if a block completed event output contains child trace spans + */ +export function hasChildTraceSpans(output: unknown): output is Record & { + childTraceSpans: TraceSpan[] +} { + return ( + output !== null && + typeof output === 'object' && + Array.isArray((output as Record).childTraceSpans) + ) +} diff --git a/apps/sim/stores/terminal/index.ts b/apps/sim/stores/terminal/index.ts index e031ce303d..9b65ac1c0e 100644 --- a/apps/sim/stores/terminal/index.ts +++ b/apps/sim/stores/terminal/index.ts @@ -1,4 +1,4 @@ export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console' -export { useTerminalConsoleStore } from './console' +export { extractChildWorkflowEntries, hasChildTraceSpans, useTerminalConsoleStore } from './console' export { useTerminalStore } from './store' export type { TerminalState } from './types'