From b1e1b4d501325fd78e9920782564ba74b8fb090f Mon Sep 17 00:00:00 2001 From: stroblme Date: Sun, 16 Aug 2026 16:14:37 +0200 Subject: [PATCH] Make an endpoint follow the pointer, and recede further at rest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things from using it. The label only jumped to its new place on release: its position comes from a memo of ours rather than React Flow's own store, so nothing moved it until the drag had already finished. It is now updated on every drag frame, with the write to storage still happening once at the end rather than once per pixel. The edges deliberately do not depend on that position — an endpoint's edges follow from which messages it touches, never from where it sits — so dragging one no longer rebuilds the edge array on every frame. And they sit further back at rest, so the nodes read first. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY --- frontend/src/components/Flow/EndpointNode.tsx | 2 +- frontend/src/components/Flow/FlowEditor.tsx | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) 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))