Not a bug — a discoverability gap. connect-nodes-wizard made wiring teachable, but the only way to reach it was from inside a node’s editor: open a node, find “Connect Nodes”, then pick the other node from a list you can’t see on the canvas. Users reason about their swarm spatially on ClientScreen — the nodes and their source/input arcs already live there — yet there was no way to associate two nodes you can both see.
The connect flow’s entry point was bound to the editor surface. The relationship semantics (ConnectNodesWiring.applyConnect) and the relationship UI (RelationshipStep) were already general and reusable, but nothing exercised them from the canvas. The gap was an entry surface, not new wiring behavior.
A canvas “edit mode” that funnels into the existing relationship prompt — composeApp-only, no new wiring logic:
ScreenCore: editMode flag + toggleEditMode(), and a dedicated two-tap selection (editFirstSelection / editSecondSelection, editSelect / clearEditSelection / cancelEditPair) kept separate from the menu’s selectedNodeId so the two selection models never collide. editSelect deliberately does not call nodeManager.selectNode, so the per-node menu never opens in edit mode.NodeItem.kt: while edit mode is active, tap / long-press / right-click all route to editSelect (suppressing execute / DataPoint toggle / ON_CLICK / context menu). The gesture handlers read screenCore.editMode.value live — their pointerInput isn’t keyed on edit mode, so a collected copy would go stale mid-session. A token-based primary ring marks selected nodes.ClientScreen.kt: a second top-right toggle (EditModeToggle, stacked under ViewModeToggle); empty-canvas tap clears the edit selection instead of reset() while in edit mode; the avatar speech-bubble hosts EditModeBubble — a teaching tip until two nodes are picked, then RelationshipStep(current = first-tapped, selected = second-tapped) verbatim. Confirm persists via the wizard’s own applyConnect + nodeManager.submit and clears the selection while staying in edit mode for the next pair.applyConnect + the internal RelationshipStep), a new entry surface should host those verbatim — resist re-deriving the write logic. The asymmetry that “the observer stores the connection” only needs to be taught in one place.pointerInput keyed on stable inputs (node id), so any mode they branch on must be read from the source-of-truth StateFlow.value at event time, not captured from a collectAsState() — otherwise the branch silently uses the value from when the pointer input was first set up.selectedNodeId would have entangled the per-node menu with edit selection; a dedicated editFirstSelection/editSecondSelection keeps each model’s invariants independent.shared/ work, host-gating, structural-only) and drag-and-drop (Phase C — an alternate entry to this same prompt that must contend with the force simulation).