@@ -95,6 +95,7 @@ import { useCanvasModeStore } from '@/stores/canvas-mode'
9595import { useChatStore } from '@/stores/chat/store'
9696import { defaultWorkflowExecutionState , useExecutionStore } from '@/stores/execution'
9797import { useSearchModalStore } from '@/stores/modals/search/store'
98+ import type { PendingConnect } from '@/stores/modals/search/types'
9899import { usePanelEditorStore } from '@/stores/panel'
99100import { useUndoRedoStore } from '@/stores/undo-redo'
100101import { useVariablesModalStore } from '@/stores/variables/modal'
@@ -209,6 +210,7 @@ interface AddBlockFromToolbarDetail {
209210 type ?: unknown
210211 enableTriggerMode ?: unknown
211212 presetOperation ?: unknown
213+ pendingConnect ?: PendingConnect
212214}
213215
214216/**
@@ -1779,9 +1781,55 @@ const WorkflowContent = React.memo(
17791781 * @param position - Drop position in ReactFlow coordinates.
17801782 */
17811783 const handleToolbarDrop = useCallback (
1782- ( data : { type : string ; enableTriggerMode ?: boolean } , position : { x : number ; y : number } ) => {
1784+ (
1785+ data : {
1786+ type : string
1787+ enableTriggerMode ?: boolean
1788+ presetOperation ?: string
1789+ forcedSource ?: { nodeId : string ; handleId : string }
1790+ } ,
1791+ position : { x : number ; y : number }
1792+ ) => {
17831793 if ( ! data . type || data . type === 'connectionBlock' ) return
17841794
1795+ const operationConfig = data . presetOperation
1796+ ? { operation : data . presetOperation }
1797+ : undefined
1798+
1799+ const { forcedSource } = data
1800+
1801+ /**
1802+ * Edge for the new block. With a `forcedSource` (a drag-release from a
1803+ * handle), wire from that exact handle — but only when it stays within the
1804+ * resolved container context, matching onConnect's boundary rules; a
1805+ * cross-boundary source yields no edge. Otherwise fall back to normal
1806+ * proximity auto-connect.
1807+ */
1808+ const resolveEdge = (
1809+ targetId : string ,
1810+ targetParentId : string | null ,
1811+ fallback : ( ) => Edge | undefined
1812+ ) : Edge | undefined => {
1813+ if ( ! forcedSource ) return fallback ( )
1814+
1815+ const isContainerStartHandle =
1816+ forcedSource . handleId === 'loop-start-source' ||
1817+ forcedSource . handleId === 'parallel-start-source'
1818+ if ( isContainerStartHandle ) {
1819+ // A container-start handle may only wire to a child of that container.
1820+ return forcedSource . nodeId === targetParentId
1821+ ? createEdgeObject ( forcedSource . nodeId , targetId , forcedSource . handleId )
1822+ : undefined
1823+ }
1824+
1825+ const sourceBlock = blocks [ forcedSource . nodeId ]
1826+ if ( ! sourceBlock ) return undefined
1827+ const sourceParentId = sourceBlock . data ?. parentId ?? null
1828+ return sourceParentId === targetParentId
1829+ ? createEdgeObject ( forcedSource . nodeId , targetId , forcedSource . handleId )
1830+ : undefined
1831+ }
1832+
17851833 try {
17861834 const containerInfo = isPointInLoopNode ( position )
17871835
@@ -1811,11 +1859,13 @@ const WorkflowContent = React.memo(
18111859 . filter ( ( b ) => b . data ?. parentId === containerInfo . loopId )
18121860 . map ( ( b ) => ( { id : b . id , type : b . type , position : b . position } ) )
18131861
1814- const autoConnectEdge = tryCreateAutoConnectEdge ( relativePosition , id , {
1815- targetParentId : containerInfo . loopId ,
1816- existingChildBlocks,
1817- containerId : containerInfo . loopId ,
1818- } )
1862+ const autoConnectEdge = resolveEdge ( id , containerInfo . loopId , ( ) =>
1863+ tryCreateAutoConnectEdge ( relativePosition , id , {
1864+ targetParentId : containerInfo . loopId ,
1865+ existingChildBlocks,
1866+ containerId : containerInfo . loopId ,
1867+ } )
1868+ )
18191869
18201870 addBlock (
18211871 id ,
@@ -1836,9 +1886,11 @@ const WorkflowContent = React.memo(
18361886
18371887 resizeLoopNodesWrapper ( )
18381888 } else {
1839- const autoConnectEdge = tryCreateAutoConnectEdge ( position , id , {
1840- targetParentId : null ,
1841- } )
1889+ const autoConnectEdge = resolveEdge ( id , null , ( ) =>
1890+ tryCreateAutoConnectEdge ( position , id , {
1891+ targetParentId : null ,
1892+ } )
1893+ )
18421894
18431895 addBlock (
18441896 id ,
@@ -1903,11 +1955,13 @@ const WorkflowContent = React.memo(
19031955 . filter ( ( b ) => b . data ?. parentId === containerInfo . loopId )
19041956 . map ( ( b ) => ( { id : b . id , type : b . type , position : b . position } ) )
19051957
1906- const autoConnectEdge = tryCreateAutoConnectEdge ( relativePosition , id , {
1907- targetParentId : containerInfo . loopId ,
1908- existingChildBlocks,
1909- containerId : containerInfo . loopId ,
1910- } )
1958+ const autoConnectEdge = resolveEdge ( id , containerInfo . loopId , ( ) =>
1959+ tryCreateAutoConnectEdge ( relativePosition , id , {
1960+ targetParentId : containerInfo . loopId ,
1961+ existingChildBlocks,
1962+ containerId : containerInfo . loopId ,
1963+ } )
1964+ )
19111965
19121966 // Add block with parent info AND autoConnectEdge (atomic operation)
19131967 addBlock (
@@ -1921,7 +1975,9 @@ const WorkflowContent = React.memo(
19211975 } ,
19221976 containerInfo . loopId ,
19231977 'parent' ,
1924- autoConnectEdge
1978+ autoConnectEdge ,
1979+ undefined ,
1980+ operationConfig
19251981 )
19261982
19271983 // Resize the container node to fit the new block
@@ -1931,9 +1987,11 @@ const WorkflowContent = React.memo(
19311987 // Centralized trigger constraints
19321988 if ( checkTriggerConstraints ( data . type ) ) return
19331989
1934- const autoConnectEdge = tryCreateAutoConnectEdge ( position , id , {
1935- targetParentId : null ,
1936- } )
1990+ const autoConnectEdge = resolveEdge ( id , null , ( ) =>
1991+ tryCreateAutoConnectEdge ( position , id , {
1992+ targetParentId : null ,
1993+ } )
1994+ )
19371995
19381996 // Regular canvas drop with auto-connect edge
19391997 // Use enableTriggerMode from drag data if present (when dragging from Triggers tab)
@@ -1947,7 +2005,8 @@ const WorkflowContent = React.memo(
19472005 undefined ,
19482006 undefined ,
19492007 autoConnectEdge ,
1950- enableTriggerMode
2008+ enableTriggerMode ,
2009+ operationConfig
19512010 )
19522011 }
19532012 } catch ( err ) {
@@ -1961,6 +2020,7 @@ const WorkflowContent = React.memo(
19612020 addBlock ,
19622021 tryCreateAutoConnectEdge ,
19632022 checkTriggerConstraints ,
2023+ createEdgeObject ,
19642024 ]
19652025 )
19662026
@@ -1972,11 +2032,30 @@ const WorkflowContent = React.memo(
19722032 return
19732033 }
19742034
1975- const { type, enableTriggerMode, presetOperation } = event . detail
2035+ const { type, enableTriggerMode, presetOperation, pendingConnect } = event . detail
19762036
19772037 if ( typeof type !== 'string' || ! type ) return
19782038 if ( type === 'connectionBlock' ) return
19792039
2040+ // Complete an edge drag-release: only a genuine palette selection carries
2041+ // `pendingConnect` (other add-block dispatchers — toolbar, sidebar, command
2042+ // list — don't), so its presence is the signal. Delegating to
2043+ // handleToolbarDrop with the drag source gives container-aware placement AND
2044+ // an edge from the released handle that respects container boundaries.
2045+ if ( pendingConnect ) {
2046+ // screenToFlowPosition subtracts the pane rect internally — pass raw client coords.
2047+ handleToolbarDrop (
2048+ {
2049+ type,
2050+ enableTriggerMode : enableTriggerMode === true ,
2051+ presetOperation : typeof presetOperation === 'string' ? presetOperation : undefined ,
2052+ forcedSource : pendingConnect . source ,
2053+ } ,
2054+ screenToFlowPosition ( { x : pendingConnect . screenX , y : pendingConnect . screenY } )
2055+ )
2056+ return
2057+ }
2058+
19802059 const basePosition = getViewportCenter ( )
19812060
19822061 if ( type === 'loop' || type === 'parallel' ) {
@@ -2054,6 +2133,8 @@ const WorkflowContent = React.memo(
20542133 effectivePermissions . canEdit ,
20552134 checkTriggerConstraints ,
20562135 tryCreateAutoConnectEdge ,
2136+ screenToFlowPosition ,
2137+ handleToolbarDrop ,
20572138 ] )
20582139
20592140 /**
@@ -3132,6 +3213,17 @@ const WorkflowContent = React.memo(
31323213 target : targetNode . id ,
31333214 targetHandle : 'target' ,
31343215 } )
3216+ } else if ( ! targetNode ) {
3217+ // Released on empty canvas: open the command palette with the drag origin
3218+ // + drop point, so the chosen block lands here wired from this handle.
3219+ useSearchModalStore . getState ( ) . open ( {
3220+ sections : [ 'blocks' , 'tools' , 'toolOperations' ] ,
3221+ pendingConnect : {
3222+ source : { nodeId : source . nodeId , handleId : source . handleId } ,
3223+ screenX : clientPos . clientX ,
3224+ screenY : clientPos . clientY ,
3225+ } ,
3226+ } )
31353227 }
31363228
31373229 connectionSourceRef . current = null
0 commit comments