diff --git a/ui/src/components/ProcessDiagram/CompoundNode.tsx b/ui/src/components/ProcessDiagram/CompoundNode.tsx
index 7c065bac..aa7e0a30 100644
--- a/ui/src/components/ProcessDiagram/CompoundNode.tsx
+++ b/ui/src/components/ProcessDiagram/CompoundNode.tsx
@@ -1,7 +1,7 @@
import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams';
import type { NodeConfig } from './types';
import type { NodeExecutionState, IterationInfo } from '../ExecutionDiagram/types';
-import { colorForType, isCompoundType } from './node-colors';
+import { colorForType, isCompoundType, iconForType, type IconElement } from './node-colors';
import { DiagramNode } from './DiagramNode';
import { DiagramEdge } from './DiagramEdge';
import styles from './ProcessDiagram.module.css';
@@ -118,7 +118,15 @@ export function CompoundNode({
- {/* Header label */}
+ {/* Header icon (left-aligned) */}
+
+ {iconForType(node.type).map((el: IconElement, i: number) =>
+ 'd' in el
+ ?
+ :
+ )}
+
+ {/* Header label (centered) */}
- {/* Icon */}
-
- {icon}
-
+ {/* Icon (lucide 24×24 scaled to 14px) */}
+
+ {iconElements.map((el: IconElement, i: number) =>
+ 'd' in el
+ ?
+ :
+ )}
+
{/* Type name + detail (clipped to available width) */}
diff --git a/ui/src/components/ProcessDiagram/node-colors.ts b/ui/src/components/ProcessDiagram/node-colors.ts
index 614a6b67..f14c1db6 100644
--- a/ui/src/components/ProcessDiagram/node-colors.ts
+++ b/ui/src/components/ProcessDiagram/node-colors.ts
@@ -91,17 +91,241 @@ export function isCompletionCompoundType(type: string | undefined): boolean {
return !!type && COMPLETION_COMPOUND_TYPES.has(type);
}
-/** Icon character for a node type */
-export function iconForType(type: string | undefined): string {
- if (!type) return '\u2699'; // gear
- const t = type.toUpperCase();
- if (t === 'ENDPOINT') return '\u25B6'; // play
- if (t === 'TO' || t === 'TO_DYNAMIC' || t === 'DIRECT' || t === 'SEDA') return '\u25A0'; // square
- if (t.startsWith('EIP_CHOICE') || t === 'EIP_WHEN' || t === 'EIP_OTHERWISE') return '\u25C6'; // diamond
- if (t === 'ON_COMPLETION') return '\u2714'; // checkmark
- if (t === 'ON_EXCEPTION' || t === 'ERROR_HANDLER' || t.startsWith('TRY') || t.startsWith('DO_')) return '\u26A0'; // warning
- if (t === 'EIP_SPLIT' || t === 'EIP_MULTICAST') return '\u2442'; // fork
- if (t === 'EIP_LOOP') return '\u21BA'; // loop arrow
- if (t === 'EIP_WIRE_TAP' || t === 'EIP_ENRICH' || t === 'EIP_POLL_ENRICH') return '\u2197'; // arrow
- return '\u2699'; // gear
+/**
+ * Lucide-based icon definitions for node types.
+ * Each icon is an array of SVG element descriptors (path d-strings or circles).
+ */
+export type IconElement = { d: string } | { cx: number; cy: number; r: number };
+
+// Lucide icon path data (24×24 viewBox, stroke-based)
+const ICONS: Record = {
+ // Play — ENDPOINT
+ play: [{ d: 'M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z' }],
+ // Send — TO
+ send: [
+ { d: 'M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z' },
+ { d: 'm21.854 2.147-10.94 10.939' },
+ ],
+ // Shuffle — TO_DYNAMIC
+ shuffle: [
+ { d: 'm18 14 4 4-4 4' },
+ { d: 'm18 2 4 4-4 4' },
+ { d: 'M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22' },
+ { d: 'M2 6h1.972a4 4 0 0 1 3.6 2.2' },
+ { d: 'M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45' },
+ ],
+ // Zap — DIRECT
+ zap: [{ d: 'M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z' }],
+ // Layers — SEDA
+ layers: [
+ { d: 'M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z' },
+ { d: 'M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12' },
+ { d: 'M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17' },
+ ],
+ // Cog — PROCESSOR (default)
+ cog: [
+ { d: 'M11 10.27 7 3.34' }, { d: 'm11 13.73-4 6.93' },
+ { d: 'M12 22v-2' }, { d: 'M12 2v2' }, { d: 'M14 12h8' },
+ { d: 'm17 20.66-1-1.73' }, { d: 'm17 3.34-1 1.73' },
+ { d: 'M2 12h2' }, { d: 'm20.66 17-1.73-1' }, { d: 'm20.66 7-1.73 1' },
+ { d: 'm3.34 17 1.73-1' }, { d: 'm3.34 7 1.73 1' },
+ { cx: 12, cy: 12, r: 3 },
+ ],
+ // Coffee — BEAN
+ coffee: [
+ { d: 'M10 2v2' }, { d: 'M14 2v2' }, { d: 'M6 2v2' },
+ { d: 'M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1' },
+ ],
+ // FileText — LOG
+ fileText: [
+ { d: 'M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z' },
+ { d: 'M14 2v5a1 1 0 0 0 1 1h5' },
+ { d: 'M10 9H8' }, { d: 'M16 13H8' }, { d: 'M16 17H8' },
+ ],
+ // Tag — SET_HEADER
+ tag: [{ d: 'M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z' }, { cx: 7.5, cy: 7.5, r: 1 }],
+ // PenLine — SET_BODY
+ penLine: [
+ { d: 'M13 21h8' },
+ { d: 'M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z' },
+ ],
+ // RefreshCw — TRANSFORM
+ refreshCw: [
+ { d: 'M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8' },
+ { d: 'M21 3v5h-5' },
+ { d: 'M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16' },
+ { d: 'M8 16H3v5' },
+ ],
+ // Package — MARSHAL
+ package: [
+ { d: 'M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z' },
+ { d: 'M12 22V12' }, { d: 'm7.5 4.27 9 5.15' },
+ ],
+ // PackageOpen — UNMARSHAL
+ packageOpen: [
+ { d: 'M12 22v-9' },
+ { d: 'M15.17 2.21a1.67 1.67 0 0 1 1.63 0L21 4.57a1.93 1.93 0 0 1 0 3.36L8.82 14.79a1.655 1.655 0 0 1-1.64 0L3 12.43a1.93 1.93 0 0 1 0-3.36z' },
+ { d: 'M20 13v3.87a2.06 2.06 0 0 1-1.11 1.83l-6 3.08a1.93 1.93 0 0 1-1.78 0l-6-3.08A2.06 2.06 0 0 1 4 16.87V13' },
+ { d: 'M21 12.43a1.93 1.93 0 0 0 0-3.36L8.83 2.2a1.64 1.64 0 0 0-1.63 0L3 4.57a1.93 1.93 0 0 0 0 3.36l12.18 6.86a1.636 1.636 0 0 0 1.63 0z' },
+ ],
+ // GitBranch — EIP_CHOICE
+ gitBranch: [{ d: 'M15 6a9 9 0 0 0-9 9V3' }, { cx: 18, cy: 6, r: 3 }, { cx: 6, cy: 18, r: 3 }],
+ // CircleAlert — EIP_WHEN (condition)
+ circleAlert: [{ cx: 12, cy: 12, r: 10 }, { d: 'M12 8v4' }, { d: 'M12 16h.01' }],
+ // CircleDot — EIP_OTHERWISE (default)
+ circleDot: [{ cx: 12, cy: 12, r: 10 }, { cx: 12, cy: 12, r: 1 }],
+ // Split — EIP_SPLIT
+ split: [
+ { d: 'M16 3h5v5' }, { d: 'M8 3H3v5' },
+ { d: 'M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3' },
+ { d: 'm15 9 6-6' },
+ ],
+ // Share2 — EIP_MULTICAST
+ share2: [
+ { cx: 18, cy: 5, r: 3 }, { cx: 6, cy: 12, r: 3 }, { cx: 18, cy: 19, r: 3 },
+ { d: 'M8.59 13.51 15.42 17.49' }, { d: 'M15.41 6.51 8.59 10.49' },
+ ],
+ // Repeat — EIP_LOOP
+ repeat: [
+ { d: 'm17 2 4 4-4 4' },
+ { d: 'M3 11v-1a4 4 0 0 1 4-4h14' },
+ { d: 'm7 22-4-4 4-4' },
+ { d: 'M21 13v1a4 4 0 0 1-4 4H3' },
+ ],
+ // Merge — EIP_AGGREGATE
+ merge: [{ d: 'm8 6 4-4 4 4' }, { d: 'M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22' }, { d: 'm20 22-5-5' }],
+ // Funnel — EIP_FILTER
+ funnel: [{ d: 'M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z' }],
+ // Users — EIP_RECIPIENT_LIST
+ users: [
+ { d: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2' },
+ { cx: 9, cy: 7, r: 4 },
+ { d: 'M16 3.128a4 4 0 0 1 0 7.744' },
+ { d: 'M22 21v-2a4 4 0 0 0-3-3.87' },
+ ],
+ // Navigation — EIP_ROUTING_SLIP
+ navigation: [{ d: 'M3,11 L22,2 L13,21 L11,13Z' }],
+ // Waypoints — EIP_DYNAMIC_ROUTER
+ waypoints: [
+ { d: 'm10.586 5.414-5.172 5.172' }, { d: 'm18.586 13.414-5.172 5.172' },
+ { d: 'M6 12h12' },
+ { cx: 12, cy: 20, r: 2 }, { cx: 12, cy: 4, r: 2 },
+ { cx: 20, cy: 12, r: 2 }, { cx: 4, cy: 12, r: 2 },
+ ],
+ // Scale — EIP_LOAD_BALANCE
+ scale: [
+ { d: 'M12 3v18' },
+ { d: 'm19 8 3 8a5 5 0 0 1-6 0zV7' },
+ { d: 'M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1' },
+ { d: 'm5 8 3 8a5 5 0 0 1-6 0zV7' },
+ { d: 'M7 21h10' },
+ ],
+ // Gauge — EIP_THROTTLE
+ gauge: [{ d: 'm12 14 4-4' }, { d: 'M3.34 19a10 10 0 1 1 17.32 0' }],
+ // Clock — EIP_DELAY
+ clock: [{ cx: 12, cy: 12, r: 10 }, { d: 'M12 6v6l4 2' }],
+ // Hash — EIP_IDEMPOTENT_CONSUMER
+ hash: [{ d: 'M4 9L20 9' }, { d: 'M4 15L20 15' }, { d: 'M10 3L8 21' }, { d: 'M16 3L14 21' }],
+ // ZapOff — EIP_CIRCUIT_BREAKER
+ zapOff: [
+ { d: 'M10.513 4.856 13.12 2.17a.5.5 0 0 1 .86.46l-1.377 4.317' },
+ { d: 'M15.656 10H20a1 1 0 0 1 .78 1.63l-1.72 1.773' },
+ { d: 'M16.273 16.273 10.88 21.83a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14H4a1 1 0 0 1-.78-1.63l4.507-4.643' },
+ { d: 'm2 2 20 20' },
+ ],
+ // Workflow — EIP_PIPELINE
+ workflow: [
+ { d: 'M5,3h4a2,2 0 0 1 2,2v4a2,2 0 0 1-2,2h-4a2,2 0 0 1-2,-2v-4a2,2 0 0 1 2,-2Z' },
+ { d: 'M7 11v4a2 2 0 0 0 2 2h4' },
+ { d: 'M15,13h4a2,2 0 0 1 2,2v4a2,2 0 0 1-2,2h-4a2,2 0 0 1-2,-2v-4a2,2 0 0 1 2,-2Z' },
+ ],
+ // ShieldAlert — ERROR_HANDLER
+ shieldAlert: [
+ { d: 'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z' },
+ { d: 'M12 8v4' }, { d: 'M12 16h.01' },
+ ],
+ // OctagonAlert — ON_EXCEPTION
+ octagonAlert: [
+ { d: 'M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z' },
+ { d: 'M12 8v4' }, { d: 'M12 16h.01' },
+ ],
+ // Shield — DO_TRY
+ shield: [{ d: 'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z' }],
+ // Hand — DO_CATCH
+ hand: [
+ { d: 'M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2' },
+ { d: 'M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2' },
+ { d: 'M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8' },
+ { d: 'M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15' },
+ ],
+ // ListChecks — DO_FINALLY
+ listChecks: [
+ { d: 'M13 5h8' }, { d: 'M13 12h8' }, { d: 'M13 19h8' },
+ { d: 'm3 17 2 2 4-4' }, { d: 'm3 7 2 2 4-4' },
+ ],
+ // Eye — EIP_WIRE_TAP
+ eye: [
+ { d: 'M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0' },
+ { cx: 12, cy: 12, r: 3 },
+ ],
+ // CirclePlus — EIP_ENRICH
+ circlePlus: [{ cx: 12, cy: 12, r: 10 }, { d: 'M8 12h8' }, { d: 'M12 8v8' }],
+ // CircleArrowDown — EIP_POLL_ENRICH
+ circleArrowDown: [{ cx: 12, cy: 12, r: 10 }, { d: 'M12 8v8' }, { d: 'm8 12 4 4 4-4' }],
+ // CircleCheck — ON_COMPLETION
+ circleCheck: [{ cx: 12, cy: 12, r: 10 }, { d: 'm9 12 2 2 4-4' }],
+};
+
+const ICON_MAP: Record = {
+ ENDPOINT: ICONS.play,
+ TO: ICONS.send,
+ TO_DYNAMIC: ICONS.shuffle,
+ DIRECT: ICONS.zap,
+ SEDA: ICONS.layers,
+
+ PROCESSOR: ICONS.cog,
+ BEAN: ICONS.coffee,
+ LOG: ICONS.fileText,
+ SET_HEADER: ICONS.tag,
+ SET_BODY: ICONS.penLine,
+ TRANSFORM: ICONS.refreshCw,
+ MARSHAL: ICONS.package,
+ UNMARSHAL: ICONS.packageOpen,
+
+ EIP_CHOICE: ICONS.gitBranch,
+ EIP_WHEN: ICONS.circleAlert,
+ EIP_OTHERWISE: ICONS.circleDot,
+ EIP_SPLIT: ICONS.split,
+ EIP_MULTICAST: ICONS.share2,
+ EIP_LOOP: ICONS.repeat,
+ EIP_AGGREGATE: ICONS.merge,
+ EIP_FILTER: ICONS.funnel,
+ EIP_RECIPIENT_LIST: ICONS.users,
+ EIP_ROUTING_SLIP: ICONS.navigation,
+ EIP_DYNAMIC_ROUTER: ICONS.waypoints,
+ EIP_LOAD_BALANCE: ICONS.scale,
+ EIP_THROTTLE: ICONS.gauge,
+ EIP_DELAY: ICONS.clock,
+ EIP_IDEMPOTENT_CONSUMER: ICONS.hash,
+ EIP_CIRCUIT_BREAKER: ICONS.zapOff,
+ EIP_PIPELINE: ICONS.workflow,
+
+ ERROR_HANDLER: ICONS.shieldAlert,
+ ON_EXCEPTION: ICONS.octagonAlert,
+ TRY_CATCH: ICONS.shield,
+ DO_TRY: ICONS.shield,
+ DO_CATCH: ICONS.hand,
+ DO_FINALLY: ICONS.listChecks,
+
+ EIP_WIRE_TAP: ICONS.eye,
+ EIP_ENRICH: ICONS.circlePlus,
+ EIP_POLL_ENRICH: ICONS.circleArrowDown,
+
+ ON_COMPLETION: ICONS.circleCheck,
+};
+
+/** Lucide SVG icon elements for a node type (24×24 viewBox, stroke-based) */
+export function iconForType(type: string | undefined): IconElement[] {
+ if (!type) return ICONS.cog;
+ return ICON_MAP[type.toUpperCase()] ?? ICONS.cog;
}