Grow a node with its ports, stop it flickering, draw what it reaches out to

- A node's height follows the ports on its busiest side. It is a function of
  the document, so `layoutGraph` reserves exactly what is drawn and nothing
  measured is fed back into the layout.
- The three status controls now sit in slots that are there whether the
  control is or not. A node running many times a second mounted and unmounted
  the stop button on every execution, resizing the card each time.
- A port bound to another flow's message is drawn as a label, naming the node
  at the far end and its type. Only the opposite direction was answered
  before. The scan behind both is now cached on the store's commit counter
  rather than reading every flow per request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
This commit is contained in:
2026-08-23 06:35:28 +02:00
co-authored by Claude Opus 5
parent 680c6053b9
commit 148d50f2bd
8 changed files with 454 additions and 131 deletions
+20 -2
View File
@@ -61,7 +61,7 @@ import { FIT_VIEW, FIT_VIEW_PANEL, FlowDock } from "./FlowDock"
import { FlowNode, type FlowNodeData } from "./FlowNode"
import { FlowPanel } from "./FlowPanel"
import { LiveEdge } from "./LiveEdge"
import { type Direction, layoutGraph } from "./layout"
import { type Direction, layoutGraph, nodeHeight } from "./layout"
import { NodePanel } from "./NodePanel"
import { RunDialog } from "./RunDialog"
import "./flow.css"
@@ -512,9 +512,27 @@ function FlowEditorInner({
...external.nodes.map((node) => node.id),
]
const shapeKey = `${direction}|${key}|${ids.join(",")}`
// How tall each node's ports make it, which `FlowNode` draws to the same
// number. A function of the document — the bindings key above already covers
// every port, so this changes exactly when the layout has to run again, and
// nothing measured is ever fed back into it.
const heights =
direction === "LR"
? new Map(
definitions.map((node) => [
node.id,
nodeHeight(
Math.max(
(node.requires ?? []).length,
(node.provides ?? []).length,
),
),
]),
)
: new Map<string, number>()
// biome-ignore lint/correctness/useExhaustiveDependencies: the shape key is the dependency; the arrays are rebuilt every render.
const positions = useMemo(
() => layoutGraph(ids, edges, direction),
() => layoutGraph(ids, edges, direction, heights),
[shapeKey, edges],
)