@@ -448,18 +448,32 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
448448 ) ;
449449 } , [ breakpointNode , layoutSeq , setNodes ] ) ;
450450
451+ const stateEvents = useRunStore ( ( s ) => s . stateEvents [ runId ] ) ;
452+
451453 // Highlight edges + nodes during execution
452454 // - Paused at breakpoint: edges INTO breakpoint node + edges to next_nodes
453- // - Running: edges OUT of completed node , target nodes of those edges
455+ // - Running: edges OUT of executing nodes , target nodes of those edges
454456 // - __start__: highlighted on first state event; __end__: highlighted when run completes
455457 useEffect ( ( ) => {
456458 const isPaused = ! ! breakpointNode ;
457- let matchIds = new Set < string > ( ) ; // Full React Flow node IDs of the "current" node
459+ let matchIds = new Set < string > ( ) ; // Full React Flow node IDs of the "current" node(s)
458460 const prevNodeIds = new Set < string > ( ) ; // Full RF IDs of the previous node (for edge filtering when paused)
459461 const nextNodeIds = new Set < string > ( ) ; // Full RF IDs of breakpoint next_nodes
460462 const activeTargetIds = new Set < string > ( ) ; // Full RF IDs for isActiveNode
461463 const nodeTypeById = new Map < string , string > ( ) ;
462464
465+ // Derive currently-executing nodes from the full event log (always consistent)
466+ const executingNodes = new Map < string , string | null > ( ) ; // nodeName → qualifiedNodeName
467+ if ( stateEvents ) {
468+ for ( const evt of stateEvents ) {
469+ if ( evt . phase === "started" ) {
470+ executingNodes . set ( evt . node_name , evt . qualified_node_name ?? null ) ;
471+ } else if ( evt . phase === "completed" ) {
472+ executingNodes . delete ( evt . node_name ) ;
473+ }
474+ }
475+ }
476+
463477 // 1) Build matchIds, nextNodeIds, node type map
464478 setNodes ( ( nds ) => {
465479 for ( const n of nds ) {
@@ -494,33 +508,38 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
494508 if ( activeNode ?. prev ) {
495509 findNodeIds ( activeNode . prev ) . forEach ( ( id ) => prevNodeIds . add ( id ) ) ;
496510 }
497- } else if ( activeNode ) {
498- // Try qualified name first (exact match via "subgraph:node" → "subgraph/node")
499- const qualifiedName = activeNode . qualifiedNodeName ;
500- if ( qualifiedName ) {
501- const qualifiedId = qualifiedName . replace ( / : / g, "/" ) ;
502- for ( const n of nds ) {
503- if ( n . id === qualifiedId ) {
504- matchIds . add ( n . id ) ;
505- }
511+ } else if ( executingNodes . size > 0 ) {
512+ // Build label → RF ID lookup once
513+ const labelToIds = new Map < string , Set < string > > ( ) ;
514+ for ( const n of nds ) {
515+ const label = n . data ?. label as string | undefined ;
516+ if ( ! label ) continue ;
517+ const plainId = n . id . includes ( "/" ) ? n . id . split ( "/" ) . pop ( ) ! : n . id ;
518+ for ( const key of [ plainId , label ] ) {
519+ let s = labelToIds . get ( key ) ;
520+ if ( ! s ) { s = new Set ( ) ; labelToIds . set ( key , s ) ; }
521+ s . add ( n . id ) ;
506522 }
507523 }
508- // Fallback: label/plainId matching
509- if ( matchIds . size === 0 ) {
510- const labelToIds = new Map < string , Set < string > > ( ) ;
511- for ( const n of nds ) {
512- const label = n . data ?. label as string | undefined ;
513- if ( ! label ) continue ;
514- const plainId = n . id . includes ( "/" ) ? n . id . split ( "/" ) . pop ( ) ! : n . id ;
515- for ( const key of [ plainId , label ] ) {
516- let s = labelToIds . get ( key ) ;
517- if ( ! s ) { s = new Set ( ) ; labelToIds . set ( key , s ) ; }
518- s . add ( n . id ) ;
524+
525+ for ( const [ nodeName , qualifiedNodeName ] of executingNodes ) {
526+ let found = false ;
527+ // Try qualified name first (exact match via "subgraph:node" → "subgraph/node")
528+ if ( qualifiedNodeName ) {
529+ const qualifiedId = qualifiedNodeName . replace ( / : / g , "/" ) ;
530+ for ( const n of nds ) {
531+ if ( n . id === qualifiedId ) {
532+ matchIds . add ( n . id ) ;
533+ found = true ;
534+ }
519535 }
520536 }
521- matchIds = labelToIds . get ( activeNode . current ) ?? new Set < string > ( ) ;
537+ // Fallback: label/plainId matching
538+ if ( ! found ) {
539+ const ids = labelToIds . get ( nodeName ) ;
540+ if ( ids ) ids . forEach ( ( id ) => matchIds . add ( id ) ) ;
541+ }
522542 }
523-
524543 }
525544
526545 return nds ;
@@ -542,7 +561,7 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
542561 isActive = intoBreakpoint
543562 || ( matchIds . has ( e . source ) && nextNodeIds . has ( e . target ) ) ;
544563 } else {
545- // Running: edges OUT of completed node
564+ // Running: edges OUT of executing nodes
546565 isActive = matchIds . has ( e . source ) ;
547566 // For __end__: also highlight edges INTO it
548567 if ( ! isActive && nodeTypeById . get ( e . target ) === "endNode" && matchIds . has ( e . target ) ) {
@@ -596,9 +615,7 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
596615 : n ;
597616 } ) ,
598617 ) ;
599- } , [ activeNode , breakpointNode , breakpointNextNodes , runStatus , layoutSeq , setNodes , setEdges ] ) ;
600-
601- const stateEvents = useRunStore ( ( s ) => s . stateEvents [ runId ] ) ;
618+ } , [ stateEvents , activeNode , breakpointNode , breakpointNextNodes , runStatus , layoutSeq , setNodes , setEdges ] ) ;
602619
603620 // Subscribe to cached graph reactively (populated async from run detail)
604621 const cachedGraph = useRunStore ( ( s ) => s . graphCache [ runId ] ) ;
0 commit comments