Repository navigation
feat: group containers, a producer's own grouping of nodes - #91
Merged
Merged
Conversation
A scope of kind `group` in a document (`subgraphs: [{ id, kind: "group",
label?, parent?, nodes? }]`) is drawn as a labelled frame around the
nodes that declare it or that it lists, nested under its parent when
that is a group too. The members, their ports and the edges between
them are untouched, and the frame is laid out in the graph's own
direction, so a group changes where its members sit and never how they
read. The platform learns nothing from the grouping; what a group is (a
layer, a domain, a stage) is the producer's business and shows only in
its label. A scope of any other kind still creates nothing: the root
scope every producer emits must not become a frame around the graph.
Claude-Session: https://claude.ai/code/session_012yAjXyWw6JyWsXvHG3cG9c
A group is now drawn and laid out the way mlir-viewer draws a nested region. Each group lays out on its own (`SEPARATE_CHILDREN`), members first, then the group as one block among its siblings, so the frame stays tight around its members; the client draws a tinted body, a header band carrying the label, and a border over both. ELK cannot route an edge between members of different groups in one separate layout, so an edge that leaves or enters a group is cut where it crosses the frame, at a boundary port (`port:group-boundary`): one piece per level, as mlir-viewer cuts an edge at each container it crosses (`group-edges.ts`). Edges reaching the same member through the same frame share the way in: one port per group, direction and inner endpoint, one piece inside. The stretch between frames keeps the edge's id, arguments and caption; a piece several edges share lists them (`wf:edgesOf`), keeps a role only when they all have it, and is not deleted on its own. Only the last piece draws an arrowhead. Every piece stays at the root of the model, so routes keep their absolute coordinates and saving, restoring and rerouting them is unchanged; ELK places each piece in its group from its endpoints, and the engine takes a boundary port's position from ELK. A dragged node stays inside the node holding it: `ContainedMovementRestrictor`, bound as GLSP's `IMovementRestrictor`, refuses a position outside the parent (below a group's header band), and a drop there is not committed. Fixed on the way: - Reopening a saved layout wrote each saved absolute position back as a position relative to the node's parent, so every member of a container landed outside it; the loader now subtracts the parent's origin. A reopened group also lost its size and its boundary ports, which only the layout run produces; `restoreGroupGeometry` rebuilds them from the saved positions and routes. - The rerouter that runs after a move treated a group as an obstacle, so an edge inside a group was routed around it. Tooling: `scripts/render-svg/render.mjs` renders a graph document to SVG through the server's layout and the real client in headless Chrome, as "Export as SVG" writes it, with a choice of layout (`--all-layouts` for the three side by side), ELK options, node families and stylesheets. Claude-Session: https://claude.ai/code/session_01E8WfqKknDNN6xpNVcJL5K8
…tead Clicking a node inside a group grew the node and the group. The group was declared resizable in the shape type hints, and GLSP's change-bounds tool looks for the nearest resizable ancestor of whatever is pressed: the click became the start of a resize of the group, which measures the group and every member in the hidden model and applies the measurements. Sprotty's `ComputedBoundsApplicator` writes `size` directly, past the `bounds` setter `WorkflowNode` overrides, and a node measures as its box plus what is drawn past it, so the member grew. Reproduced in headless Chrome with real mouse events: 217x74 became 251x72, the group 271 became 280. - Groups are not resizable by hand (`resizable: false`); a frame grows when its members need it. - `NodeSizePreservingBoundsApplicator`, bound as the `ComputedBoundsApplicator`: a measurement moves a workflow node but never resizes it, so no other measuring path can grow one either. - `WorkflowChangeBoundsOperationHandler` keeps a moved node's own size unless its type is declared resizable: GLSP's handler copied whatever size the client sent, which would have made a grown node permanent. - A member dragged right or down takes the frame with it: `growGroupsToFit` grows the group, and any group around it, after the move, the right-hand boundary ports follow, and their edges are routed again with the moved node's. The restrictor leaves a group's area open to the right and downwards and still refuses the left edge and the header; other containers stay closed on every side. Tests: group-move (4), node-size-preserving-bounds-applicator (2), the restrictor's updated cases, parity baseline +1 binding. Claude-Session: https://claude.ai/code/session_01E8WfqKknDNN6xpNVcJL5K8
The client draws a family's colour and icon from its own copy of the families (`clientBehavior.nodeFamilies`), as the webview gets them from the profile; `--families` reached only the server, which classifies the nodes, so a render showed no family icons. Claude-Session: https://claude.ai/code/session_01E8WfqKknDNN6xpNVcJL5K8
This was referenced Oct 23, 2026
…bers Review of #91: - `ContainedMovementRestrictor` held a node inside any node holding it. A control structure's branch (an if, a loop) holds its members the same way but never grows, and its layout leaves 15-20 px of padding, so a lone member of one could barely move and a drop further was reverted. Only a group now holds its members; the others move as freely as on main. - Dragging a group saved only the group's absolute position. Its members kept their old absolute positions in the layout file and reopened shifted back by the drag, out of the frame (unless a later save of all positions happened to land last). A move now saves every named node inside the moved one too (`group-move.test.ts`). - Comments: groups lay out left to right (the comment said "the graph's direction"), and the platform's comments no longer name a product. Claude-Session: https://claude.ai/code/session_01E8WfqKknDNN6xpNVcJL5K8
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.
A scope of kind
groupin a graph document,subgraphs: [{ id, kind: "group", label?, parent?, nodes? }], is drawn as a labelled frame around the nodes that declare it or that it lists; a group whoseparentis a group nests inside it. The members keep their ids, ports and edges, and the frame lays out on its own, left to right (ELK layered,SEPARATE_CHILDREN), so grouping changes where members sit and never how they read.The platform learns nothing from the grouping. What a group is, a layer, a domain, a stage, is the producer's business and shows only in its label. A scope of any other kind still creates nothing, so the root scope every producer emits does not become a frame around the whole graph. Two groups that name each other as parent both land at the root instead of vanishing inside each other; a node two groups list is claimed by the first.
Changes:
NODE_GROUP/GROUP_LABELin shared;createGroupNodeand the grouping pass in the GModel transform; the layout configurator, layout engine (auto-sized container, ports kept in order) and diagram configuration (type mapping, hint: movable, not deletable);GroupNodeandGroupNodeViewon the client with their CSS, registered in the stock views module (parity baseline updated by one entry).Tests: seven server cases in
group-scopes.test.ts(members moved, label fallback, nesting, edges kept, other kinds ignored, parent cycle, double claim), a client source test, the parity test. Typecheck, every suite and the neutrality gates pass.Second platform change for the NDF document diagram (
ide/ndf-ide/ndf-ide-plan.md, N1); the CLI's--group refinement|domainemits these scopes.Second commit: nested regions, edges cut at frames, moves kept inside
Modelled on mlir-viewer's nested containers:
SEPARATE_CHILDREN), drawn as a tinted body with a header band carrying the label.port:group-boundary), one piece per level. Edges reaching the same member through the same frame share one port and one inner piece. The stretch between frames keeps the edge's id, arguments and caption; a shared piece lists its edges (wf:edgesOf) and is not deleted on its own. Every piece stays at the root, so saved and rerouted routes keep absolute coordinates.ContainedMovementRestrictor(GLSPIMovementRestrictor) refuses a drag that would take a member outside its group, or under the group's header. Members of other containers (an if or loop branch) move as freely as before.scripts/render-svg/render.mjsrenders a graph document to SVG through the server layout and the real client in headless Chrome, with a choice of layout.Tests:
group-edges.test.ts(8, incl. a real-ELK layout check),group-geometry.test.ts(3, reopen round trip),contained-movement-restrictor.test.ts(6), updatedgroup-scopesandgroup-node-viewtests, parity baseline +2 bindings. Typecheck, neutrality and all suites pass.Review fixes (45d5f9a)
group-move.test.ts).https://claude.ai/code/session_01E8WfqKknDNN6xpNVcJL5K8