mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-17 09:52:38 -05:00
Compare commits
2 Commits
feat/kb
...
active-exe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42d09ef8cb | ||
|
|
cbb98a0868 |
@@ -88,21 +88,38 @@ export function useTerminalFilters() {
|
|||||||
let result = entries
|
let result = entries
|
||||||
|
|
||||||
if (hasActiveFilters) {
|
if (hasActiveFilters) {
|
||||||
result = entries.filter((entry) => {
|
// Determine which top-level entries pass the filters
|
||||||
// Block ID filter
|
const visibleBlockIds = new Set<string>()
|
||||||
if (filters.blockIds.size > 0 && !filters.blockIds.has(entry.blockId)) {
|
for (const entry of entries) {
|
||||||
return false
|
if (entry.parentWorkflowBlockId) continue
|
||||||
}
|
|
||||||
|
|
||||||
// Status filter
|
let passes = true
|
||||||
if (filters.statuses.size > 0) {
|
if (filters.blockIds.size > 0 && !filters.blockIds.has(entry.blockId)) {
|
||||||
|
passes = false
|
||||||
|
}
|
||||||
|
if (passes && filters.statuses.size > 0) {
|
||||||
const isError = !!entry.error
|
const isError = !!entry.error
|
||||||
const hasStatus = isError ? filters.statuses.has('error') : filters.statuses.has('info')
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = entries.filter((entry) => visibleBlockIds.has(entry.blockId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by executionOrder (monotonically increasing integer from server)
|
// Sort by executionOrder (monotonically increasing integer from server)
|
||||||
|
|||||||
@@ -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({
|
const EntryNodeRow = memo(function EntryNodeRow({
|
||||||
node,
|
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 (
|
||||||
|
<div className='flex min-w-0 flex-col'>
|
||||||
|
{/* Workflow Block Header */}
|
||||||
|
<div
|
||||||
|
data-entry-id={entry.id}
|
||||||
|
className={clsx(
|
||||||
|
ROW_STYLES.base,
|
||||||
|
'h-[26px]',
|
||||||
|
isSelected ? ROW_STYLES.selected : ROW_STYLES.hover
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (hasChildren) {
|
||||||
|
onToggleNode(nodeId)
|
||||||
|
}
|
||||||
|
onSelectEntry(entry)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||||
|
<div
|
||||||
|
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||||
|
style={{ background: bgColor }}
|
||||||
|
>
|
||||||
|
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'min-w-0 truncate font-medium text-[13px]',
|
||||||
|
hasError
|
||||||
|
? 'text-[var(--text-error)]'
|
||||||
|
: isSelected || isExpanded
|
||||||
|
? 'text-[var(--text-primary)]'
|
||||||
|
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{entry.blockName}
|
||||||
|
</span>
|
||||||
|
{hasChildren && (
|
||||||
|
<ChevronDown
|
||||||
|
className={clsx(
|
||||||
|
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
|
||||||
|
!isExpanded && '-rotate-90'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'flex-shrink-0 font-medium text-[13px]',
|
||||||
|
!isRunning &&
|
||||||
|
(isCanceled ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]')
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<StatusDisplay
|
||||||
|
isRunning={isRunning}
|
||||||
|
isCanceled={isCanceled}
|
||||||
|
formattedDuration={formatDuration(entry.durationMs, { precision: 2 }) ?? '-'}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nested Child Workflow Blocks (recursive) */}
|
||||||
|
{isExpanded && hasChildren && (
|
||||||
|
<div className={ROW_STYLES.nested}>
|
||||||
|
{children.map((child) => (
|
||||||
|
<EntryNodeRow
|
||||||
|
key={child.entry.id}
|
||||||
|
node={child}
|
||||||
|
selectedEntryId={selectedEntryId}
|
||||||
|
onSelectEntry={onSelectEntry}
|
||||||
|
expandedNodes={expandedNodes}
|
||||||
|
onToggleNode={onToggleNode}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Regular block
|
// Regular block
|
||||||
return (
|
return (
|
||||||
<BlockRow
|
<BlockRow
|
||||||
@@ -555,6 +648,8 @@ export const Terminal = memo(function Terminal() {
|
|||||||
const uniqueBlocks = useMemo(() => {
|
const uniqueBlocks = useMemo(() => {
|
||||||
const blocksMap = new Map<string, { blockId: string; blockName: string; blockType: string }>()
|
const blocksMap = new Map<string, { blockId: string; blockName: string; blockType: string }>()
|
||||||
allWorkflowEntries.forEach((entry) => {
|
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)) {
|
if (!blocksMap.has(entry.blockId)) {
|
||||||
blocksMap.set(entry.blockId, {
|
blocksMap.set(entry.blockId, {
|
||||||
blockId: entry.blockId,
|
blockId: entry.blockId,
|
||||||
@@ -667,19 +762,22 @@ export const Terminal = memo(function Terminal() {
|
|||||||
|
|
||||||
const newestExec = executionGroups[0]
|
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[] = []
|
const nodeIdsToExpand: string[] = []
|
||||||
for (const node of newestExec.entryTree) {
|
const collectExpandableNodes = (nodes: EntryNode[]) => {
|
||||||
if (node.nodeType === 'subflow' && node.children.length > 0) {
|
for (const node of nodes) {
|
||||||
nodeIdsToExpand.push(node.entry.id)
|
if (node.children.length === 0) continue
|
||||||
// Also expand all iteration children
|
if (
|
||||||
for (const iterNode of node.children) {
|
node.nodeType === 'subflow' ||
|
||||||
if (iterNode.nodeType === 'iteration') {
|
node.nodeType === 'iteration' ||
|
||||||
nodeIdsToExpand.push(iterNode.entry.id)
|
node.nodeType === 'workflow'
|
||||||
}
|
) {
|
||||||
|
nodeIdsToExpand.push(node.entry.id)
|
||||||
|
collectExpandableNodes(node.children)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
collectExpandableNodes(newestExec.entryTree)
|
||||||
|
|
||||||
if (nodeIdsToExpand.length > 0) {
|
if (nodeIdsToExpand.length > 0) {
|
||||||
setExpandedNodes((prev) => {
|
setExpandedNodes((prev) => {
|
||||||
|
|||||||
@@ -120,10 +120,10 @@ export function isSubflowBlockType(blockType: string): boolean {
|
|||||||
/**
|
/**
|
||||||
* Node type for the tree structure
|
* 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 {
|
export interface EntryNode {
|
||||||
/** The console entry (for blocks) or synthetic entry (for subflows/iterations) */
|
/** 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.
|
* Sorts by start time to ensure chronological order.
|
||||||
*/
|
*/
|
||||||
function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
|
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 regularBlocks: ConsoleEntry[] = []
|
||||||
const iterationEntries: ConsoleEntry[] = []
|
const iterationEntries: ConsoleEntry[] = []
|
||||||
|
const childWorkflowEntries = new Map<string, ConsoleEntry[]>()
|
||||||
|
|
||||||
for (const entry of entries) {
|
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)
|
iterationEntries.push(entry)
|
||||||
} else {
|
} else {
|
||||||
regularBlocks.push(entry)
|
regularBlocks.push(entry)
|
||||||
@@ -338,12 +343,53 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build nodes for regular blocks
|
/**
|
||||||
const regularNodes: EntryNode[] = regularBlocks.map((entry) => ({
|
* Recursively builds child nodes for workflow blocks.
|
||||||
entry,
|
* Handles multi-level nesting where a child workflow block itself has children.
|
||||||
children: [],
|
*/
|
||||||
nodeType: 'block' as const,
|
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)
|
// Combine all nodes and sort by executionOrder ascending (oldest first, top-down)
|
||||||
const allNodes = [...subflowNodes, ...regularNodes]
|
const allNodes = [...subflowNodes, ...regularNodes]
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/executi
|
|||||||
import { useNotificationStore } from '@/stores/notifications'
|
import { useNotificationStore } from '@/stores/notifications'
|
||||||
import { useVariablesStore } from '@/stores/panel'
|
import { useVariablesStore } from '@/stores/panel'
|
||||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
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 { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
@@ -63,6 +67,7 @@ interface BlockEventHandlerConfig {
|
|||||||
executionIdRef: { current: string }
|
executionIdRef: { current: string }
|
||||||
workflowEdges: Array<{ id: string; target: string; sourceHandle?: string | null }>
|
workflowEdges: Array<{ id: string; target: string; sourceHandle?: string | null }>
|
||||||
activeBlocksSet: Set<string>
|
activeBlocksSet: Set<string>
|
||||||
|
activeBlockRefCounts: Map<string, number>
|
||||||
accumulatedBlockLogs: BlockLog[]
|
accumulatedBlockLogs: BlockLog[]
|
||||||
accumulatedBlockStates: Map<string, BlockState>
|
accumulatedBlockStates: Map<string, BlockState>
|
||||||
executedBlockIds: Set<string>
|
executedBlockIds: Set<string>
|
||||||
@@ -309,6 +314,7 @@ export function useWorkflowExecution() {
|
|||||||
executionIdRef,
|
executionIdRef,
|
||||||
workflowEdges,
|
workflowEdges,
|
||||||
activeBlocksSet,
|
activeBlocksSet,
|
||||||
|
activeBlockRefCounts,
|
||||||
accumulatedBlockLogs,
|
accumulatedBlockLogs,
|
||||||
accumulatedBlockStates,
|
accumulatedBlockStates,
|
||||||
executedBlockIds,
|
executedBlockIds,
|
||||||
@@ -328,9 +334,18 @@ export function useWorkflowExecution() {
|
|||||||
const updateActiveBlocks = (blockId: string, isActive: boolean) => {
|
const updateActiveBlocks = (blockId: string, isActive: boolean) => {
|
||||||
if (!workflowId) return
|
if (!workflowId) return
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
|
const count = activeBlockRefCounts.get(blockId) ?? 0
|
||||||
|
activeBlockRefCounts.set(blockId, count + 1)
|
||||||
activeBlocksSet.add(blockId)
|
activeBlocksSet.add(blockId)
|
||||||
} else {
|
} 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))
|
setActiveBlocks(workflowId, new Set(activeBlocksSet))
|
||||||
}
|
}
|
||||||
@@ -506,6 +521,20 @@ export function useWorkflowExecution() {
|
|||||||
addConsoleEntry(data, data.output as NormalizedBlockOutput)
|
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) {
|
if (onBlockCompleteCallback) {
|
||||||
onBlockCompleteCallback(data.blockId, data.output).catch((error) => {
|
onBlockCompleteCallback(data.blockId, data.output).catch((error) => {
|
||||||
logger.error('Error in onBlockComplete callback:', error)
|
logger.error('Error in onBlockComplete callback:', error)
|
||||||
@@ -1280,6 +1309,7 @@ export function useWorkflowExecution() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeBlocksSet = new Set<string>()
|
const activeBlocksSet = new Set<string>()
|
||||||
|
const activeBlockRefCounts = new Map<string, number>()
|
||||||
const streamedContent = new Map<string, string>()
|
const streamedContent = new Map<string, string>()
|
||||||
const accumulatedBlockLogs: BlockLog[] = []
|
const accumulatedBlockLogs: BlockLog[] = []
|
||||||
const accumulatedBlockStates = new Map<string, BlockState>()
|
const accumulatedBlockStates = new Map<string, BlockState>()
|
||||||
@@ -1292,6 +1322,7 @@ export function useWorkflowExecution() {
|
|||||||
executionIdRef,
|
executionIdRef,
|
||||||
workflowEdges,
|
workflowEdges,
|
||||||
activeBlocksSet,
|
activeBlocksSet,
|
||||||
|
activeBlockRefCounts,
|
||||||
accumulatedBlockLogs,
|
accumulatedBlockLogs,
|
||||||
accumulatedBlockStates,
|
accumulatedBlockStates,
|
||||||
executedBlockIds,
|
executedBlockIds,
|
||||||
@@ -1902,6 +1933,7 @@ export function useWorkflowExecution() {
|
|||||||
const accumulatedBlockStates = new Map<string, BlockState>()
|
const accumulatedBlockStates = new Map<string, BlockState>()
|
||||||
const executedBlockIds = new Set<string>()
|
const executedBlockIds = new Set<string>()
|
||||||
const activeBlocksSet = new Set<string>()
|
const activeBlocksSet = new Set<string>()
|
||||||
|
const activeBlockRefCounts = new Map<string, number>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blockHandlers = buildBlockEventHandlers({
|
const blockHandlers = buildBlockEventHandlers({
|
||||||
@@ -1909,6 +1941,7 @@ export function useWorkflowExecution() {
|
|||||||
executionIdRef,
|
executionIdRef,
|
||||||
workflowEdges,
|
workflowEdges,
|
||||||
activeBlocksSet,
|
activeBlocksSet,
|
||||||
|
activeBlockRefCounts,
|
||||||
accumulatedBlockLogs,
|
accumulatedBlockLogs,
|
||||||
accumulatedBlockStates,
|
accumulatedBlockStates,
|
||||||
executedBlockIds,
|
executedBlockIds,
|
||||||
@@ -2104,6 +2137,7 @@ export function useWorkflowExecution() {
|
|||||||
|
|
||||||
const workflowEdges = useWorkflowStore.getState().edges
|
const workflowEdges = useWorkflowStore.getState().edges
|
||||||
const activeBlocksSet = new Set<string>()
|
const activeBlocksSet = new Set<string>()
|
||||||
|
const activeBlockRefCounts = new Map<string, number>()
|
||||||
const accumulatedBlockLogs: BlockLog[] = []
|
const accumulatedBlockLogs: BlockLog[] = []
|
||||||
const accumulatedBlockStates = new Map<string, BlockState>()
|
const accumulatedBlockStates = new Map<string, BlockState>()
|
||||||
const executedBlockIds = new Set<string>()
|
const executedBlockIds = new Set<string>()
|
||||||
@@ -2115,6 +2149,7 @@ export function useWorkflowExecution() {
|
|||||||
executionIdRef,
|
executionIdRef,
|
||||||
workflowEdges,
|
workflowEdges,
|
||||||
activeBlocksSet,
|
activeBlocksSet,
|
||||||
|
activeBlockRefCounts,
|
||||||
accumulatedBlockLogs,
|
accumulatedBlockLogs,
|
||||||
accumulatedBlockStates,
|
accumulatedBlockStates,
|
||||||
executedBlockIds,
|
executedBlockIds,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import type { ExecutionResult, StreamingExecution } from '@/executor/types'
|
import type { ExecutionResult, StreamingExecution } from '@/executor/types'
|
||||||
import { useExecutionStore } from '@/stores/execution'
|
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 { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
|
|
||||||
@@ -39,6 +43,7 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
const workflowEdges = useWorkflowStore.getState().edges
|
const workflowEdges = useWorkflowStore.getState().edges
|
||||||
|
|
||||||
const activeBlocksSet = new Set<string>()
|
const activeBlocksSet = new Set<string>()
|
||||||
|
const activeBlockRefCounts = new Map<string, number>()
|
||||||
|
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
input: options.workflowInput,
|
input: options.workflowInput,
|
||||||
@@ -103,6 +108,8 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'block:started': {
|
case 'block:started': {
|
||||||
|
const startCount = activeBlockRefCounts.get(event.data.blockId) ?? 0
|
||||||
|
activeBlockRefCounts.set(event.data.blockId, startCount + 1)
|
||||||
activeBlocksSet.add(event.data.blockId)
|
activeBlocksSet.add(event.data.blockId)
|
||||||
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
||||||
|
|
||||||
@@ -115,8 +122,14 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'block:completed':
|
case 'block:completed': {
|
||||||
activeBlocksSet.delete(event.data.blockId)
|
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))
|
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
||||||
|
|
||||||
setBlockRunStatus(wfId, event.data.blockId, 'success')
|
setBlockRunStatus(wfId, event.data.blockId, 'success')
|
||||||
@@ -140,13 +153,34 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
iterationContainerId: event.data.iterationContainerId,
|
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) {
|
if (options.onBlockComplete) {
|
||||||
options.onBlockComplete(event.data.blockId, event.data.output).catch(() => {})
|
options.onBlockComplete(event.data.blockId, event.data.output).catch(() => {})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'block:error':
|
case 'block:error': {
|
||||||
activeBlocksSet.delete(event.data.blockId)
|
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))
|
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
||||||
|
|
||||||
setBlockRunStatus(wfId, event.data.blockId, 'error')
|
setBlockRunStatus(wfId, event.data.blockId, 'error')
|
||||||
@@ -171,6 +205,7 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
iterationContainerId: event.data.iterationContainerId,
|
iterationContainerId: event.data.iterationContainerId,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'execution:completed':
|
case 'execution:completed':
|
||||||
executionResult = {
|
executionResult = {
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ export class BlockExecutor {
|
|||||||
block: SerializedBlock,
|
block: SerializedBlock,
|
||||||
executionOrder: number
|
executionOrder: number
|
||||||
): void {
|
): void {
|
||||||
const blockId = node.id
|
const blockId = node.metadata?.originalBlockId ?? node.id
|
||||||
const blockName = block.metadata?.name ?? blockId
|
const blockName = block.metadata?.name ?? blockId
|
||||||
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
|
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ export class BlockExecutor {
|
|||||||
executionOrder: number,
|
executionOrder: number,
|
||||||
endedAt: string
|
endedAt: string
|
||||||
): void {
|
): void {
|
||||||
const blockId = node.id
|
const blockId = node.metadata?.originalBlockId ?? node.id
|
||||||
const blockName = block.metadata?.name ?? blockId
|
const blockName = block.metadata?.name ?? blockId
|
||||||
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
|
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export { indexedDBStorage } from './storage'
|
export { indexedDBStorage } from './storage'
|
||||||
export { useTerminalConsoleStore } from './store'
|
export { useTerminalConsoleStore } from './store'
|
||||||
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types'
|
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types'
|
||||||
|
export { extractChildWorkflowEntries, hasChildTraceSpans } from './utils'
|
||||||
|
|||||||
@@ -224,7 +224,11 @@ export const useTerminalConsoleStore = create<ConsoleStore>()(
|
|||||||
|
|
||||||
const newEntry = get().entries[0]
|
const newEntry = get().entries[0]
|
||||||
|
|
||||||
if (newEntry?.error && newEntry.blockType !== 'cancelled') {
|
if (
|
||||||
|
newEntry?.error &&
|
||||||
|
newEntry.blockType !== 'cancelled' &&
|
||||||
|
!newEntry.parentWorkflowBlockId
|
||||||
|
) {
|
||||||
notifyBlockError({
|
notifyBlockError({
|
||||||
error: newEntry.error,
|
error: newEntry.error,
|
||||||
blockName: newEntry.blockName || 'Unknown Block',
|
blockName: newEntry.blockName || 'Unknown Block',
|
||||||
@@ -249,7 +253,9 @@ export const useTerminalConsoleStore = create<ConsoleStore>()(
|
|||||||
})),
|
})),
|
||||||
|
|
||||||
exportConsoleCSV: (workflowId: string) => {
|
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) {
|
if (entries.length === 0) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface ConsoleEntry {
|
|||||||
iterationTotal?: number
|
iterationTotal?: number
|
||||||
iterationType?: SubflowType
|
iterationType?: SubflowType
|
||||||
iterationContainerId?: string
|
iterationContainerId?: string
|
||||||
|
parentWorkflowBlockId?: string
|
||||||
isRunning?: boolean
|
isRunning?: boolean
|
||||||
isCanceled?: boolean
|
isCanceled?: boolean
|
||||||
}
|
}
|
||||||
@@ -44,6 +45,7 @@ export interface ConsoleUpdate {
|
|||||||
iterationTotal?: number
|
iterationTotal?: number
|
||||||
iterationType?: SubflowType
|
iterationType?: SubflowType
|
||||||
iterationContainerId?: string
|
iterationContainerId?: string
|
||||||
|
parentWorkflowBlockId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsoleStore {
|
export interface ConsoleStore {
|
||||||
|
|||||||
78
apps/sim/stores/terminal/console/utils.ts
Normal file
78
apps/sim/stores/terminal/console/utils.ts
Normal file
@@ -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<ConsoleEntry, 'id' | 'timestamp'>[] {
|
||||||
|
const { parentBlockId, executionId, executionOrder, workflowId, childTraceSpans } = params
|
||||||
|
const entries: Omit<ConsoleEntry, 'id' | 'timestamp'>[] = []
|
||||||
|
|
||||||
|
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<string, unknown> & {
|
||||||
|
childTraceSpans: TraceSpan[]
|
||||||
|
} {
|
||||||
|
return (
|
||||||
|
output !== null &&
|
||||||
|
typeof output === 'object' &&
|
||||||
|
Array.isArray((output as Record<string, unknown>).childTraceSpans)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console'
|
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console'
|
||||||
export { useTerminalConsoleStore } from './console'
|
export { extractChildWorkflowEntries, hasChildTraceSpans, useTerminalConsoleStore } from './console'
|
||||||
export { useTerminalStore } from './store'
|
export { useTerminalStore } from './store'
|
||||||
export type { TerminalState } from './types'
|
export type { TerminalState } from './types'
|
||||||
|
|||||||
Reference in New Issue
Block a user