Found while building #1017 (QA loop). The tests in packages/vscode/src/webview/src/utils/workflow-diff.test.ts assert the behaviour as it is today, so they are green — this issue records the defect they document, for the feature track to decide on. No test is skipped.
What happens
computeWorkflowDiff compares serialized form, not meaning, in two places. Both make the confirmation dialog overstate what an incoming apply_workflow will change.
1. Connections: the two key builders disagree on the port default
makeEdgeKey (workflow-diff.ts:19-21) builds the canvas key with edge.sourceHandle ?? ''; makeWorkflowEdgeKey (lines 23-30) builds the incoming key from conn.fromPort verbatim. Nothing normalizes between them — while serializeWorkflow (services/workflow-service.ts:65-66) normalizes a null handle to 'output' / 'input'.
So one unchanged wire counts as 1 added + 1 removed:
- a canvas edge with null handles, versus the same wire arriving as
fromPort: "output";
- and beyond the null case, because handle ids differ per node component.
StartNode.tsx:58 renders id="out", SubAgentNode.tsx:169,182 renders id="input" / id="output", PromptNode.tsx:122,135 renders id="in" / id="out" — while the AI-authoring guide's examples tell agents to write "fromPort": "output" (packages/core/resources/workflow-schema.json:530, and again at 697 / 1008 / 1095).
A start → subAgent wire is therefore out→input on the canvas and output→input from an agent. That is the most common wire in the product, and it diffs as a change on every apply.
2. Nodes: JSON.stringify(data) is key-order sensitive
modifiedNodes compares JSON.stringify(currentNode.data) against JSON.stringify(node.data) (lines 87-88). Two consequences:
- the same fields declared in a different order read as modified;
- an
askUserQuestion node reads as modified whenever an agent omits the option ids, because deserializeWorkflow runs ensureNodeDataItemIds (packages/core/src/utils/node-data-normalize.ts:48) and so every option on the canvas carries a generated id that an agent has no reason to reproduce.
Impact
App.tsx:586 computes this diff for any apply_workflow with requireConfirmation, and DiffPreviewDialog is the only thing the user sees before their canvas is replaced. The failure is one of trust rather than data loss: connection churn and phantom "modified" nodes appear on applies that change nothing, so the counts stop meaning anything and the review gate stops being read. DiffPreviewDialog.tsx:144 also keys its "no changes" message off totalChanges, so a genuine no-op apply is not reported as one.
Nothing is written incorrectly — the diff is a summary only; the apply itself is unaffected.
Suggested direction (feature track's call)
Normalize both sides before comparison rather than changing either builder alone: canonicalize the port default the same way serializeWorkflow does, and compare node data structurally (or strip generated ids) instead of by JSON.stringify. Note the handle ids genuinely differ per node component, so canonicalizing the null case alone does not fix the out vs output case.
Note for whoever fixes this
packages/vscode/src/webview/src/utils/workflow-diff.test.ts pins the current behaviour in four named cases:
reports one unchanged connection as 1 added + 1 removed when the canvas handles are null
reports a start→subAgent wire as 1 added + 1 removed because the canvas emits 'out' where an agent writes 'output'
reports a node as modified when only the key order of its data differs
reports an askUserQuestion node as modified when the agent omits the generated option ids
Those assertions describe the defect, not the desired behaviour — update them as part of the fix, rather than working around them.
Found while building #1017 (QA loop). The tests in
packages/vscode/src/webview/src/utils/workflow-diff.test.tsassert the behaviour as it is today, so they are green — this issue records the defect they document, for the feature track to decide on. No test is skipped.What happens
computeWorkflowDiffcompares serialized form, not meaning, in two places. Both make the confirmation dialog overstate what an incomingapply_workflowwill change.1. Connections: the two key builders disagree on the port default
makeEdgeKey(workflow-diff.ts:19-21) builds the canvas key withedge.sourceHandle ?? '';makeWorkflowEdgeKey(lines 23-30) builds the incoming key fromconn.fromPortverbatim. Nothing normalizes between them — whileserializeWorkflow(services/workflow-service.ts:65-66) normalizes a null handle to'output'/'input'.So one unchanged wire counts as 1 added + 1 removed:
fromPort: "output";StartNode.tsx:58rendersid="out",SubAgentNode.tsx:169,182rendersid="input"/id="output",PromptNode.tsx:122,135rendersid="in"/id="out"— while the AI-authoring guide's examples tell agents to write"fromPort": "output"(packages/core/resources/workflow-schema.json:530, and again at 697 / 1008 / 1095).A
start → subAgentwire is thereforeout→inputon the canvas andoutput→inputfrom an agent. That is the most common wire in the product, and it diffs as a change on every apply.2. Nodes:
JSON.stringify(data)is key-order sensitivemodifiedNodescomparesJSON.stringify(currentNode.data)againstJSON.stringify(node.data)(lines 87-88). Two consequences:askUserQuestionnode reads as modified whenever an agent omits the option ids, becausedeserializeWorkflowrunsensureNodeDataItemIds(packages/core/src/utils/node-data-normalize.ts:48) and so every option on the canvas carries a generatedidthat an agent has no reason to reproduce.Impact
App.tsx:586computes this diff for anyapply_workflowwithrequireConfirmation, andDiffPreviewDialogis the only thing the user sees before their canvas is replaced. The failure is one of trust rather than data loss: connection churn and phantom "modified" nodes appear on applies that change nothing, so the counts stop meaning anything and the review gate stops being read.DiffPreviewDialog.tsx:144also keys its "no changes" message offtotalChanges, so a genuine no-op apply is not reported as one.Nothing is written incorrectly — the diff is a summary only; the apply itself is unaffected.
Suggested direction (feature track's call)
Normalize both sides before comparison rather than changing either builder alone: canonicalize the port default the same way
serializeWorkflowdoes, and compare node data structurally (or strip generated ids) instead of byJSON.stringify. Note the handle ids genuinely differ per node component, so canonicalizing the null case alone does not fix theoutvsoutputcase.Note for whoever fixes this
packages/vscode/src/webview/src/utils/workflow-diff.test.tspins the current behaviour in four named cases:reports one unchanged connection as 1 added + 1 removed when the canvas handles are nullreports a start→subAgent wire as 1 added + 1 removed because the canvas emits 'out' where an agent writes 'output'reports a node as modified when only the key order of its data differsreports an askUserQuestion node as modified when the agent omits the generated option idsThose assertions describe the defect, not the desired behaviour — update them as part of the fix, rather than working around them.