@@ -23,9 +23,11 @@ const SUBAGENT_HANDLE = 'subagent';
2323const SUBAGENT_TARGET_HANDLE = 'subagent-target' ;
2424const IF_PORT_BASE_TOP = 45 ;
2525const IF_PORT_STEP = 30 ;
26- const IF_COLLAPSED_MULTI_CONDITION_PORT_TOP = 18 ;
27- const IF_COLLAPSED_MULTI_FALLBACK_PORT_TOP = 45 ;
2826const SUBAGENT_PORT_MIN_TOP = 42 ;
27+ const DEFAULT_HEADER_CENTER_Y = 24 ;
28+ const DEFAULT_SECONDARY_CENTER_Y = 81 ;
29+ const PORT_RADIUS = 6 ;
30+ const AGGREGATE_PORT_RADIUS = 8 ;
2931const PREVIOUS_OUTPUT_TEMPLATE = '{{PREVIOUS_OUTPUT}}' ;
3032const GENERIC_AGENT_SPINNER_KEY = '__generic_agent_spinner__' ;
3133const IF_CONDITION_OPERATORS = [
@@ -650,7 +652,7 @@ export class WorkflowEditor {
650652 }
651653 return {
652654 x : targetNode . x ,
653- y : targetNode . y + 24
655+ y : targetNode . y + this . getNodeHeaderCenterYOffset ( targetNode )
654656 } ;
655657 }
656658
@@ -852,7 +854,7 @@ export class WorkflowEditor {
852854
853855 getIfConditionPortTop ( node : EditorNode , index : number ) : number {
854856 if ( node . data ?. collapsed ) {
855- return this . getIfPortTop ( index ) ;
857+ return this . getNodeHeaderPortTop ( node ) ;
856858 }
857859
858860 const nodeEl = document . getElementById ( node . id ) ;
@@ -872,7 +874,7 @@ export class WorkflowEditor {
872874 getIfFallbackPortTop ( node : EditorNode ) : number {
873875 const conditions = this . getIfConditions ( node ) ;
874876 if ( node . data ?. collapsed ) {
875- return this . getIfPortTop ( conditions . length ) ;
877+ return this . getNodeSecondaryPortTop ( node ) ;
876878 }
877879
878880 const nodeEl = document . getElementById ( node . id ) ;
@@ -959,11 +961,11 @@ export class WorkflowEditor {
959961 if ( node . type === 'if' ) {
960962 if ( this . shouldAggregateCollapsedIfPorts ( node ) ) {
961963 if ( sourceHandle === IF_FALLBACK_HANDLE ) {
962- return IF_COLLAPSED_MULTI_FALLBACK_PORT_TOP + 6 ;
964+ return this . getNodeSecondaryCenterYOffset ( node ) ;
963965 }
964966 const conditionIndex = this . getIfConditionIndexFromHandle ( sourceHandle ) ;
965967 if ( conditionIndex !== null ) {
966- return IF_COLLAPSED_MULTI_CONDITION_PORT_TOP + 6 ;
968+ return this . getNodeHeaderCenterYOffset ( node ) ;
967969 }
968970 }
969971 if ( sourceHandle === IF_FALLBACK_HANDLE ) {
@@ -975,9 +977,35 @@ export class WorkflowEditor {
975977 }
976978 }
977979
978- if ( sourceHandle === 'approve' ) return 51 ;
979- if ( sourceHandle === 'reject' ) return 81 ;
980- return 24 ;
980+ if ( sourceHandle === 'approve' ) return this . getNodeHeaderCenterYOffset ( node ) ;
981+ if ( sourceHandle === 'reject' ) return this . getNodeSecondaryCenterYOffset ( node ) ;
982+ return this . getNodeHeaderCenterYOffset ( node ) ;
983+ }
984+
985+ getNodeHeaderCenterYOffset ( node : EditorNode ) : number {
986+ const nodeEl = document . getElementById ( node . id ) ;
987+ const headerEl = nodeEl ?. querySelector ( '.node-header' ) ;
988+ if ( ! ( headerEl instanceof HTMLElement ) ) return DEFAULT_HEADER_CENTER_Y ;
989+ return Math . round ( headerEl . offsetTop + ( headerEl . offsetHeight / 2 ) ) ;
990+ }
991+
992+ getNodeHeaderPortTop ( node : EditorNode ) : number {
993+ return this . getNodeHeaderCenterYOffset ( node ) - PORT_RADIUS ;
994+ }
995+
996+ getNodeSecondaryCenterYOffset ( node : EditorNode ) : number {
997+ const nodeEl = document . getElementById ( node . id ) ;
998+ const headerEl = nodeEl ?. querySelector ( '.node-header' ) ;
999+ if ( ! ( nodeEl instanceof HTMLElement ) || ! ( headerEl instanceof HTMLElement ) ) {
1000+ return DEFAULT_SECONDARY_CENTER_Y ;
1001+ }
1002+ const bodyTop = headerEl . offsetTop + headerEl . offsetHeight ;
1003+ const bodyHeight = Math . max ( nodeEl . offsetHeight - bodyTop , PORT_RADIUS * 2 ) ;
1004+ return Math . round ( bodyTop + ( bodyHeight / 2 ) ) ;
1005+ }
1006+
1007+ getNodeSecondaryPortTop ( node : EditorNode ) : number {
1008+ return this . getNodeSecondaryCenterYOffset ( node ) - PORT_RADIUS ;
9811009 }
9821010
9831011 setWorkflowState ( state : WorkflowState ) : void {
@@ -1003,6 +1031,15 @@ export class WorkflowEditor {
10031031 }
10041032 }
10051033
1034+ setCancelRunButtonHint ( reason : string | null ) : void {
1035+ if ( ! this . cancelRunButton ) return ;
1036+ if ( reason ) {
1037+ this . cancelRunButton . setAttribute ( 'data-tooltip' , reason ) ;
1038+ } else {
1039+ this . cancelRunButton . removeAttribute ( 'data-tooltip' ) ;
1040+ }
1041+ }
1042+
10061043 setCanvasValidationMessage ( message : string | null ) : void {
10071044 if ( this . canvasValidationTimeout !== null ) {
10081045 clearTimeout ( this . canvasValidationTimeout ) ;
@@ -1138,6 +1175,7 @@ export class WorkflowEditor {
11381175 const showCancel = this . workflowState === 'running' ;
11391176 this . cancelRunButton . style . display = showCancel ? 'inline-flex' : 'none' ;
11401177 this . cancelRunButton . disabled = ! showCancel ;
1178+ this . setCancelRunButtonHint ( showCancel ? 'Cancel workflow' : null ) ;
11411179 }
11421180
11431181 if ( this . clearButton ) {
@@ -2209,14 +2247,14 @@ export class WorkflowEditor {
22092247
22102248 } else if ( node . type === 'approval' ) {
22112249 container . appendChild ( buildLabel ( 'Approval Message' ) ) ;
2212- const pInput = document . createElement ( 'input ' ) ;
2213- pInput . type = 'text ' ;
2214- pInput . className = 'input' ;
2250+ const pInput = document . createElement ( 'textarea ' ) ;
2251+ pInput . className = 'input textarea-input ' ;
2252+ pInput . rows = 4 ;
22152253 pInput . value = data . prompt || '' ;
22162254 pInput . placeholder = 'Message shown to user when approval is required' ;
22172255 pInput . addEventListener ( 'input' , ( e : any ) => {
22182256 data . prompt = e . target . value ;
2219- this . scheduleSave ( ) ;
2257+ this . updatePreview ( node ) ;
22202258 } ) ;
22212259 container . appendChild ( pInput ) ;
22222260
@@ -2269,52 +2307,49 @@ export class WorkflowEditor {
22692307 node . id ,
22702308 SUBAGENT_TARGET_HANDLE ,
22712309 'port-subagent-target' ,
2272- 'Subagent target '
2310+ 'Set as subagent '
22732311 )
22742312 ) ;
22752313 }
22762314
22772315 if ( node . type !== 'start' ) {
2278- const portIn = this . createPort ( node . id , 'input' , 'port-in' ) ;
2316+ const inputTooltip = node . type === 'end' ? 'End input' : 'Input' ;
2317+ const portIn = this . createPort ( node . id , 'input' , 'port-in' , inputTooltip , this . getNodeHeaderPortTop ( node ) ) ;
22792318 el . appendChild ( portIn ) ;
22802319 }
22812320
22822321 if ( node . type !== 'end' ) {
22832322 if ( node . type === 'if' ) {
22842323 const conditions = this . getIfConditions ( node ) ;
22852324 if ( this . shouldAggregateCollapsedIfPorts ( node ) ) {
2286- const title = `${ conditions . length } condition branches (expand to wire specific branches)` ;
22872325 const aggregateConditionPort = this . createPort (
22882326 node . id ,
22892327 this . getIfConditionHandle ( 0 ) ,
22902328 'port-out port-condition port-condition-aggregate' ,
2291- title ,
2292- IF_COLLAPSED_MULTI_CONDITION_PORT_TOP ,
2329+ 'Expand to connect' ,
2330+ this . getNodeHeaderCenterYOffset ( node ) - AGGREGATE_PORT_RADIUS ,
22932331 false
22942332 ) ;
22952333 aggregateConditionPort . textContent = String ( conditions . length ) ;
2296- aggregateConditionPort . setAttribute ( 'aria-label' , ` ${ conditions . length } conditions` ) ;
2334+ aggregateConditionPort . setAttribute ( 'aria-label' , 'Expand to connect' ) ;
22972335 el . appendChild ( aggregateConditionPort ) ;
22982336 el . appendChild (
22992337 this . createPort (
23002338 node . id ,
23012339 IF_FALLBACK_HANDLE ,
23022340 'port-out port-condition-fallback' ,
2303- 'False fallback ' ,
2304- IF_COLLAPSED_MULTI_FALLBACK_PORT_TOP
2341+ 'Fallback path ' ,
2342+ this . getNodeSecondaryPortTop ( node )
23052343 )
23062344 ) ;
23072345 } else {
2308- conditions . forEach ( ( condition : any , index : any ) => {
2309- const operatorLabel = condition . operator === 'contains' ? 'Contains' : 'Equal' ;
2310- const conditionValue = condition . value || '' ;
2311- const title = `Condition ${ index + 1 } : ${ operatorLabel } "${ conditionValue } "` ;
2346+ conditions . forEach ( ( _condition : any , index : any ) => {
23122347 el . appendChild (
23132348 this . createPort (
23142349 node . id ,
23152350 this . getIfConditionHandle ( index ) ,
23162351 'port-out port-condition' ,
2317- title ,
2352+ `Condition ${ index + 1 } ` ,
23182353 this . getIfConditionPortTop ( node , index )
23192354 )
23202355 ) ;
@@ -2324,28 +2359,45 @@ export class WorkflowEditor {
23242359 node . id ,
23252360 IF_FALLBACK_HANDLE ,
23262361 'port-out port-condition-fallback' ,
2327- 'False fallback ' ,
2362+ 'Fallback path ' ,
23282363 this . getIfFallbackPortTop ( node )
23292364 )
23302365 ) ;
23312366 }
23322367 } else if ( node . type === 'agent' ) {
2333- el . appendChild ( this . createPort ( node . id , 'output' , 'port-out' ) ) ;
2368+ el . appendChild ( this . createPort ( node . id , 'output' , 'port-out' , 'Output' , this . getNodeHeaderPortTop ( node ) ) ) ;
23342369 if ( node . data ?. tools ?. subagents ) {
23352370 el . appendChild (
23362371 this . createPort (
23372372 node . id ,
23382373 SUBAGENT_HANDLE ,
23392374 'port-subagent' ,
2340- 'Subagent '
2375+ 'Add subagent '
23412376 )
23422377 ) ;
23432378 }
23442379 } else if ( node . type === 'approval' ) {
2345- el . appendChild ( this . createPort ( node . id , 'approve' , 'port-out port-true' , 'Approve' ) ) ;
2346- el . appendChild ( this . createPort ( node . id , 'reject' , 'port-out port-false' , 'Reject' ) ) ;
2380+ el . appendChild (
2381+ this . createPort (
2382+ node . id ,
2383+ 'approve' ,
2384+ 'port-out port-true' ,
2385+ 'Approve path' ,
2386+ this . getNodeHeaderPortTop ( node )
2387+ )
2388+ ) ;
2389+ el . appendChild (
2390+ this . createPort (
2391+ node . id ,
2392+ 'reject' ,
2393+ 'port-out port-false' ,
2394+ 'Reject path' ,
2395+ this . getNodeSecondaryPortTop ( node )
2396+ )
2397+ ) ;
23472398 } else {
2348- el . appendChild ( this . createPort ( node . id , 'output' , 'port-out' ) ) ;
2399+ const outputTooltip = node . type === 'start' ? 'Next step' : 'Output' ;
2400+ el . appendChild ( this . createPort ( node . id , 'output' , 'port-out' , outputTooltip , this . getNodeHeaderPortTop ( node ) ) ) ;
23492401 }
23502402 }
23512403 }
@@ -2360,7 +2412,11 @@ export class WorkflowEditor {
23602412 ) : HTMLDivElement {
23612413 const port = document . createElement ( 'div' ) ;
23622414 port . className = `port ${ className } ${ connectable ? '' : ' port-disabled' } ` ;
2363- if ( title ) port . title = title ;
2415+ if ( title ) {
2416+ port . title = title ;
2417+ port . setAttribute ( 'data-tooltip' , title ) ;
2418+ port . setAttribute ( 'aria-label' , title ) ;
2419+ }
23642420 if ( typeof top === 'number' ) {
23652421 port . style . top = `${ top } px` ;
23662422 }
0 commit comments