feat: render doTry/doCatch/doFinally like route-level handler sections
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s

Backend: DO_TRY compounds now use a virtual _TRY_BODY wrapper with LR
layout for the try body, while DO_CATCH/DO_FINALLY stack below as
separate sections (TB). Edges from DO_TRY are skipped like route-level
handler edges. Removes ELK-v2 debug logging.

Frontend: _TRY_BODY renders as transparent wrapper, DO_CATCH as red
tinted section, DO_FINALLY as teal section. DO_FINALLY color changed
from red to teal (completion handler, not error).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-28 09:04:36 +01:00
parent 1702200a60
commit dbf64ecb48
3 changed files with 205 additions and 64 deletions

View File

@@ -59,6 +59,47 @@ export function CompoundNode({
e => descendantIds.has(e.sourceId) && descendantIds.has(e.targetId),
);
const childProps = {
edges, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
overlayActive, iterationState, onIterationChange,
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
};
// _TRY_BODY: transparent wrapper — no header, no border, just layout
if (node.type === '_TRY_BODY') {
return (
<g transform={`translate(${x}, ${y})`}>
{renderInternalEdges(internalEdges, absX, absY, executionOverlay)}
{renderChildren(node, absX, absY, childProps)}
</g>
);
}
// DO_CATCH / DO_FINALLY: section-like styling (tinted bg, thin border, label)
if (node.type === 'DO_CATCH' || node.type === 'DO_FINALLY') {
const sectionLabel = node.type === 'DO_CATCH'
? (node.label ? `catch: ${node.label}` : 'catch')
: (node.label ? `finally: ${node.label}` : 'finally');
return (
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
{/* Tinted background */}
<rect x={0} y={0} width={w} height={h} rx={CORNER_RADIUS}
fill={color} fillOpacity={0.06} />
{/* Border */}
<rect x={0} y={0} width={w} height={h} rx={CORNER_RADIUS}
fill="none" stroke={color} strokeWidth={1} strokeOpacity={0.4} />
{/* Section label */}
<text x={8} y={12} fill={color} fontSize={10} fontWeight={600}>
{sectionLabel}
</text>
{renderInternalEdges(internalEdges, absX, absY, executionOverlay)}
{renderChildren(node, absX, absY, childProps)}
</g>
);
}
// Default compound rendering (DO_TRY, EIP_CHOICE, etc.)
return (
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
{/* Container body */}
@@ -110,46 +151,56 @@ export function CompoundNode({
</foreignObject>
)}
{/* Internal edges (rendered after background, before children) */}
<g className="edges">
{internalEdges.map((edge, i) => {
const isTraversed = executionOverlay
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
: undefined;
return (
<DiagramEdge
key={`${edge.sourceId}-${edge.targetId}-${i}`}
edge={{
...edge,
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
}}
traversed={isTraversed}
/>
);
})}
</g>
{renderInternalEdges(internalEdges, absX, absY, executionOverlay)}
{renderChildren(node, absX, absY, childProps)}
</g>
);
}
{/* Children — recurse into compound children, render leaves as DiagramNode */}
/** Render internal edges adjusted for compound coordinates */
function renderInternalEdges(
internalEdges: DiagramEdgeType[],
absX: number, absY: number,
executionOverlay?: Map<string, NodeExecutionState>,
) {
return (
<g className="edges">
{internalEdges.map((edge, i) => {
const isTraversed = executionOverlay
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
: undefined;
return (
<DiagramEdge
key={`${edge.sourceId}-${edge.targetId}-${i}`}
edge={{
...edge,
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
}}
traversed={isTraversed}
/>
);
})}
</g>
);
}
/** Render children — compounds recurse, leaves become DiagramNode */
function renderChildren(
node: DiagramNodeType,
absX: number, absY: number,
props: Omit<CompoundNodeProps, 'node' | 'parentX' | 'parentY'>,
) {
return (
<>
{node.children?.map(child => {
if (isCompoundType(child.type) && child.children && child.children.length > 0) {
return (
<CompoundNode
key={child.id}
node={child}
edges={edges}
parentX={absX}
parentY={absY}
selectedNodeId={selectedNodeId}
hoveredNodeId={hoveredNodeId}
nodeConfigs={nodeConfigs}
executionOverlay={executionOverlay}
overlayActive={overlayActive}
iterationState={iterationState}
onIterationChange={onIterationChange}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeEnter={onNodeEnter}
onNodeLeave={onNodeLeave}
{...props}
/>
);
}
@@ -161,19 +212,19 @@ export function CompoundNode({
x: (child.x ?? 0) - absX,
y: (child.y ?? 0) - absY,
}}
isHovered={hoveredNodeId === child.id}
isSelected={selectedNodeId === child.id}
config={child.id ? nodeConfigs?.get(child.id) : undefined}
executionState={executionOverlay?.get(child.id ?? '')}
overlayActive={overlayActive}
onClick={() => child.id && onNodeClick(child.id)}
onDoubleClick={() => child.id && onNodeDoubleClick?.(child.id)}
onMouseEnter={() => child.id && onNodeEnter(child.id)}
onMouseLeave={onNodeLeave}
isHovered={props.hoveredNodeId === child.id}
isSelected={props.selectedNodeId === child.id}
config={child.id ? props.nodeConfigs?.get(child.id) : undefined}
executionState={props.executionOverlay?.get(child.id ?? '')}
overlayActive={props.overlayActive}
onClick={() => child.id && props.onNodeClick(child.id)}
onDoubleClick={() => child.id && props.onNodeDoubleClick?.(child.id)}
onMouseEnter={() => child.id && props.onNodeEnter(child.id)}
onMouseLeave={props.onNodeLeave}
/>
);
})}
</g>
</>
);
}