diff --git a/frontend/src/components/Flow/EndpointNode.tsx b/frontend/src/components/Flow/EndpointNode.tsx index dbdf376..6ab7247 100644 --- a/frontend/src/components/Flow/EndpointNode.tsx +++ b/frontend/src/components/Flow/EndpointNode.tsx @@ -29,7 +29,7 @@ function EndpointNodeComponent({ data, selected }: NodeProps) { // Padding keeps the text off the connector dot, which sits on the edge. "flex max-w-48 cursor-grab items-center gap-2 px-3 py-1", // Quiet at rest so the logic reads first; legible when reached for. - "text-muted-foreground transition-colors", + "text-muted-foreground/55 transition-colors", "hover:text-foreground active:cursor-grabbing", selected && "text-foreground", )} diff --git a/frontend/src/components/Flow/FlowEditor.tsx b/frontend/src/components/Flow/FlowEditor.tsx index 2476ae7..896d8b7 100644 --- a/frontend/src/components/Flow/FlowEditor.tsx +++ b/frontend/src/components/Flow/FlowEditor.tsx @@ -433,10 +433,13 @@ function FlowEditorInner({ ) // Edges follow from the name bindings, so they are derived, never stored. + // Kept off `external` deliberately: an endpoint's edges depend on which + // messages it touches, never on where it sits, so dragging one must not + // rebuild the edge array on every frame. // biome-ignore lint/correctness/useExhaustiveDependencies: the key is the dependency; the array identity changes on every drag frame. const edges = useMemo( () => [...deriveEdges(definitions, flowName), ...external.edges], - [key, flowName, external], + [key, flowName, detail.endpoints], ) const shownNodes = useMemo( @@ -740,13 +743,21 @@ function FlowEditorInner({ nodes={shownNodes} edges={edges} onNodesChange={onNodesChange} + onNodeDrag={(_event, _node, dragged) => { + // An endpoint's position is ours, not React Flow's, so it only + // follows the pointer if we move it every frame. + const endpoints = dragged.filter(isEndpointNode) + if (!endpoints.length) return + setMoved((current) => { + const next = { ...current } + for (const node of endpoints) next[node.id] = node.position + return next + }) + }} onNodeDragStop={(_event, _node, dragged) => { for (const node of dragged.filter(isEndpointNode)) { + // Written once at the end; every frame would be a write per pixel. rememberPlacement(flowName, node.id, node.position) - setMoved((current) => ({ - ...current, - [node.id]: node.position, - })) } const own = dragged.filter(isDocumentNode) if (own.length) commit(definitions, mergeDragged(canvasNodes, own))