Compare commits

...

2 Commits

  1. 43
      src/lib/chat/editOperations.test.ts
  2. 19
      src/lib/chat/editOperations.ts
  3. 4
      src/lib/chat/tools.ts

43
src/lib/chat/editOperations.test.ts

@ -381,6 +381,49 @@ describe("applyEditOperations", () => {
expect(updated!.data).toHaveProperty("status", "idle"); 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", () => { it("skips with message when nodeId not found", () => {
const ops: EditOperation[] = [ const ops: EditOperation[] = [
{ {

19
src/lib/chat/editOperations.ts

@ -23,7 +23,12 @@ export type EditOperation =
data?: Record<string, unknown>; data?: Record<string, unknown>;
} }
| { type: "removeNode"; nodeId: string } | { type: "removeNode"; nodeId: string }
| { type: "updateNode"; nodeId: string; data: Record<string, unknown> } | {
type: "updateNode";
nodeId: string;
position?: { x: number; y: number };
data: Record<string, unknown>;
}
| { | {
type: "addEdge"; type: "addEdge";
source: string; source: string;
@ -137,19 +142,27 @@ export function applyEditOperations(
} }
const node = nodes[nodeIndex]; 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); const safeData = sanitizeChatNodeData(node.type, operation.data);
if (Object.keys(safeData).length === 0) { if (Object.keys(safeData).length === 0 && !hasValidPosition) {
skipped.push( skipped.push(
`updateNode: no editable fields provided for node "${operation.nodeId}"` `updateNode: no editable fields provided for node "${operation.nodeId}"`
); );
break; break;
} }
console.log("Updating node", operation.nodeId, "with data", safeData);
// Update node data immutably // Update node data immutably
nodes = nodes.map((n) => nodes = nodes.map((n) =>
n.id === operation.nodeId n.id === operation.nodeId
? { ? {
...n, ...n,
position: hasValidPosition ? rawPosition : n.position,
data: { data: {
...n.data, ...n.data,
...safeData, ...safeData,

4
src/lib/chat/tools.ts

@ -206,6 +206,10 @@ export function createChatTools(nodeIds: string[]) {
.record(z.string(), z.unknown()) .record(z.string(), z.unknown())
.optional() .optional()
.describe("Node data to set/merge for addNode/updateNode"), .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 source: z
.string() .string()
.optional() .optional()

Loading…
Cancel
Save