Pipeline builder dispatch kinds#
Note
This page is generated by scripts/generate_dispatch_reference.py
from
phenotypic.gui.builder._callbacks.DispatchKind plus a
hand-curated mapping that mirrors spec §5.6 verbatim. Run the
script after touching either to regenerate; the --check flag
is wired into CI to catch drift.
Every state-mutating event in the pipeline builder funnels through a
single fan-in callback that dispatches via
phenotypic.gui.builder._callbacks._dispatch_state_update().
Each kind value is one of the literals enumerated below; the
payload schemas come straight from spec §5.6 of the Builder canvas —
DAG redesign design document.
Dispatch flow per mutation:
Pre-mutation: the dispatcher runs
_seed_input_imageon every reachable scope (idempotent guard for validation Rule 6).Mutation: the named
kindbranch applies to a deep-copied state dict.Post-mutation: the fan-in callback re-runs
phenotypic.gui.builder._validation.validate(); the resulting list lands inSTORE_ISSUES, drives the toolbar count badge, and gates Run preview / Save pipeline (see Pipeline builder validation rules).
The kinds are documented in declaration order so the literal alias and this page never diverge.
Legacy linear builder#
add_node#
Payload schema: {class_name: str}
Append a fresh StepNode for class_name to the current legacy scope. Selects the new node.
add_pipeline#
Payload schema: {}
Append a StepNode carrying an empty nested scope.
select_node#
Payload schema: {node_id: str}
Set selected_node_id.
delete_node#
Payload schema: {}
Remove selected_node_id from the current scope.
drill_in#
Payload schema: {}
Push selected_node_id onto the breadcrumb (only if that node has a nested scope).
drill_out#
Payload schema: {}
Pop the breadcrumb tail.
reorder#
Payload schema: {order: list[str]}
Reorder the current scope’s nodes by node-id sequence.
edit_param#
Payload schema: {node_id: str, name: str, value: Any, omit: bool}
Set or delete params[name] on a specific node.
edit_label#
Payload schema: {node_id: str, label: str}
Update the node label.
port_slot_add#
Payload schema: {node_id: str, param: str}
Append a None slot to the consumer’s aux_ports list for a list-typed param. No-op for scalar ports.
port_slot_remove#
Payload schema: {node_id: str, param: str, slot: int}
Remove the slot at slot from the consumer’s aux_ports list and reindex remaining slots.
Legacy aux-port mutation#
wire_create#
Payload schema: {target_node_id: str, param: str, slot: int, class_name: str}
Embed a fresh aux StepNode under consumer.aux_ports[param][slot]. The dispatcher validates the target node, registry class, and operation-type compatibility before writing, then sets inspector_focus_aux to the filled slot. Retained for legacy inspector aux-wire payloads; the fixed linear map routes side-value creation through linear_palette_add.
wire_delete#
Payload schema: {target_node_id: str, param: str, slot: int}
Clear one legacy aux slot by setting consumer.aux_ports[param][slot] = None. If inspector_focus_aux points at the same slot, it is cleared too.
drill_in_aux#
Payload schema: {target_node_id: str, param: str, slot: int}
Push an aux-slot breadcrumb segment for an occupied legacy aux slot and clear inspector_focus_aux. Empty slots and missing target nodes are no-ops.
set_inspector_focus#
Payload schema: {focus: "aux" | "consumer", target_node_id: str, param: str, slot: int}
Set inspector_focus_aux to an occupied legacy aux slot when focus == "aux"; any other focus clears it. Missing nodes or empty slots are rejected without mutation.
DAG canvas: palette drag-and-drop#
block_create#
Payload schema: {class_name: str, x: float, y: float, container_block_id: str | None, ts: int}
Append a fresh BlockNode to the root scope (when container_block_id is None) or to a container’s nested scope (DFS lookup, innermost-wins hit-test per spec §4.4). Rejects class_name == "InputImage" with a toast — Input Image is auto-seeded per scope and cannot be created from the palette (spec §4.8 + §4.10). Drop coordinates (x, y) are not persisted; the leaf-first dagre pass re-lays the canvas on the next render (spec §4.7).
DAG canvas: wire drawing#
edge_create#
Payload schema: {source_block_id: str, target_block_id: str, target_port: str, edge_kind: "image" | "aux", ts: int}
Mint an Edge between two ports inside the same scope. For scalar aux + image-flow: replaces any existing wire from source_block_id (single-wire rule, spec §4.2 / §4.3). For list aux: server-side append — the dispatcher resolves target_slot = block.list_slot_counts.get(target_port, 0) and increments list_slot_counts[target_port] by 1 (eliminates the concurrent-drag race; the client emits no slot index). Cross-scope wires are rejected with a toast (spec §4.4).
edge_delete#
Payload schema: {edge_id: str, ts: int}
Remove a single edge by edge_id (DFS across every scope). For list-aux edges, remaining edges’ target_slot values are NOT renumbered — the freed slot becomes an empty placeholder. Use list_aux_reorder to compact.
DAG canvas: list-aux fan-in#
list_aux_reorder#
Payload schema: {block_id: str, param: str, new_order: list[str | None], ts: int}
Update the canonical execution order of a list-typed aux port. new_order is a permutation of the wired edge_ids interspersed with None placeholders for empty slots; the dispatcher rebuilds each edge’s target_slot from its position in the new order and updates block.list_slot_counts[param] = len(new_order). Non-permutation inputs are rejected with a toast (no-op).
list_aux_add_empty_slot#
Payload schema: {block_id: str, param: str, ts: int}
Increment block.list_slot_counts[param] by 1. No edge is materialised — empty slots live solely on the consumer block. At to_pipeline_dag time, slot indices in [0, count) not covered by an edge emit None entries.
DAG canvas: selection#
wire_select#
Payload schema: {edge_id: str | None, ts: int}
Set selected_edge_id. None deselects. Setting a new id clears selected_block_id (mutual exclusion, spec §4.5).
block_select#
Payload schema: {block_id: str | None, ts: int}
Set selected_block_id. None deselects. Setting a new id clears selected_edge_id (mutual exclusion, spec §4.5).
DAG canvas: Pipeline containers#
block_reparent#
Payload schema: {block_id: str, new_parent_block_id: str | None, x: float, y: float, ts: int}
Move a block between scopes. new_parent_block_id == None promotes the block to the current scope; a non-None value adopts it into that container’s nested scope (sibling-container moves are a single atomic dispatch). Rejects InputImage block_ids (the source must remain in its scope). Drag-out direction (new parent is an ancestor) with orphan edges → snap-back + toast; drag-in / sibling direction with orphan edges → drop the incompatible edges + toast the count, then commit the move (spec §4.4).
block_collapsed_toggle#
Payload schema: {block_id: str, ts: int}
Toggle block.collapsed for a Pipeline container. No-op on non-container blocks (anything whose class_name != "ImagePipeline"). The visual expand/collapse state is per-block (per spec §4.4).
drill_into_container#
Payload schema: {block_id: str, ts: int}
Push block_id onto state.breadcrumb. Validated: block_id must resolve to a Pipeline container at the current breadcrumb depth (siblings at other depths are rejected silently).
drill_to_scope#
Payload schema: {target_breadcrumb: list[str], ts: int}
Atomic breadcrumb replacement — replaces state.breadcrumb with target_breadcrumb in one dispatch. Each block_id in target_breadcrumb must resolve to a real Pipeline container at the right depth in state.root; stale ids → reject + toast. Used by scroll_to for cross-scope navigation.
block_delete_request#
Payload schema: {block_id: str, ts: int}
First stage of the two-stage container delete. Rejects InputImage block_ids (defense in depth). Non-container OR empty container (only the auto-seeded InputImage sentinel inside) → delegates to block_delete_confirm immediately. Non-empty container → sets state.pending_delete_block_id = block_id which opens the confirm-delete modal (body: “Delete container <label> and its N inner block(s)?” where N excludes InputImage).
block_delete_confirm#
Payload schema: {block_id: str, ts: int}
Second stage (or single stage if no confirmation was needed). Atomically removes the block, its nested scope (if any) including every inner block + edge, and every edge in the containing scope whose source or target is the block. Clears selected_block_id / selected_edge_id / pending_delete_block_id when they pointed at the deleted block or one of its edges.
Fixed linear port map#
target_select#
Payload schema: {target: LinearTarget, open_menu: bool} or a serialized LinearTarget payload directly
Persist the selected insertion/fill target for the current linear scope and optionally open the port menu. The fan-in callback emits this from linear port clicks and parameter replace actions.
linear_palette_add#
Payload schema: {class_name: str}
Add class_name at the selected fixed-linear target. Continuation/image-output/image-input targets insert on the image spine; parameter targets fill scalar or list aux ports. InputImage is rejected because each scope owns exactly one auto-seeded input. Unsupported linear shapes pause the edit and queue a warning toast.
linear_aux_replace_begin#
Payload schema: {target: LinearTarget, source_block_id: str, nonce: str}
Stage an exact fixed-linear auxiliary replacement target before the next palette add. The dispatcher records the consumer block, parameter or list slot, current source, pipeline revision, and nonce. Missing or changed targets clear the pending replacement instead of falling back to top-level insertion.
linear_delete_node_request#
Payload schema: {block_id: str}
Stage deletion by setting a linear pending-delete token for any string block_id. Unsupported shapes are no-ops here; existence and spine validation happen in linear_delete_node_confirm.
linear_delete_node_confirm#
Payload schema: {block_id: str}
Delete a fixed-linear spine block, reconnecting the image spine and removing side values owned by that block. This branch performs the existence and spine validation; missing or non-spine ids leave the graph unchanged. Clears the pending delete token afterward.
linear_node_move#
Payload schema: {block_id: str, direction: "left" | "right"}
Move a fixed-linear spine block one position left or right. Unsupported shapes, invalid block ids, and invalid directions are no-ops.
linear_clear_param#
Payload schema: {target: LinearTarget}
Clear one scalar side value or one list-slot side value at a fixed-linear parameter target. If clearing would remove an embedded ImagePipeline source, the dispatcher stages a confirmation token instead of deleting immediately.
linear_clear_param_confirm#
Payload schema: {target: LinearTarget}
Confirm a previously staged fixed-linear parameter clear, remove the side-value edge(s), reset the scope target to continuation, and clear the pending-delete token.
linear_drill_param_pipeline#
Payload schema: {target: LinearTarget, source_block_id: str | None}
Drill into an aux ImagePipeline source selected from a fixed-linear parameter target. When source_block_id is omitted, the dispatcher resolves the source from the target’s current aux edge.
linear_select_aux_value#
Payload schema: {source_block_id: str}
Select an existing aux source block in the current fixed-linear scope and clear edge selection. Missing source ids are no-ops. This retained dispatcher branch is not emitted by the current fixed-linear UI, whose side-value controls route through replace, clear, and drill actions.