diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte index eada672f24..2053017639 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte @@ -54,6 +54,7 @@ } from '../agentToolUtils' import { loadFlowModuleState } from '../flowStateUtils.svelte' import { getNoteEditorContext } from '$lib/components/graph/noteEditor.svelte' + import { getGroupEditorContext } from '$lib/components/graph/groupEditor.svelte' interface Props { sidebarSize?: number | undefined @@ -125,6 +126,7 @@ // Get NoteEditor context for note position updates const noteEditorContext = getNoteEditorContext() + const groupEditorContext = getGroupEditorContext() $effect(() => { if (!moveManager.movingModuleId) return @@ -569,6 +571,7 @@ selectNextId(id) removeAtId(flowStore.val.value.modules, id) } + groupEditorContext?.groupEditor.removeNode(id) refreshStateStore(flowStore) onDelete?.(id) delete flowStateStore.val[id] @@ -629,6 +632,11 @@ } targetModules.splice(insertIndex, 0, ...removedModules) selectionManager.selectByIds(removedModules.map((m) => m.id)) + for (const m of removedModules) { + groupEditorContext?.groupEditor.handleNodeMoved( + m.id, detail.sourceId, detail.targetId + ) + } } else { let indexToRemove = originalModules.findIndex((m) => moveManager.movingModuleId == m.id) let [removedModule] = originalModules.splice(indexToRemove, 1) @@ -639,6 +647,9 @@ } targetModules.splice(insertIndex, 0, removedModule) selectionManager.selectId(removedModule.id) + groupEditorContext?.groupEditor.handleNodeMoved( + removedModule.id, detail.sourceId, detail.targetId + ) } moveManager.clearMoving() } else { @@ -676,6 +687,7 @@ toolKind ) const id = targetModules[index].id + groupEditorContext?.groupEditor.addInsertedNode(id, detail.sourceId, detail.targetId) selectionManager.selectId(id) if (detail.inlineScript?.instructions) { diff --git a/frontend/src/lib/components/graph/groupEditor.svelte.ts b/frontend/src/lib/components/graph/groupEditor.svelte.ts index c133fd5ea7..fc211e8436 100644 --- a/frontend/src/lib/components/graph/groupEditor.svelte.ts +++ b/frontend/src/lib/components/graph/groupEditor.svelte.ts @@ -195,6 +195,55 @@ export class GroupEditor { isAvailable(): boolean { return !!this.flowStore.val.value } + + /** Remove a deleted node from all groups. Removes empty groups. */ + removeNode(nodeId: string): void { + const groups = this.getGroups() + let changed = false + for (const group of groups) { + const idx = group.module_ids.indexOf(nodeId) + if (idx !== -1) { + group.module_ids.splice(idx, 1) + changed = true + } + } + if (changed) { + this.setGroups(groups.filter((g) => g.module_ids.length > 0)) + } + } + + /** Add a newly inserted node to the group that contains both its neighbors. */ + addInsertedNode(newNodeId: string, sourceId?: string, targetId?: string): void { + if (!sourceId || !targetId) return + const groups = this.getGroups() + for (const group of groups) { + if (group.module_ids.includes(sourceId) && group.module_ids.includes(targetId)) { + group.module_ids.push(newNodeId) + this.setGroups(groups) + return + } + } + } + + /** Handle a node that was moved to a new position in the flow. */ + handleNodeMoved(movedId: string, sourceId?: string, targetId?: string): void { + const groups = this.getGroups() + const currentGroup = groups.find((g) => g.module_ids.includes(movedId)) + + if (currentGroup) { + // Was in a group — only keep if BOTH neighbors are in the same group + const sourceInGroup = sourceId ? currentGroup.module_ids.includes(sourceId) : false + const targetInGroup = targetId ? currentGroup.module_ids.includes(targetId) : false + if (!(sourceInGroup && targetInGroup)) { + // Moved to boundary or outside the group — remove + currentGroup.module_ids = currentGroup.module_ids.filter((id) => id !== movedId) + this.setGroups(groups.filter((g) => g.module_ids.length > 0)) + } + } else { + // Wasn't in a group — check if moved into one + this.addInsertedNode(movedId, sourceId, targetId) + } + } } export type GroupEditorContext = {