feat: support iteration wrapper nodes and filter overlay by selected iteration
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 38s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

Server:
- Add split_depth and loop_depth columns (V9 migration)
- Persist splitDepth/loopDepth with reflection fallback for older agent versions

UI:
- Detect iterations via wrapper processorTypes (loopIteration, splitIteration, multicastBranch)
- Filter overlay by selected iteration at the wrapper level
- Skip non-selected iteration wrappers entirely (wrapper + children)
- Don't add synthetic wrappers to overlay (no diagram node correspondence)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-28 18:57:27 +01:00
parent c4b396e618
commit faf5d505f4
6 changed files with 76 additions and 71 deletions

View File

@@ -1,13 +1,26 @@
import { useMemo } from 'react';
import type { NodeExecutionState, IterationInfo, ProcessorNode } from './types';
/** Synthetic wrapper processorTypes emitted by the agent for each iteration. */
const ITERATION_WRAPPER_TYPES = new Set([
'loopIteration', 'splitIteration', 'multicastBranch',
]);
/**
* Extract the iteration index from a wrapper node.
*/
function wrapperIndex(proc: ProcessorNode): number | undefined {
return proc.loopIndex ?? proc.splitIndex ?? proc.multicastIndex ?? undefined;
}
/**
* Recursively walks the ProcessorNode tree and populates an overlay map
* keyed by processorId → NodeExecutionState.
*
* Handles iteration filtering: when a processor has a loop/split/multicast
* index, only include it if it matches the currently selected iteration
* for its parent compound node.
* Iteration wrappers (loopIteration, splitIteration, multicastBranch) are
* used for filtering: only the wrapper matching the selected iteration
* is recursed into. The wrapper itself is not added to the overlay
* (it's synthetic and has no corresponding diagram node).
*/
function buildOverlay(
processors: ProcessorNode[],
@@ -19,29 +32,23 @@ function buildOverlay(
if (!proc.processorId || !proc.status) continue;
if (proc.status !== 'COMPLETED' && proc.status !== 'FAILED') continue;
// Iteration filtering: if this processor belongs to an iterated parent,
// only include it when the index matches the selected iteration.
if (parentId && iterationState.has(parentId)) {
const info = iterationState.get(parentId)!;
if (info.type === 'loop' && proc.loopIndex != null) {
if (proc.loopIndex !== info.current) {
// Still recurse into children so nested compounds are discovered,
// but skip adding this processor to the overlay.
continue;
// Iteration wrapper: filter by selected iteration, skip the wrapper itself
if (ITERATION_WRAPPER_TYPES.has(proc.processorType)) {
if (parentId && iterationState.has(parentId)) {
const info = iterationState.get(parentId)!;
const idx = wrapperIndex(proc);
if (idx != null && idx !== info.current) {
continue; // Skip this wrapper and all its children
}
}
if (info.type === 'split' && proc.splitIndex != null) {
if (proc.splitIndex !== info.current) {
continue;
}
}
if (info.type === 'multicast' && proc.multicastIndex != null) {
if (proc.multicastIndex !== info.current) {
continue;
}
// Matching wrapper: don't add to overlay but recurse into children
if (proc.children?.length) {
buildOverlay(proc.children, overlay, iterationState, proc.processorId);
}
continue;
}
// Regular processor: add to overlay
const subRouteFailed =
proc.status === 'FAILED' &&
(proc.processorType?.includes('DIRECT') || proc.processorType?.includes('SEDA'));
@@ -53,7 +60,7 @@ function buildOverlay(
hasTraceData: true,
});
// Recurse into children, passing this processor as the parent for iteration filtering.
// Recurse into children
if (proc.children?.length) {
buildOverlay(proc.children, overlay, iterationState, proc.processorId);
}

View File

@@ -1,56 +1,33 @@
import { useCallback, useEffect, useState } from 'react';
import type { IterationInfo, ProcessorNode } from './types';
const WRAPPER_TYPES: Record<string, IterationInfo['type']> = {
loopIteration: 'loop',
splitIteration: 'split',
multicastBranch: 'multicast',
};
/**
* Walks the processor tree and detects compound nodes that have iterated
* children (loop, split, multicast). Populates a map of compoundId →
* IterationInfo so the UI can show stepper widgets and filter iterations.
* Walks the processor tree and detects compound nodes that have iteration
* wrapper children (loopIteration, splitIteration, multicastBranch).
*/
function detectIterations(
processors: ProcessorNode[],
result: Map<string, IterationInfo>,
): void {
for (const proc of processors) {
if (!proc.children?.length) continue;
if (!proc.children?.length || !proc.processorId) continue;
// Check if children indicate a loop compound
const loopChild = proc.children.find(
(c) => c.loopSize != null && c.loopSize > 0,
);
if (loopChild && proc.processorId) {
result.set(proc.processorId, {
current: 0,
total: loopChild.loopSize!,
type: 'loop',
});
}
// Check if children indicate a split compound
const splitChild = proc.children.find(
(c) => c.splitSize != null && c.splitSize > 0,
);
if (splitChild && !loopChild && proc.processorId) {
result.set(proc.processorId, {
current: 0,
total: splitChild.splitSize!,
type: 'split',
});
}
// Check if children indicate a multicast compound
if (!loopChild && !splitChild) {
const multicastIndices = new Set<number>();
for (const child of proc.children) {
if (child.multicastIndex != null) {
multicastIndices.add(child.multicastIndex);
}
}
if (multicastIndices.size > 0 && proc.processorId) {
// Check if children are iteration wrappers
for (const [wrapperType, iterType] of Object.entries(WRAPPER_TYPES)) {
const wrappers = proc.children.filter(c => c.processorType === wrapperType);
if (wrappers.length > 0) {
result.set(proc.processorId, {
current: 0,
total: multicastIndices.size,
type: 'multicast',
total: wrappers.length,
type: iterType,
});
break;
}
}
@@ -62,13 +39,12 @@ function detectIterations(
/**
* Manages per-compound iteration state for the execution overlay.
*
* Scans the processor tree to detect compounds with iterated children
* and tracks which iteration index is currently selected for each.
* Scans the processor tree to detect compounds with iteration wrapper
* children and tracks which iteration index is currently selected.
*/
export function useIterationState(processors: ProcessorNode[] | undefined) {
const [state, setState] = useState<Map<string, IterationInfo>>(new Map());
// Initialize iteration info when processors change
useEffect(() => {
if (!processors) return;
const newState = new Map<string, IterationInfo>();