Skip to content

Let plugins create nodes with nothing selected; unship the demo plugins - #387

Merged
dovvnloading merged 2 commits into
mainfrom
ux/create-nodes-without-selection
Sep 1, 2026
Merged

dovvnloading merged 2 commits into
mainfrom
ux/create-nodes-without-selection

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Problem

executePlugin's wire call carried no spawn position, so a node created without a parent had nowhere host-decided to go. That single gap propagated outward:

  • plugin_sdk.py rejected requires_parent=False at registration time, stating exactly that reason.
  • So plugins.py demanded a selected parent for every plugin.
  • So on an empty canvas, no plugin could be created at all - clicking one produced a warning notification and nothing else.

Three user-visible defects followed:

System Prompt required an existing node. A system prompt is authored before the conversation it governs, yet it could only be added by first selecting a node - so the user had to send a message, spending tokens, purely to unlock the action that should have shaped that message.

Double-click created a blank placeholder node. The handler described itself as "the R1 stand-in for the plugin picker / context menu creation paths". Those paths shipped; the stand-in did not go away, and because plugins needed a pre-existing node, it had become the only way to put a first node on an empty canvas - an undiscoverable gesture producing an untitled node.

Three demo plugins shipped to users. hello_node, counter_node and sandboxed_demo sat in the shipped plugins/ root, so MANIFEST.in's recursive-include plugins packaged them into the wheel and every user's picker listed "Hello Node", "Counter Node" and "Sandboxed Env Probe".

Change

Creation

  • The picker sends the canvas viewport's center with every executePlugin call, surfaced to factories and handlers as PluginRunContext.spawn_x/spawn_y. SceneCanvas registers a provider rather than storing a coordinate, so a pan or zoom since the last render cannot yield a stale position.
  • requires_parent=False registers now, for both register_node_kind and register_builtin_plugin. add_plugin_node accepts parent_id=None and leaves such a node unconnected.
  • System Prompt is parentless-capable: with nothing selected (or a stale selection) it creates the note unattached, deduping against an existing pending one. SceneDocument.adopt_pending_system_prompt then draws the note -> root edge when the first parentless chat node appears - the exact shape _resolve_branch_system_prompt looks for at send time. Without that step the note would be silently inert.
  • The double-click create gesture is removed, and zoomOnDoubleClick returns to React Flow's default, which the gesture had displaced.

Demo plugins

  • The three move to backend/tests/fixture_plugins/. They remain real plugins exercised through real discovery - the save/reload round-trip and the out-of-process worker isolation tests depend on that - just from a root that ships to nobody. The shipped root is now exactly the seven first-party plugins.

Test plan

  • New backend coverage: a pending note is created at the reported spawn point and left unattached; a second call reuses it; an unknown/stale parent id is treated as no selection rather than a blocking warning; a new branch root adopts the pending note and the note -> root edge exists.
  • requires_parent=False now asserts a real registration and a requires_parent is False spec, replacing the test that asserted it raised; a sibling test pins the True default.
  • New test_no_demo_plugin_ships_in_the_picker pins that none of the three can reappear in the shipped picker. The "More Plugins" catch-all behavior is proven against the fixture root instead.
  • test_executeplugin_rejects_wrong_arity_before_running_the_handler updated for the new 4-argument signature.
  • Frontend: the picker's spawn point is asserted for the selected, unselected and provider-registered cases, plus one test proving the provider is consulted at click time rather than at render time.
  • Full suites on a clean checkout of this branch: 3095 backend passed, 19 skipped; npm run check (schema drift, typecheck, lint, vitest, build, bundle size) green; ruff clean.

dovvnloading and others added 2 commits September 1, 2026 10:06
executePlugin's wire call carried no spawn position, so a node created
without a parent had nowhere host-decided to go. That one gap propagated
outward: the SDK rejected requires_parent=False at registration, so
backend/plugins.py demanded a selected parent for EVERY plugin, so on an
empty canvas no plugin could be created at all.

System Prompt was the worst case. A system prompt is authored before the
conversation it governs, yet it could only be added by first selecting a
node - so the user had to send a message, spending tokens, purely to
unlock the action that should have shaped that message.

The canvas double-click gesture was the other half. It described itself
as "the R1 stand-in for the plugin picker / context menu creation paths";
those paths shipped, the stand-in did not go away, and because plugins
needed a pre-existing node it had quietly become the only way to put a
first node on an empty canvas - an undiscoverable double-click producing
an untitled placeholder.

Creation:

- The picker sends the canvas viewport's center with every executePlugin
  call, surfaced to factories and handlers as PluginRunContext.spawn_x/
  spawn_y. SceneCanvas registers a provider rather than storing a point,
  so a pan or zoom since the last render cannot yield a stale position.
- requires_parent=False registers now, for both register_node_kind and
  register_builtin_plugin. add_plugin_node accepts parent_id=None and
  leaves such a node unconnected.
- System Prompt is parentless-capable: with nothing selected (or a stale
  selection) it creates the note unattached, deduping against an existing
  pending one. SceneDocument.adopt_pending_system_prompt then draws the
  note -> root edge when the first parentless chat node appears, which is
  the exact shape _resolve_branch_system_prompt looks for at send time -
  without it the note would be silently inert.
- The double-click create gesture is gone, and zoomOnDoubleClick returns
  to React Flow's default, which the gesture had displaced.

Demo plugins:

- hello_node, counter_node and sandboxed_demo move to
  backend/tests/fixture_plugins/. They sat in the shipped plugins/ root,
  so MANIFEST.in's `recursive-include plugins` packaged them into the
  wheel and every user's picker listed "Hello Node", "Counter Node" and
  "Sandboxed Env Probe". They remain REAL plugins exercised through real
  discovery - the save/reload round-trip and the out-of-process worker
  tests depend on that - just from a root that ships to nobody. The
  shipped root is now exactly the seven first-party plugins, and a test
  pins that none of the three can reappear in the picker.

Co-Authored-By: Claude <noreply@anthropic.com>
Both specs created their first node with a canvas double-click, the
gesture this branch removed. create-node.spec.ts even documented why:
"This is also the only way to get a FIRST node onto an empty canvas:
every plugin-picker entry requires an already-selected parent node, so
there is no other real entry point for node #1." That is precisely the
defect the branch fixes, so both now bootstrap through the entry point it
creates - the Plugins picker with nothing selected.

workspace-save-reload.spec.ts gets simpler rather than merely different:
it needed the placeholder only to satisfy System Prompt's then-required
parent_node_id, and that placeholder was itself an unpersisted kind
(excluded from session_save.py's _REGULAR_KINDS), so the test carried a
node that saving deliberately discarded. The note it actually asserts on
is now the only node on the canvas.

create-node.spec.ts additionally asserts the empty canvas starts at zero
nodes and that the parentless entry really produces the note kind, so it
covers both halves of the creation contract: a plugin that needs no
parent works on a blank canvas, and one that does still branches off the
selection.

Co-Authored-By: Claude <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit 0d4c54e into main Sep 1, 2026
4 checks passed
@dovvnloading
dovvnloading deleted the ux/create-nodes-without-selection branch September 1, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant