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..89ba67a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -633,32 +633,77 @@ export function applyDiagramOperations( // Add to map cellMap.set(op.cell_id, importedNode) } else if (op.operation === "delete") { - const existingCell = cellMap.get(op.cell_id) - if (!existingCell) { + // Protect root cells from deletion + if (op.cell_id === "0" || op.cell_id === "1") { errors.push({ type: "delete", cellId: op.cell_id, - message: `Cell with id="${op.cell_id}" not found`, + message: `Cannot delete root cell "${op.cell_id}"`, }) 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}`, + const existingCell = cellMap.get(op.cell_id) + if (!existingCell) { + // Cell not found - might have been cascade-deleted by a previous operation + // Skip silently instead of erroring (AI may redundantly list children/edges) + continue + } + + // 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 + // Also recursively collect children of those edges (e.g., edge labels) + for (const cellId of cellsToDelete) { + const referencingEdges = root.querySelectorAll( + `mxCell[source="${cellId}"], mxCell[target="${cellId}"]`, + ) + referencingEdges.forEach((edge) => { + const edgeId = edge.getAttribute("id") + // Protect root cells from being added via edge references + if (edgeId && edgeId !== "0" && edgeId !== "1") { + // Recurse to collect edge's children (like labels) + collectDescendants(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) + } + } } } diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index bc3c3da..c0d5b81 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.8", + "version": "0.1.10", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "type": "module", "main": "dist/index.js", diff --git a/packages/mcp-server/src/diagram-operations.ts b/packages/mcp-server/src/diagram-operations.ts index 5976e87..93225fc 100644 --- a/packages/mcp-server/src/diagram-operations.ts +++ b/packages/mcp-server/src/diagram-operations.ts @@ -182,32 +182,77 @@ export function applyDiagramOperations( // Add to map cellMap.set(op.cell_id, importedNode) } else if (op.operation === "delete") { - const existingCell = cellMap.get(op.cell_id) - if (!existingCell) { + // Protect root cells from deletion + if (op.cell_id === "0" || op.cell_id === "1") { errors.push({ type: "delete", cellId: op.cell_id, - message: `Cell with id="${op.cell_id}" not found`, + message: `Cannot delete root cell "${op.cell_id}"`, }) 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}`, + const existingCell = cellMap.get(op.cell_id) + if (!existingCell) { + // Cell not found - might have been cascade-deleted by a previous operation + // Skip silently instead of erroring (AI may redundantly list children/edges) + continue + } + + // 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 + // Also recursively collect children of those edges (e.g., edge labels) + for (const cellId of cellsToDelete) { + const referencingEdges = root.querySelectorAll( + `mxCell[source="${cellId}"], mxCell[target="${cellId}"]`, + ) + referencingEdges.forEach((edge) => { + const edgeId = edge.getAttribute("id") + // Protect root cells from being added via edge references + if (edgeId && edgeId !== "0" && edgeId !== "1") { + // Recurse to collect edge's children (like labels) + collectDescendants(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) + } + } } }