diff --git a/src/lib/chat/editOperations.test.ts b/src/lib/chat/editOperations.test.ts index 7cabde19..2fd7d697 100644 --- a/src/lib/chat/editOperations.test.ts +++ b/src/lib/chat/editOperations.test.ts @@ -381,6 +381,49 @@ describe("applyEditOperations", () => { expect(updated!.data).toHaveProperty("status", "idle"); }); + it("updates node position from top-level position without requiring data changes", () => { + const ops: EditOperation[] = [ + { + type: "updateNode", + nodeId: "prompt-1", + position: { x: 420, y: 240 }, + data: {}, + }, + ]; + + const result = applyEditOperations(ops, { + nodes: baseNodes, + edges: baseEdges, + }); + + const updated = result.nodes.find((n) => n.id === "prompt-1"); + expect(updated!.position).toEqual({ x: 420, y: 240 }); + expect(updated!.data).toHaveProperty("prompt", "sunset photo"); + expect(result.applied).toBe(1); + expect(result.skipped).toEqual([]); + }); + + it("updates node position from data.position for backwards compatibility", () => { + const ops: EditOperation[] = [ + { + type: "updateNode", + nodeId: "prompt-1", + data: { position: { x: 320, y: 180 } }, + }, + ]; + + const result = applyEditOperations(ops, { + nodes: baseNodes, + edges: baseEdges, + }); + + const updated = result.nodes.find((n) => n.id === "prompt-1"); + expect(updated!.position).toEqual({ x: 320, y: 180 }); + expect(updated!.data).not.toHaveProperty("position"); + expect(result.applied).toBe(1); + expect(result.skipped).toEqual([]); + }); + it("skips with message when nodeId not found", () => { const ops: EditOperation[] = [ { diff --git a/src/lib/chat/editOperations.ts b/src/lib/chat/editOperations.ts index 27b4635b..64aa40cc 100644 --- a/src/lib/chat/editOperations.ts +++ b/src/lib/chat/editOperations.ts @@ -23,7 +23,12 @@ export type EditOperation = data?: Record; } | { type: "removeNode"; nodeId: string } - | { type: "updateNode"; nodeId: string; data: Record } + | { + type: "updateNode"; + nodeId: string; + position?: { x: number; y: number }; + data: Record; + } | { type: "addEdge"; source: string; @@ -137,19 +142,27 @@ export function applyEditOperations( } const node = nodes[nodeIndex]; + const rawPosition = + operation.position ?? + (operation.data.position as { x: number; y: number } | undefined); + const hasValidPosition = + rawPosition && + Number.isFinite(rawPosition.x) && + Number.isFinite(rawPosition.y); const safeData = sanitizeChatNodeData(node.type, operation.data); - if (Object.keys(safeData).length === 0) { + if (Object.keys(safeData).length === 0 && !hasValidPosition) { skipped.push( `updateNode: no editable fields provided for node "${operation.nodeId}"` ); break; } - + console.log("Updating node", operation.nodeId, "with data", safeData); // Update node data immutably nodes = nodes.map((n) => n.id === operation.nodeId ? { ...n, + position: hasValidPosition ? rawPosition : n.position, data: { ...n.data, ...safeData, diff --git a/src/lib/chat/tools.ts b/src/lib/chat/tools.ts index e068a6bc..ecdfd6af 100644 --- a/src/lib/chat/tools.ts +++ b/src/lib/chat/tools.ts @@ -206,6 +206,10 @@ export function createChatTools(nodeIds: string[]) { .record(z.string(), z.unknown()) .optional() .describe("Node data to set/merge for addNode/updateNode"), + position: z + .object({ x: z.number(), y: z.number() }) + .optional() + .describe("Canvas position for addNode/updateNode"), source: z .string() .optional()