openpencil/packages/docs/programmable/cli/comparing.md
Danila Poyarkov 3f594fdc3a
feat: add visual diff and patch apply tools and openpencil diff (#810)
* feat(core): add visual diff and patch apply tools

diff_visual renders two nodes at one scale through the existing raster export, compares them with pixelmatch, and returns the diff PNG with the changed ratio and region in source-node coordinates. It takes export_image's scale and maxEdge inputs. FigmaAPI gains a CanvasKit-backed raster codec and a pageId export option, so the app and headless CLI decode pixels and render nodes off the current page.

diff_apply applies diff_create and diff_show patches through the Figma API, validates every node before changing any, and supports dryRun and force. diff_show now simulates changes on a detached copy with the same property code. One serializer and parser back all three. diffDocuments compares two documents page by page by name path.

Image tool results now reach models as media with their metadata as text, for any tool rather than export_image alone. diff_create, diff_jsx, and diff_visual join the default AI tool set, and the diff tools are no longer hidden from WebMCP.

* feat(cli): add diff commands and agent diff guidance

openpencil diff create, jsx, show, apply, and visual run the Core diff tools on a file or the running app; apply writes back with --write or --output like eval. diff files compares two documents page by page and exits 1 when they differ.

The chat prompt asks the agent to edit in place and to verify risky edits against a reference copy with diff_jsx, diff_create, and diff_visual. The skill, CLI reference, MCP tool table, and a new Comparing Designs page document the commands and tools.

* feat(core): diff and patch node trees as JSX attributes

diff_create, diff_show, diff_apply, and diffDocuments used a hand-rolled
`key: value` property format that covered about fifteen properties,
matched children by name path, and could not see moves.

Nodes are now projected to the attributes the JSX export prints, and
jsondiffpatch matches children (by ID or by name path) and detects
moves. Patches list `-`/`+` attribute lines per node plus moved, added,
and removed children. diff_apply checks every hunk first, applies
attribute changes through the renderer's prop handling, and changes only
the fields an attribute moves, so IDs, instance links, and other state
survive. diff_show takes JSX attributes instead of a JSON props object.

design-jsx gains sceneNodeAttributes, parseJSXAttributes, and
jsxNodeFields for this, and the export round-trip property table is
shared so every case is also diffed and applied. `diff files` loads its
documents in order so node IDs, and so its patches, are deterministic.

* fix(core): keep diff_apply atomic and diff files honest about differences

- Added nodes render before anything else changes; if one fails, for
  example on a missing component, the rendered ones are deleted and
  nothing else is committed.
- A hunk with an attribute the renderer ignores fails instead of
  reporting "unchanged".
- diffDocuments reports `changed` from page statuses, and a page only
  one document has gets its status but no patch, since patches do not
  add or remove pages. diff files uses it, so an added empty page no
  longer reads as a match.
- diff files rejects a --page neither document has and a --depth that
  is not a non-negative integer, exiting 2; diff_create's depth is
  validated the same way.
2026-10-03 21:00:13 +04:00

3.3 KiB

title description
Comparing Designs Diff nodes and documents structurally and visually, and apply patches.

Comparing Designs

The diff commands compare two nodes, preview or apply changes as patches, render pixel diffs, and compare whole documents. Each node command works on a file or, without one, on the document open in the running app.

Patches

openpencil diff create design.fig --from 1:23 --to 1:87

Prints a patch that turns the first tree into the second. Its properties are the JSX attributes the JSX export writes, so it covers everything the export does. Children match by name, and a reordered child reads as one move:

@@ /Card #1:23
-rounded={8}
+rounded={12}
@@ /Card/Header #1:24
-bg="#FFFFFF"
+bg="#F4F4F5"
@@ /Card/Badge #1:26 moved to 0
@@ /Card/Note #1:27 removed
@@ /Card/Price added to #1:23 at 3
+<Text name="Price" size={24}>$9</Text>

Each hunk names a node by path, for reading, and by ID, which locates it. - lines hold the old attribute values and + lines the new ones; an added node is its JSX.

diff show previews setting attributes on a node without changing it, and prints the patch:

openpencil diff show 1:24 design.fig --attributes 'bg="#F4F4F5" rounded={8}' > header.diff

diff apply applies a patch to the document whose IDs it names. Every node must still have the patch's old values unless --force skips that check, and nothing changes unless every hunk applies, so a patch never half-applies:

openpencil diff apply header.diff design.fig --dry-run   # validate first
openpencil diff apply header.diff design.fig --write     # save in place
openpencil diff apply header.diff design.fig -o out.fig  # save elsewhere

Updates change only the fields their attributes set, so node IDs, instance links, and state JSX does not describe stay intact. Removed nodes are deleted and added nodes are rendered from their JSX.

JSX diff

openpencil diff jsx design.fig --from 1:23 --to 1:87

Compares two subtrees as a line diff of their design JSX, for reading rather than applying.

Visual diff

openpencil diff visual design.fig --from 1:23 --to 1:87 --output diff.png

Renders both nodes at the same scale and writes a PNG with changed pixels in red over a faded copy of the source. The report includes the changed pixel ratio and the changed region in source-node coordinates. --scale and --max-edge bound the render the same way export_image does; --threshold sets the color tolerance.

Comparing documents

openpencil diff files before.fig after.fig
openpencil diff files before.fig after.fig --page "Mobile" --json

Compares two documents page by page. Pages match by name and nodes by name path, so two versions of a file compare even though node IDs differ; the patch applies to the first document. Patches do not add or remove pages: a page only one document has is listed by name and status instead. Like diff(1), the command exits with status 1 when the documents differ.

Agents

The same operations are available as the diff_create, diff_jsx, diff_show, diff_apply, and diff_visual tools for the built-in AI chat and MCP clients. diff_visual returns its image to the model, so an agent can confirm that an edit touched only the intended region.