openpencil/packages/docs/programmable
Danila Poyarkov 8d10b9ca27
fix: export layers and pages that are not on screen in app mode (#877)
* fix(automation): export layers from a page that is not on screen

The app's raster export rendered against the page on screen unless the
caller passed a page, so MCP export_image with ids on any other page, or
with page_id naming another page, failed with "Raster export selection
must stay on a single page". Automation shares one app between clients,
so the page on screen says nothing about what a request means.

Render on the page that holds the requested layers instead. The user's
view and selection stay where they were.

* fix(cli): export the requested page from the running app

`openpencil export --page` never reached the app: `exportViaApp` only
forwarded `--document-id` and `--page-id`, and the app's `export` RPC
exported the given nodes or the selection on screen, ignoring the target
page. `--page` and `--page-id` therefore exported whatever was selected.

The CLI now resolves `--page` to a page ID through `list_documents` and
asks for a page-scoped export. The app answers a page-scoped export with
the layers of the target page, loading a `.fig` page that has not been
shown yet without switching to it.

CLI tests address the package source by `#cli/`, as Core and fig tests
already do, so the alias owner widens to the whole package.

* fix(automation): prepare fonts and layout for a page exported off screen

A page export loaded the layers of a page that had not been shown, but not
its fonts or layout, so text and auto layout could render differently from
the screen. preparePageNodes runs the same font and layout pass as a page
switch, once per page, without switching or superseding a switch.

The CLI export test now writes its own discovery file, so it no longer
replaces or removes the record of an app that is running.

* fix(automation): prepare a .fig page before running a tool on it

A `.fig` opens with only its first page populated; the others get their
layers, fonts and layout when first shown. The automation tool handler built
its FigmaAPI on the target page without loading it, so MCP tools aimed at a
page nobody had opened (`page_id`) saw an empty page: find_nodes found
nothing, export_image reported "No visible nodes to export", and create_shape
added a shape to a page that then held only that shape.

Prepare the target page first with preparePageNodes, as page exports do:
layers, fonts and layout, once per page. The page on screen does not change.

* fix(automation): render explicit export IDs on the page that holds them

Since the visual diff tools, the automation FigmaAPI passes its target page
with every raster export, so export_image with IDs from another page asked to
render them on the target page and failed with "Raster export selection must
stay on a single page". The page now names which layers to export only when no
IDs are given; an ID list is rendered on its own page.

* fix(core): share one off-screen page preparation between concurrent callers

Two concurrent preparePageNodes calls for the same page both populated it
and resolved its fonts, and the font manager's blocked-node set has no
reference count, so the first to finish unblocked text the second was still
resolving. Callers now share the in-flight preparation, which is kept once
it succeeds and retried after a failure.

preparePageNodes also reports whether the page is ready, so a caller can
refuse to run on a page whose document was closed or replaced mid-way
instead of acting on a page with no layers. Its unused options are gone:
one caller's signal cannot cancel a shared preparation.

* fix(automation): prepare the target page once for every command

Preparing an unshown .fig page lived in the page export handler, so explicit
export IDs, export_jsx, eval, tools, and the RPC fallback still saw such a
page as empty. The request dispatcher now prepares the resolved target page
before any page-targeted command, and stops with an error when the page's
document closed while it loaded.

* docs(changelog): fold the off-screen page fixes into one entry

* refactor(automation): rely on the dispatcher to prepare a tool's target page

The request dispatcher now prepares the target page before every
page-targeted command, so the tool handler no longer does it itself. The
tests run tools through the dispatcher, which is where that guarantee lives.

* docs(changelog): drop the tool entry now covered by the off-screen page fix

---------

Co-authored-by: Jason Woltje <1139190+jetrich@users.noreply.github.com>
2026-10-04 13:55:23 +00:00
..
cli fix: export layers and pages that are not on screen in app mode (#877) 2026-10-04 13:55:23 +00:00
sdk feat(updater): show download progress while installing an update 2026-09-25 23:15:14 +04:00
ai-chat.md feat(app): show who works on each page (#815) 2026-10-04 01:18:16 +04:00
byok-provider-compatibility.md docs(ai): tighten BYOK compatibility probes 2026-08-05 16:23:23 +03:00
collaboration.md feat(app): show who works on each page (#815) 2026-10-04 01:18:16 +04:00
index.md feat: export components as Storybook stories (#751) 2026-09-30 03:16:47 +04:00
jsx-renderer.md feat(design-jsx): export every property the renderer accepts (#814) 2026-10-03 15:40:21 +04:00
mcp-server.md feat: check designs live with a Lint panel, canvas markers, and fixes (#804) 2026-10-04 10:08:39 +00:00
native-api.md refactor(design-jsx): extract design JSX into its own package (#793) 2026-09-30 20:55:27 +04:00