-
Notifications
You must be signed in to change notification settings - Fork 0
Node Links, Edges & Link Inspector
Node links are the named, node-to-node relationships on the canvas. They are domain objects persisted as NodeLink, then projected into React Flow edges for rendering. The key separation is:
-
NodeLinkis the durable shape: ids, kind, config, auto flag, label, transient run result. - React Flow
Edgeis the view shape produced from aNodeLink. - The inspector edits the
NodeLink; it should not treat edge data as the source of truth for config.
A link is created and maintained through this path:
- Persisted links are normalized by
parseNodeLink/parseNodeLinks. - Renderer state holds normalized
NodeLink[]. - Renderer helpers map those links into React Flow edges with
type: 'nodelink'. - The canvas renders each edge through
NodeLinkEdge. - Nodes expose connect affordances through
LinkHandles. - Selecting an edge opens
LinkInspectoragainst the originalNodeLink. - Inspector changes flow back as patches through
patchLink, then state rebuilds edges. - Saving serializes links through
serializeLinks, which drops transient run state.
flowchart LR
A["Project file links"] -->|parseNodeLink / parseNodeLinks| B["NodeLink[] normalized"]
B -->|deserializeLinks| C["workspace link state"]
C -->|linksFromSerialized / reconcileLinkEdges| D["React Flow Edge[] type='nodelink'"]
D --> E["NodeLinkEdge"]
C --> F["LinkInspector"]
F -->|onChange LinkPatch| G["patchLink"]
G --> C
C -->|serializeLinks| A
H["LinkHandles on node chrome"] -->|connect drag, handler outside excerpt| C
E -->|selected edge| F
The important nodes in this flow are the normalization boundary, the edge projection, and the inspector edit loop. Normalization is deliberately tolerant of junk entries, but it is also strict about required fields and per-kind config. The edge projection is intentionally narrow: linksFromSerialized copies kind, auto, lastRun, and label into edge.data, but it does not copy config. That means the inspector must receive the full NodeLink from workspace state, not reconstruct it from the edge.
This file owns the shared parse/normalize rules. It is Electron-free, DOM-free, and never throws. The renderer can import it directly, while core/workspace code can reuse the same rules without pulling browser-only or node:fs dependencies into the wrong bundle.
Supported kinds are:
| Kind | Required config shape |
|---|---|
file-output |
kind, path: string, mode: 'overwrite' | 'append', header: boolean
|
context-inject |
kind, wrapper: boolean, pastePointer: boolean
|
a2a-peer |
kind, message: 'last-output' | 'full-capture', deliverReply: boolean
|
parseNodeLink requires:
-
id,source,target, andkindto be strings. -
kindto be one of the known link kinds. -
source !== target. -
autoto be a boolean. -
configto be a record whose ownkindmatches the link kind and whose remaining fields satisfy that kind’s shape.
Optional behavior:
-
createdAtaccepts any number; if absent or invalid it falls back toDate.now(). -
labelsurvives only if it is a string that trims to a non-empty value, then it is capped at 60 characters. -
lastRunsurvives only if it is a record withat: number,ok: boolean, andsummary: string. -
parseNodeLinksmaps over an array and drops everynullresult.
This parser is the validation gate. If a link disappears after reload, the most likely cause is that its persisted form failed one of these checks. The parser intentionally drops junk silently, so production of invalid links can look like missing links rather than a thrown error.
This module contains renderer-side pure helpers for already-normalized links. It stays React Flow-shaped where useful.
-
deserializeLinks(raw)delegates toparseNodeLinks, so renderer load uses the same rules as shared/core. -
serializeLinks(links)produces JSON-safe copies, dropslastRun, and normalizeslabelby trimming and capping it at 60 characters.lastRunis intentionally transient; a stale result from a previous run should not be written back to the project file. -
linksFromSerialized(links)builds React Flow edges:-
id,source,target type: 'nodelink'data: { kind, auto, lastRun, label }
-
-
reconcileLinkEdges(links, previous)rebuilds edges from links but preservesedge.selectedby id. This matters when state changes while an edge is selected; without it, an inspector driven by edge selection can close unexpectedly. -
patchLink(link, patch)applies partial updates. Missinglabelmeans unchanged; explicitly passinglabel: undefinedremoves it. Other fields are only overwritten when notundefined. This helper does not validate or trim; callers such as the inspector are expected to pass valid values. -
removeLinksForNode(links, nodeId)drops links in both directions when a node is deleted.
The boundary to remember: these helpers assume links are already normalized. They do not replace the shared parser as the persistence validation layer.
NodeLinkEdge is the custom React Flow edge for type: 'nodelink'. It renders:
- A bezier path via
getBezierPathandBaseEdge. - Selected styling: lime
#c6f135stroke and width2; otherwise#4a4a48and width1.5. - A midpoint kind chip in SVG. The chip maps:
-
file-output→file -
context-inject→ctx -
a2a-peer→a2a - unknown kind → the raw kind string
-
- An optional user-facing name through
EdgeLabelRenderer. The label is a DOM overlay at the midpoint, not SVG text. This is deliberate: it remains readable at any zoom. The label element carriesnodrag nopanso interacting with it does not drag or pan the canvas.
NodeLinkEdge reads only data.kind and data.label from the edge. The rest of the link data, especially config, is not available on the edge. If you extend the edge to show more metadata, coordinate with linksFromSerialized; do not assume edge.data contains the full NodeLink.
LinkHandles is the shared anchor pair for linkable nodes:
- A target handle on the left.
isConnectableStart={false}prevents starting a connection from the target side. - A source handle on the right.
The handles have no ids. That matches the current link model, which stores only node ids in source and target; there are no sourceHandle / targetHandle fields on NodeLink. The comment states the handles are invisible until the node or an edge drag is active, with visibility controlled by the link-handle CSS class.
For extension work: if a node ever needs multiple link anchors, the data model must grow handle ids and the edge projection must set React Flow’s handle fields. The current model assumes one source anchor and one target anchor per linkable node.
LinkInspector is the popover shown when a node-link edge is selected. It receives:
- The full
NodeLink. -
sourceKindandtargetKind, used to gate valid behaviors. - Callbacks for
onChange,onRun,onDelete, andonClose. - A
runningflag so manual runs can disable the run button.
The inspector does not mutate the link directly. It emits partial patches. The parent is responsible for applying them, typically through patchLink, then reconciling edges.
validKinds(sourceKind, targetKind) mirrors the canvas connect rules as expressed through the registry imported as ../../../core/links/registry:
- If
LINK_SOURCES['file-output']includes the source kind, the inspector offersfile-output. - If that same source-kind gate passes and the target is
chatorterminal, it also offerscontext-inject. - If the target is
a2a-peerandLINK_SOURCES['a2a-peer']includes the source kind, it offersa2a-peer.
Note the coupling: as written, context-inject availability is gated by the file-output source list, not a separate context-inject source list. If the registry is meant to distinguish those source sets, this function and the canvas connect rules need to be updated together.
- Label: optional text input, max 60. An empty string is emitted as
undefined, whichpatchLinkinterprets as removing the label. - Behavior: kind select. Changing kind resets config with
linkDefaultConfig(kind)from the registry. -
file-output: path relative to project, modeoverwrite/append, timestamp header checkbox. -
context-inject: forchattargets, a wrapper checkbox; forterminaltargets, a paste-pointer checkbox. -
a2a-peer: forchatsources, a payload select for latest assistant reply versus full conversation; terminal sources show explanatory text instead. A checkbox controls whether the reply is delivered back. - Auto: checkbox controlling
auto. - Run now: calls
onRun, disabled whilerunning. - Delete: two-step in-popover confirmation, then
onDelete. - Last run: if
lastRunexists, shows an OK/error summary.
The inspector also re-exports linkDefaultConfig and LinkConfig for tests.
The cleanest way to reason about the system is:
-
Persistence enters through
parseNodeLinksordeserializeLinks. -
State owns the normalized
NodeLink[]. -
Rendering derives edges through
linksFromSerializedorreconcileLinkEdges. -
Edge rendering uses
NodeLinkEdge, which reads onlykindandlabel. -
Affordances come from
LinkHandles, one source and one target per linkable node. -
Selection should survive state updates through
reconcileLinkEdges. -
Editing happens in
LinkInspector, which emits patches. -
Saving goes through
serializeLinks, droppinglastRun.
When you modify link behavior, keep these boundaries intact:
- Do not persist
lastRunthrough normal serialization. - Do not rely on
edge.data.config; resolve the selected edge id back to aNodeLinkin workspace state. - Do not bypass
parseNodeLinkon load unless you deliberately own a different normalization layer. Invalid configs will be dropped. - When changing
kind, resetconfigto a shape that matches the new kind, or reload-time parsing will reject the link. - When deleting a node, remove incident links with
removeLinksForNode.
Adding a new link kind requires coordinated changes across several layers:
- The shared allowlist and per-kind config validator in
src/shared/node-links.ts. - The
NodeLink/LinkConfigtype union in shared types. - The registry module used for source-kind gating and
linkDefaultConfig. - The edge kind chip mapping in
NodeLinkEdge. - The inspector’s
KIND_OPTIONS,validKinds, and per-kind field controls. - The actual runtime executor or scheduler that performs the link action.
Adding multiple anchors per node requires more than UI work. The current NodeLink fields are only node ids, and LinkHandles are not keyed by handle id. Supporting multiple links of different roles would require handle ids on nodes, handle ids on links, and corresponding React Flow edge wiring.
The provided excerpts cover the shared parser, renderer projection helpers, edge renderer, handle component, and inspector. They do not show the canvas parent component that opens the inspector, the React Flow onConnect handler that creates links, the runtime executor/scheduler, the registry implementation, the persistence call sites in core/workspace-files.ts, the CSS for link classes, or any IPC surface. Those areas must be checked before changing creation, execution, or persistence semantics.
Sources: src/shared/node-links.ts, src/renderer/src/state/workspace-links.ts, src/renderer/src/canvas/NodeLinkEdge.tsx, src/renderer/src/nodes/LinkHandles.tsx, src/renderer/src/components/LinkInspector.tsx
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance