diff --git a/ui/src/components/ProcessDiagram/CompoundNode.tsx b/ui/src/components/ProcessDiagram/CompoundNode.tsx index 549b1fae..9a8e45e1 100644 --- a/ui/src/components/ProcessDiagram/CompoundNode.tsx +++ b/ui/src/components/ProcessDiagram/CompoundNode.tsx @@ -68,18 +68,19 @@ export function CompoundNode({ // Execution overlay state for this compound const ownState = node.id ? executionOverlay?.get(node.id) : undefined; - const hasExecutedChild = ownState && hasExecutedDescendant(node, executionOverlay); + const executedDescendant = hasExecutedDescendant(node, executionOverlay); const isCompleted = ownState?.status === 'COMPLETED'; const isFailed = ownState?.status === 'FAILED'; + const descendantFailed = executedDescendant && hasFailedDescendant(node, executionOverlay); - // Gated = gate processor (filter/idempotent) blocked all children from executing - const isGated = ownState && !hasExecutedChild + // Gated = gate processor blocked all children from executing + const isGated = ownState && !executedDescendant && (ownState.filterMatched === false || ownState.duplicateMessage === true); - // Color priority: gated (amber) > failed (red) > completed (green) > default + // Color: own status first, then infer from descendants (for path containers like when/otherwise) const effectiveColor = isGated ? 'var(--amber)' - : isFailed ? '#C0392B' - : isCompleted ? '#3D7C47' + : isFailed || descendantFailed ? '#C0392B' + : isCompleted || executedDescendant ? '#3D7C47' : color; // Dim compound when overlay is active but neither the compound nor any @@ -295,3 +296,18 @@ function hasExecutedDescendant( } return false; } + +function hasFailedDescendant( + node: DiagramNodeType, + overlay?: Map, +): boolean { + if (!overlay || !node.children) return false; + for (const child of node.children) { + if (child.id) { + const state = overlay.get(child.id); + if (state?.status === 'FAILED') return true; + } + if (child.children && hasFailedDescendant(child, overlay)) return true; + } + return false; +}