Repository navigation
Let plugins create nodes with nothing selected; unship the demo plugins - #387
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.pyrejectedrequires_parent=Falseat registration time, stating exactly that reason.plugins.pydemanded a selected parent for every plugin.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_nodeandsandboxed_demosat in the shippedplugins/root, soMANIFEST.in'srecursive-include pluginspackaged them into the wheel and every user's picker listed "Hello Node", "Counter Node" and "Sandboxed Env Probe".Change
Creation
executePlugincall, surfaced to factories and handlers asPluginRunContext.spawn_x/spawn_y.SceneCanvasregisters a provider rather than storing a coordinate, so a pan or zoom since the last render cannot yield a stale position.requires_parent=Falseregisters now, for bothregister_node_kindandregister_builtin_plugin.add_plugin_nodeacceptsparent_id=Noneand leaves such a node unconnected.SceneDocument.adopt_pending_system_promptthen draws the note -> root edge when the first parentless chat node appears - the exact shape_resolve_branch_system_promptlooks for at send time. Without that step the note would be silently inert.zoomOnDoubleClickreturns to React Flow's default, which the gesture had displaced.Demo plugins
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
requires_parent=Falsenow asserts a real registration and arequires_parent is Falsespec, replacing the test that asserted it raised; a sibling test pins theTruedefault.test_no_demo_plugin_ships_in_the_pickerpins 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_handlerupdated for the new 4-argument signature.npm run check(schema drift, typecheck, lint, vitest, build, bundle size) green;ruffclean.