From acf3bc7e422e0255bb207963d67e6ed5a8e4a8d8 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Mon, 29 Dec 2025 23:17:04 +0900 Subject: [PATCH] fix(edit_diagram): implement cascade delete for children and edges - Add automatic cascade deletion when deleting a cell - Recursively delete all child cells (parent attribute references) - Delete all edges referencing deleted cells (source/target) - Skip silently if cell already deleted (handles AI redundant ops) - Update prompts to inform AI about cascade behavior Fixes #450 --- app/api/chat/route.ts | 6 ++-- lib/system-prompts.ts | 6 ++-- lib/utils.ts | 66 +++++++++++++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 1d1ad1e..96f448e 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -605,7 +605,7 @@ Notes: Operations: - update: Replace an existing cell by its id. Provide cell_id and complete new_xml. - add: Add a new cell. Provide cell_id (new unique id) and new_xml. -- delete: Remove a cell by its id. Only cell_id is needed. +- delete: Remove a cell. Cascade is automatic: children AND edges (source/target) are auto-deleted. Only specify ONE cell_id. For update/add, new_xml must be a complete mxCell element including mxGeometry. @@ -614,8 +614,8 @@ For update/add, new_xml must be a complete mxCell element including mxGeometry. Example - Add a rectangle: {"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": ""}]} -Example - Delete a cell: -{"operations": [{"operation": "delete", "cell_id": "rect-1"}]}`, +Example - Delete container (children & edges auto-deleted): +{"operations": [{"operation": "delete", "cell_id": "2"}]}`, inputSchema: z.object({ operations: z .array( diff --git a/lib/system-prompts.ts b/lib/system-prompts.ts index 5016ae6..0400907 100644 --- a/lib/system-prompts.ts +++ b/lib/system-prompts.ts @@ -276,7 +276,7 @@ edit_diagram uses ID-based operations to modify cells directly by their id attri **Operations:** - **update**: Replace an existing cell. Provide cell_id and new_xml. - **add**: Add a new cell. Provide cell_id (new unique id) and new_xml. -- **delete**: Remove a cell. Only cell_id is needed. +- **delete**: Remove a cell. **Cascade is automatic**: children AND edges (source/target) are auto-deleted. Only specify ONE cell_id. **Input Format:** \`\`\`json @@ -301,9 +301,9 @@ Add new shape: {"operations": [{"operation": "add", "cell_id": "new1", "new_xml": "\\n \\n"}]} \`\`\` -Delete cell: +Delete container (children & edges auto-deleted): \`\`\`json -{"operations": [{"operation": "delete", "cell_id": "5"}]} +{"operations": [{"operation": "delete", "cell_id": "2"}]} \`\`\` **Error Recovery:** diff --git a/lib/utils.ts b/lib/utils.ts index 74e7038..cdb2c9a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -635,30 +635,60 @@ export function applyDiagramOperations( } else if (op.operation === "delete") { const existingCell = cellMap.get(op.cell_id) if (!existingCell) { - errors.push({ - type: "delete", - cellId: op.cell_id, - message: `Cell with id="${op.cell_id}" not found`, - }) + // Cell not found - might have been cascade-deleted by a previous operation + // Skip silently instead of erroring (AI may redundantly list children/edges) continue } - // Check for edges referencing this cell (warning only, still delete) - const referencingEdges = root.querySelectorAll( - `mxCell[source="${op.cell_id}"], mxCell[target="${op.cell_id}"]`, - ) - if (referencingEdges.length > 0) { - const edgeIds = Array.from(referencingEdges) - .map((e) => e.getAttribute("id")) - .join(", ") - console.warn( - `[applyDiagramOperations] Deleting cell "${op.cell_id}" which is referenced by edges: ${edgeIds}`, + // Cascade delete: collect all cells to delete (children + edges + self) + const cellsToDelete = new Set() + + // Recursive function to find all descendants + const collectDescendants = (cellId: string) => { + if (cellsToDelete.has(cellId)) return + cellsToDelete.add(cellId) + + // Find children (cells where parent === cellId) + const children = root.querySelectorAll( + `mxCell[parent="${cellId}"]`, + ) + children.forEach((child) => { + const childId = child.getAttribute("id") + if (childId && childId !== "0" && childId !== "1") { + collectDescendants(childId) + } + }) + } + + // Collect the target cell and all its descendants + collectDescendants(op.cell_id) + + // Find edges referencing any of the cells to be deleted + for (const cellId of cellsToDelete) { + const referencingEdges = root.querySelectorAll( + `mxCell[source="${cellId}"], mxCell[target="${cellId}"]`, + ) + referencingEdges.forEach((edge) => { + const edgeId = edge.getAttribute("id") + if (edgeId) cellsToDelete.add(edgeId) + }) + } + + // Log what will be deleted + if (cellsToDelete.size > 1) { + console.log( + `[applyDiagramOperations] Cascade delete "${op.cell_id}" → deleting ${cellsToDelete.size} cells: ${Array.from(cellsToDelete).join(", ")}`, ) } - // Remove the node - existingCell.parentNode?.removeChild(existingCell) - cellMap.delete(op.cell_id) + // Delete all collected cells + for (const cellId of cellsToDelete) { + const cell = cellMap.get(cellId) + if (cell) { + cell.parentNode?.removeChild(cell) + cellMap.delete(cellId) + } + } } }