feat(editor): Add empty node groups (no-changelog) - #37884
Conversation
Only the canvas inserts this node, so it must not be user-addable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The re-insert added the placeholder and its connections without history, and snapshotted the group after the insert, so undo restored the deleted node next to an orphan placeholder on a duplicate parallel path. Record the placeholder and its edges, and snapshot the group before the insert. Also count only connectable members when deciding whether the deleted node was the group's last one: a group of one node plus a sticky lost its placeholder and bridged past itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An empty group holds only a hidden placeholder, so ungrouping strands it on the canvas, there are no nodes to add to the chat, and collapsing is a no-op. Hide those controls and guard the shared ungroup and toggle paths. Also cap the description panel height so a long description scrolls instead of covering the canvas, and reject a second connection onto a chip rather than auto-extending the group out of its empty state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An empty group is force-collapsed by the mapping, so it has no inline title editor even when the view store still reports it expanded. The rename flow read the view store alone, set the autofocus id and nothing happened. Decide on what the canvas actually renders instead, and apply the same correction to the description context menu item. Also open that rename flow right after the "add empty group" menu item creates a group, so the user can type its title straight away. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e last real member Counting connectable members does not say which node is being deleted. Deleting a sticky from a group of one node plus a sticky passed the guard, so a placeholder joined a group that still held a real node and was then orphaned: two connectable members never read as an empty group. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The chip handles were opacity:0 and pointer-events:none, inherited from the decorative handles on a normal collapsed group, so a connection could never start or land on an empty group. Give an empty group's handles a visible, interactive style while normal groups keep the hidden ones.
…tion Generate on an empty group now drops three chained demo nodes into the group when the AI builder is unavailable, so the empty-to-filled round trip is demoable offline. An empty group always shows its description and enlarges its connection handle hit area.
A collapsed group with a description now shows it under the title, not only on hover or toggle, unifying empty and filled chips.
Add the prototype's resting empty-group affordances to the chip: - An EMPTY badge and a dashed border, gated on isEmptyGroup. - A centered "+" that opens the node creator scoped to the group's placeholder. Picking a node runs the existing replace path, so the placeholder is swapped for the node inside the group and its boundary connections move onto the node. No new fill helper is needed. - A Build button in the description editor next to Save. Build saves the objective, then generates. The standalone toolbar sparkle is removed for empty groups so there is one Build affordance. Connection inheritance on fill is the grouped replaceNode contract, which is already locked by its own test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An empty group's card carries an add-node body under the header, so the VueFlow node height, the collapsed rect used by the layout pushes, and the component wrapper must all agree on the card height. getGroupCardHeight is the single source for that value. Expanded groups are unchanged. Also replaces the v1 alignment plan with the frame-by-frame v2 plan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The group title bar is now one card, as in the prototype frames: a header with the title, a right slot (EMPTY badge on an empty group, collapse chevron on a filled one), and the description inline under the title. The description is clamped to one line when expanded and two when collapsed, and opens an in-place editor on click; Save and Build stay in that editor. An empty group adds a body with the centered add-node "+". Collapsed cards show their side handles as dots because edges re-anchor onto them; only an empty card accepts new connections. The card height comes from getGroupCardHeight, the same source the mapping uses for the VueFlow node, so the handles sit at the middle of what is drawn. The floating description panel and its pin, info-icon and hover-reveal affordances are gone from the component; their providers are retired separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The canvas toolbar gets an add-group button above the sticky-note button, as in the prototype; it inserts the group at the centre of the visible canvas. Creation moves into useCanvasOperations.addEmptyGroup, which adds the placeholder node and its group inside one history bulk so undo removes both together. The context menu action uses the same path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The comment described a missing drop guard. Group connection rules are decided on release for every endpoint, and node handles carry no earlier per-group validation either, so an empty card already behaves as a node does during a drag. The comment now says so; no marker remains. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nline card The group card shows its description inline in every state, so the pin/eye toggle, the info-icon hover reveal, the per-workflow pinned-state storage and the four show/hide context-menu items have nothing left to control. Remove them and their strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VueFlow pauses its props-to-store sync for a tick after every store echo and drops a nodes update that lands in that window. Adding the placeholder and creating its group a microtask apart could therefore leave the canvas with a bare placeholder and no group card. Run both store mutations in one synchronous block so the mapping emits a single VueFlow update. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A source node validates a drop with its own isValidConnection, which parses the target handle id as a port string (mode/type/index). The group card's handles used the bare ids "left"/"right", which parse to the default mode "outputs", so a node output dropped on the group's left handle read as output-to-output and was refused before the connect event fired. Give the group handles the same port-string ids node handles use: inputs/main/0 on the left (target) and outputs/main/0 on the right (source). NodeView.resolveGroupEndpoint still rewrites the endpoint onto the placeholder node, so the stored connection is unchanged; the collapsed-group edge remap keeps using the constants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The editor measured its textarea inside a bare nextTick, which can run before the browser lays the element out, so scrollHeight read the collapsed one-line height and the editor opened too short until the next keystroke. Measure in a requestAnimationFrame instead, and give the textarea a fixed one-line starting height so the measurement is stable under the canvas zoom transform. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header clips its overflow so the resting title and description stay inside their line clamps. The description editor is an absolutely positioned overlay that grows below the header, so its action buttons (cancel, save, build) were clipped by that same overflow. Drop the clip only while editing; the header keeps its fixed height, so the card layout is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lower the group description zoom threshold so a fit-to-view of a normal workflow still shows the description and its click-to-edit target. Below 0.66 an empty group lost its only affordance to add a description. Elevate the description editor overlay with a shadow so it reads as a popover above the member nodes it covers, not a box glued on top. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Running ultrareview automatically — Adds a new placeholder node type and reworks group/connection logic across 35 files, touching core workflow data, deletion/undo paths, and execution pass-through—high risk of subtle regressions in group handling and workflow integrity.. I'll post findings when complete. |
PR review overviewBased on ownership of the 33 changed files in this PR:
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bundle ReportChanges will increase total bundle size by 6.59kB (0.01%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: editor-ui-esmAssets Changed:
Files in
Files in
Files in
Files in
Files in
Files in
Files in
|
There was a problem hiding this comment.
Ultrareview completed in 15m 20s
17 issues found and verified against the latest diff
Confidence score: 2/5
packages/frontend/editor-ui/src/app/views/NodeView.vuecan apply async demo-generation results to the newly active workflow after navigation, potentially mutating the wrong workflow; bind the continuation to the originating group before merging results.packages/frontend/editor-ui/src/app/composables/useCanvasOperations.tscan corrupt workflow topology when deleting grouped nodes with multiple main ports, andNodeView.vuecan leave a direct bypass around generated chains; preserve distinct branch/merge connections and prevent old neighbours from being reconnected after boundary edges are created.packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasNodeGroupLayout.tsanduseCanvasMapping.groups.tscan retain expanded state for empty groups while rendering no frame, shifting nearby nodes and allowing hidden placeholders to be selected; use one effective collapsed state for layout, mapping, and selection.packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vuehas two interaction regressions: expanded-group descriptions can render behind member nodes, and long collapsed names may not receive a tooltip; move the editor to the canvas popper and detect horizontal truncation.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts">
<violation number="1" location="packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts:404">
P2: Custom agent: **Frontend**
`add_empty_group` is untested, so regressions in this empty-canvas menu path or its read-only gating can ship despite the existing operation test. Add a `useContextMenu` test that asserts the item appears for an empty selection and is disabled in read-only mode.</violation>
</file>
<file name="packages/frontend/editor-ui/src/app/views/NodeView.vue">
<violation number="1" location="packages/frontend/editor-ui/src/app/views/NodeView.vue:1124">
P2: When the mock generation operation rejects, this detached promise produces an unhandled failure after `onGenerateGroup` has already returned. Await the operation and catch the error so a failed generation is reported instead of becoming an unhandled rejection.</violation>
<violation number="2" location="packages/frontend/editor-ui/src/app/views/NodeView.vue:1159">
P2: When mock generation runs from a scoped document, calling `useUniqueNodeName()` inside the event handler resolves the wrong document context because Vue has no active injection context after setup. Create this composable during setup and reuse it, or derive unique names from `workflowDocumentStore` directly.
(Based on your team's feedback about avoiding provide/inject outside the component tree.)</violation>
<violation number="3" location="packages/frontend/editor-ui/src/app/views/NodeView.vue:1163">
P1: Custom agent: **Frontend**
When navigation occurs while demo generation is loading node metadata, the continuation mutates the newly active workflow instead of the group that started generation. Bind the operation to the originating document and abort all post-await mutations when the document changes.</violation>
<violation number="4" location="packages/frontend/editor-ui/src/app/views/NodeView.vue:1232">
P1: When a connected empty group uses the mock generation path, deleting the placeholder reconnects its old neighbours after the new boundary edges were added. The generated chain therefore has an extra direct bypass from each predecessor to successor. Replace the placeholder through the existing grouped-node replacement operation, or remove it without connecting adjacent nodes.</violation>
</file>
<file name="packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue">
<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue:112">
P3: The new `$style.empty` and `$style.descriptionEditing` bindings have no matching module styles and add no useful state styling. Remove both unused references or add the corresponding module selectors.</violation>
<violation number="2" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue:789">
P2: Custom agent: **Frontend**
A long collapsed group name is now ellipsized, but `updateTruncated()` still checks only vertical overflow, so `isTitleTruncated` stays false and the full name tooltip never opens. Detect horizontal overflow with `scrollWidth > clientWidth` for collapsed titles.</violation>
<violation number="3" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue:883">
P2: When a filled group is expanded, a long description editor is painted inside the group’s `-10000` VueFlow stacking context, so member nodes paint over the textarea and actions. Teleport the editor to the canvas popper layer or raise the group node while editing.</violation>
</file>
<file name="packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasNodeGroupLayout.ts">
<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasNodeGroupLayout.ts:97">
P1: Custom agent: **Frontend**
When an expanded group loses its last real node, this line collapses only `rect` while the view state still treats the group as expanded. The layout then moves nearby nodes around the invisible `expandedRect`, even though only the empty card renders. Use `collapsedRect` as the empty group’s effective `expandedRect`, or exclude empty groups from layout push sources.</violation>
</file>
<file name="packages/frontend/editor-ui/src/features/workflows/canvas/stores/canvasNodeGroups.constants.ts">
<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/stores/canvasNodeGroups.constants.ts:23">
P2: This threshold change leaves the existing title-bar test failing: at zoom `0.5`, the description now renders because `0.5 >= 0.4`. Update the test to use a zoom below `0.4` and add a boundary assertion for `0.4`.</violation>
</file>
<file name="packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue">
<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue:982">
P3: When an empty group is selected, Ctrl/Cmd+Shift+G remains enabled because `canUngroupSelection` only checks that a group is selected, but this guard silently returns. Exclude empty groups from the shortcut eligibility, or disable the shortcut when no non-empty group is selected.</violation>
<violation number="2" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue:1548">
P2: When a customer right-clicks a new canvas location and chooses Add empty group, this event carries no location, so the group is created at the previous click position or `[0, 0]`. Forward the projected context-menu coordinates through the event, or update `lastClickPosition` when opening the canvas context menu.</violation>
</file>
<file name="packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.groups.ts">
<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.groups.ts:275">
P2: When an empty group is still expanded in view state, this line collapses only its rendered card. Layout then pushes nearby nodes for an invisible frame, while selection can add the hidden placeholder; use one effective collapsed predicate for layout and selection too, or clear the view state when the group becomes empty.</violation>
</file>
<file name="packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts">
<violation number="1" location="packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts:623">
P1: When the deleted group member has multiple main input or output ports, this rewiring changes the workflow topology: distinct IF/Switch branches can both execute downstream, and separate Merge inputs are collapsed. Do not replace multi-port members with the single-port placeholder, or add matching ports and preserve each connection index.</violation>
<violation number="2" location="packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts:3784">
P2: When two empty-group creations overlap, such as a double-click while the node-property load is pending, both calls compute `Group 1` before either mutation. Compute the default name after the await with a fresh group list, or serialize/recheck creation before calling `createGroup`.</violation>
<violation number="3" location="packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts:3801">
P2: When a user adds an empty group after cancelling a connection, this call can connect the hidden placeholder to the previously active node instead of creating a standalone group. Pass `isAutoAdd: true` or otherwise disable automatic connection for the placeholder insertion.</violation>
</file>
<file name="packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts">
<violation number="1" location="packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts:286">
P2: When a group still references an unresolved non-sticky node, this filter drops that member and classifies `[placeholderId, staleId]` as empty. Require every member ID to resolve before returning the placeholder, so stale groups are not collapsed or given empty-group chip operations.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| const names = ['Extract', 'Transform', 'Load'].map((label) => uniqueNodeName(label)); | ||
|
|
||
| // Add the three nodes chained left to right. | ||
| await addNodesAndConnections( |
There was a problem hiding this comment.
P1: Custom agent: Frontend
When navigation occurs while demo generation is loading node metadata, the continuation mutates the newly active workflow instead of the group that started generation. Bind the operation to the originating document and abort all post-await mutations when the document changes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/views/NodeView.vue, line 1163:
<comment>When navigation occurs while demo generation is loading node metadata, the continuation mutates the newly active workflow instead of the group that started generation. Bind the operation to the originating document and abort all post-await mutations when the document changes.</comment>
<file context>
@@ -1043,6 +1081,157 @@ function onCreateSticky() {
+ const names = ['Extract', 'Transform', 'Load'].map((label) => uniqueNodeName(label));
+
+ // Add the three nodes chained left to right.
+ await addNodesAndConnections(
+ names.map((name, i) => ({
+ type: SET_NODE_TYPE,
</file context>
| rect: isEmpty || isGroupCollapsed(group.id) ? collapsedRect : expandedRect, | ||
| collapsedRect, | ||
| expandedRect, |
There was a problem hiding this comment.
P1: Custom agent: Frontend
When an expanded group loses its last real node, this line collapses only rect while the view state still treats the group as expanded. The layout then moves nearby nodes around the invisible expandedRect, even though only the empty card renders. Use collapsedRect as the empty group’s effective expandedRect, or exclude empty groups from layout push sources.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasNodeGroupLayout.ts, line 97:
<comment>When an expanded group loses its last real node, this line collapses only `rect` while the view state still treats the group as expanded. The layout then moves nearby nodes around the invisible `expandedRect`, even though only the empty card renders. Use `collapsedRect` as the empty group’s effective `expandedRect`, or exclude empty groups from layout push sources.</comment>
<file context>
@@ -81,13 +84,17 @@ export function buildNodeGroupLayoutComponents({
nodeIds: [...group.nodeIds],
- rect: isGroupCollapsed(group.id) ? collapsedRect : expandedRect,
+ // An empty group always renders as its card, whatever the view state says.
+ rect: isEmpty || isGroupCollapsed(group.id) ? collapsedRect : expandedRect,
collapsedRect,
expandedRect,
</file context>
| rect: isEmpty || isGroupCollapsed(group.id) ? collapsedRect : expandedRect, | |
| collapsedRect, | |
| expandedRect, | |
| rect: isEmpty || isGroupCollapsed(group.id) ? collapsedRect : expandedRect, | |
| collapsedRect, | |
| expandedRect: isEmpty ? collapsedRect : expandedRect, |
| { node: placeholder.name, type: main, index: 0 }, | ||
| ]); | ||
| } | ||
| for (const outgoing of bySource[node.name]?.[main]?.flat() ?? []) { |
There was a problem hiding this comment.
P1: When the deleted group member has multiple main input or output ports, this rewiring changes the workflow topology: distinct IF/Switch branches can both execute downstream, and separate Merge inputs are collapsed. Do not replace multi-port members with the single-port placeholder, or add matching ports and preserve each connection index.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts, line 623:
<comment>When the deleted group member has multiple main input or output ports, this rewiring changes the workflow topology: distinct IF/Switch branches can both execute downstream, and separate Merge inputs are collapsed. Do not replace multi-port members with the single-port placeholder, or add matching ports and preserve each connection index.</comment>
<file context>
@@ -559,6 +561,75 @@ export function useCanvasOperations() {
+ { node: placeholder.name, type: main, index: 0 },
+ ]);
+ }
+ for (const outgoing of bySource[node.name]?.[main]?.flat() ?? []) {
+ if (!outgoing) continue;
+ addPlaceholderConnection([
</file context>
| .map((name) => store.getNodeByName(name)?.id) | ||
| .filter((id): id is string => Boolean(id)); | ||
| store.addNodesToGroup(groupId, createdIds); | ||
| deleteNode(placeholder.id, { trackHistory: true }); |
There was a problem hiding this comment.
P1: When a connected empty group uses the mock generation path, deleting the placeholder reconnects its old neighbours after the new boundary edges were added. The generated chain therefore has an extra direct bypass from each predecessor to successor. Replace the placeholder through the existing grouped-node replacement operation, or remove it without connecting adjacent nodes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/views/NodeView.vue, line 1232:
<comment>When a connected empty group uses the mock generation path, deleting the placeholder reconnects its old neighbours after the new boundary edges were added. The generated chain therefore has an extra direct bypass from each predecessor to successor. Replace the placeholder through the existing grouped-node replacement operation, or remove it without connecting adjacent nodes.</comment>
<file context>
@@ -1043,6 +1081,157 @@ function onCreateSticky() {
+ .map((name) => store.getNodeByName(name)?.id)
+ .filter((id): id is string => Boolean(id));
+ store.addNodesToGroup(groupId, createdIds);
+ deleteNode(placeholder.id, { trackHistory: true });
+}
+
</file context>
| disabled: isReadOnly.value, | ||
| }, | ||
| { | ||
| id: 'add_empty_group', |
There was a problem hiding this comment.
P2: Custom agent: Frontend
add_empty_group is untested, so regressions in this empty-canvas menu path or its read-only gating can ship despite the existing operation test. Add a useContextMenu test that asserts the item appears for an empty selection and is disabled in read-only mode.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts, line 404:
<comment>`add_empty_group` is untested, so regressions in this empty-canvas menu path or its read-only gating can ship despite the existing operation test. Add a `useContextMenu` test that asserts the item appears for an empty selection and is disabled in read-only mode.</comment>
<file context>
@@ -454,10 +400,13 @@ export function useContextMenuItems(
disabled: isReadOnly.value,
},
+ {
+ id: 'add_empty_group',
+ label: i18n.baseText('contextMenu.addEmptyGroup'),
+ disabled: isReadOnly.value,
</file context>
| (c): c is IConnection => Boolean(c), | ||
| ); | ||
|
|
||
| const { uniqueNodeName } = useUniqueNodeName(); |
There was a problem hiding this comment.
P2: When mock generation runs from a scoped document, calling useUniqueNodeName() inside the event handler resolves the wrong document context because Vue has no active injection context after setup. Create this composable during setup and reuse it, or derive unique names from workflowDocumentStore directly.
(Based on your team's feedback about avoiding provide/inject outside the component tree.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/views/NodeView.vue, line 1159:
<comment>When mock generation runs from a scoped document, calling `useUniqueNodeName()` inside the event handler resolves the wrong document context because Vue has no active injection context after setup. Create this composable during setup and reuse it, or derive unique names from `workflowDocumentStore` directly.
(Based on your team's feedback about avoiding provide/inject outside the component tree.) </comment>
<file context>
@@ -1043,6 +1081,157 @@ function onCreateSticky() {
+ (c): c is IConnection => Boolean(c),
+ );
+
+ const { uniqueNodeName } = useUniqueNodeName();
+ const names = ['Extract', 'Transform', 'Load'].map((label) => uniqueNodeName(label));
+
</file context>
| // is still empty — the placeholder is its only real member. | ||
| const connectableMembers = group.nodeIds | ||
| .map((id) => workflowDocumentNodes.getNodeById(id)) | ||
| .filter((node) => node !== undefined && node.type !== STICKY_NODE_TYPE); |
There was a problem hiding this comment.
P2: When a group still references an unresolved non-sticky node, this filter drops that member and classifies [placeholderId, staleId] as empty. Require every member ID to resolve before returning the placeholder, so stale groups are not collapsed or given empty-group chip operations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts, line 286:
<comment>When a group still references an unresolved non-sticky node, this filter drops that member and classifies `[placeholderId, staleId]` as empty. Require every member ID to resolve before returning the placeholder, so stale groups are not collapsed or given empty-group chip operations.</comment>
<file context>
@@ -274,6 +275,24 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
+ // is still empty — the placeholder is its only real member.
+ const connectableMembers = group.nodeIds
+ .map((id) => workflowDocumentNodes.getNodeById(id))
+ .filter((node) => node !== undefined && node.type !== STICKY_NODE_TYPE);
+ if (connectableMembers.length !== 1) return undefined;
+ const [node] = connectableMembers;
</file context>
| .filter((node) => node !== undefined && node.type !== STICKY_NODE_TYPE); | |
| .filter((node) => node === undefined || node.type !== STICKY_NODE_TYPE); |
| // A description can run to GROUP_DESCRIPTION_MAX_LENGTH; cap the editor | ||
| // so a long one scrolls instead of covering the canvas. | ||
| max-height: 40vh; | ||
| z-index: var.$index-popper; |
There was a problem hiding this comment.
P2: When a filled group is expanded, a long description editor is painted inside the group’s -10000 VueFlow stacking context, so member nodes paint over the textarea and actions. Teleport the editor to the canvas popper layer or raise the group node while editing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue, line 883:
<comment>When a filled group is expanded, a long description editor is painted inside the group’s `-10000` VueFlow stacking context, so member nodes paint over the textarea and actions. Teleport the editor to the canvas popper layer or raise the group node while editing.</comment>
<file context>
@@ -849,39 +784,141 @@ function onWrapperPointerDown(event: PointerEvent) {
+ // A description can run to GROUP_DESCRIPTION_MAX_LENGTH; cap the editor
+ // so a long one scrolls instead of covering the canvas.
+ max-height: 40vh;
+ z-index: var.$index-popper;
+}
+
</file context>
| ) { | ||
| // Ungrouping an empty group would strand its placeholder as a bare node on | ||
| // the canvas. Every entry point (toolbar, shortcut, context menu) lands here. | ||
| if (workflowDocumentStore.value.isEmptyGroup(groupId)) return; |
There was a problem hiding this comment.
P3: When an empty group is selected, Ctrl/Cmd+Shift+G remains enabled because canUngroupSelection only checks that a group is selected, but this guard silently returns. Exclude empty groups from the shortcut eligibility, or disable the shortcut when no non-empty group is selected.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue, line 982:
<comment>When an empty group is selected, Ctrl/Cmd+Shift+G remains enabled because `canUngroupSelection` only checks that a group is selected, but this guard silently returns. Exclude empty groups from the shortcut eligibility, or disable the shortcut when no non-empty group is selected.</comment>
<file context>
@@ -960,6 +977,10 @@ function onCanvasGroupUngroup(
) {
+ // Ungrouping an empty group would strand its placeholder as a bare node on
+ // the canvas. Every entry point (toolbar, shortcut, context menu) lands here.
+ if (workflowDocumentStore.value.isEmptyGroup(groupId)) return;
+
// Capture before deletion — the group is gone by the time we track.
</file context>
| $style.wrapper, | ||
| { | ||
| [$style.collapsed]: isCollapsed.value, | ||
| [$style.empty]: isEmptyGroup.value, |
There was a problem hiding this comment.
P3: The new $style.empty and $style.descriptionEditing bindings have no matching module styles and add no useful state styling. Remove both unused references or add the corresponding module selectors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue, line 112:
<comment>The new `$style.empty` and `$style.descriptionEditing` bindings have no matching module styles and add no useful state styling. Remove both unused references or add the corresponding module selectors.</comment>
<file context>
@@ -93,6 +109,8 @@ const wrapperClasses = computed(() => [
$style.wrapper,
{
[$style.collapsed]: isCollapsed.value,
+ [$style.empty]: isEmptyGroup.value,
+ [$style.readOnly]: props.readOnly,
[$style.selected]: props.selected,
</file context>
❌ 3 Tests Failed:
View the top 3 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
Summary
POC for empty node groups on the canvas. An empty group renders as a card with an inline, editable description, so you can name and describe a block of work before adding any nodes. Empty groups can be created from the canvas toolbar and context menu, connect through chip handles, and be filled by adding a node or generating one through the AI builder.
How to test
Related Linear tickets, Github issues, and Community forum posts
Review / Merge checklist
Backport to Beta,Backport to Stable, orBackport to v1(if the PR is an urgent fix that needs to be backported)🤖 PR Summary generated by AI