diff --git a/packages/pen-mcp/src/tools/element-tool-helpers.ts b/packages/pen-mcp/src/tools/element-tool-helpers.ts index 12550d3d9..f709cd712 100644 --- a/packages/pen-mcp/src/tools/element-tool-helpers.ts +++ b/packages/pen-mcp/src/tools/element-tool-helpers.ts @@ -4,6 +4,7 @@ import { invalidateCache, openDocument, resolveDocPath, + saveDocument, } from '../document-manager'; import { findNodeInTree, findParentInTree, getDocChildren } from '../utils/node-operations'; import { generateId } from '../utils/id'; @@ -176,11 +177,32 @@ export async function insertElementTree(args: { const rollback = async (reason: string): Promise => { if (!isLive && fileSnapshot !== null) { + // Restore disk content first so on-disk state is correct even if + // the subsequent live-canvas sync fails. await writeFile(resolvedFp, fileSnapshot, 'utf-8'); invalidateCache(resolvedFp); + // saveDocument is DUAL-WRITE for file-backed paths (document-manager.ts:411): + // it writes to disk AND calls pushLiveDocument. Our earlier + // handleBatchDesign call already pushed the bad insert to the live + // canvas; we must push the restored doc so the renderer state + // matches disk again. If no sync URL is active, pushLiveDocument + // is a no-op, so this is safe in all scenarios. + try { + const restoredDoc = await openDocument(resolvedFp); + await saveDocument(resolvedFp, restoredDoc); + } catch (syncErr) { + // Disk is the source of truth and is already restored; surface + // the sync failure so the caller knows live canvas may be stale + // but don't swallow the original insert failure. + throw new Error( + `${reason} (document restored to pre-insert state on disk, but re-syncing live canvas ` + + `after rollback failed: ${syncErr instanceof Error ? syncErr.message : String(syncErr)}. ` + + `Live canvas may show stale state until next refresh.)`, + ); + } } throw new Error( - `${reason} ${isLive ? '(live canvas cannot be atomically rolled back — the bad insert may still be visible until next refresh)' : '(document restored to pre-insert state)'}`, + `${reason} ${isLive ? '(live canvas cannot be atomically rolled back — the bad insert may still be visible until next refresh)' : '(document restored to pre-insert state on disk and re-synced to live canvas)'}`, ); };