Penpot Surface Comparison

This note is the current comparison surface for Penpot work in NEXUS.

Use it when deciding whether a task should use:

  • backend API
  • MCP/plugin
  • exported .penpot file

This is a derived working note, not the final source of truth.

The goal is practical:

  • show what is currently available where
  • make gaps visible
  • keep future AI and human work from rediscovering the same surface differences repeatedly

Update Rule

When a collaborator proves a capability, limitation, or incompatibility on one Penpot surface:

  • update this note
  • say whether it is verified, inferred, or still open
  • do not silently let chat become the only place that comparison knowledge exists

Current Comparison

Concern Backend API MCP/plugin Exported .penpot
Current live file identity Verified Verified Snapshot only
Current live page identity Verified Verified Snapshot only
Current selection Not available Verified Not available
Current page / UI context Not available Verified Not available
Page and object structure Verified through file/page methods Verified through live plugin code Verified from archive contents
Full file snapshot export Verified via export-binfile Not primary surface Native artifact
Board-level visual export Not yet verified on backend path Verified via export_shape Not applicable
Whole-page visual export Not yet verified on backend path Not yet stable in current setup Not applicable
Live mutation surface exists Verified by API docs Verified by plugin API/tool model Not a live surface
Live mutation shown in open Penpot app Inferred, not yet proven in recorded test Inferred, not yet proven in recorded test Not applicable
Plugin-context operations Not available Verified surface exists Not available
Portability / offline inspection Low Low High

Current Verified Backend API Findings

Verified useful methods include:

  • get-file
  • get-page
  • get-file-fragment
  • get-project-files
  • search-files
  • export-binfile
  • import-binfile

Verified mutation-oriented methods are exposed in the docs:

  • update-file
  • update-file-library-sync-status
  • update-file-snapshot

The current backend export behavior is:

  1. call export-binfile
  2. consume the SSE stream
  3. wait for the final asset URI
  4. download the resulting .penpot archive

Current Verified MCP/Plugin Findings

Verified in the current local setup:

  • the plugin can connect to the MCP server
  • Codex can reach the live file through the MCP/plugin path
  • live file/page enumeration works
  • live board enumeration works
  • board-level export_shape works
  • whole-page export_shape is currently unreliable and should not be the default export attempt
  • component-oriented live board editing works well through the plugin surface
  • duplicating a component base, evolving the duplicate, and instantiating it back into the board is a practical live workflow
  • for duplicated slice-card shells, directly editing inherited text nodes did not reliably show up in exported visuals
  • the current reliable workaround is to keep the component shell and overlay fresh local text nodes for per-card variation
  • inspect-style API surfaces are available through generateStyle and generateMarkup, and they are useful comparison surfaces when the visual export path is suspicious
  • createText("...") works in the live plugin path, but the created text can remain at 1x1 until it is explicitly resized
  • a detached or otherwise local projection should not be treated as having trustworthy provenance back to a reusable component source unless that trace is carried separately in naming or docs

Verified current live file example:

  • LaundryLog
  • pages:
  • Components
  • Screens
  • PATHS
  • Event Model

Current local operational note:

  • the working local path currently uses @penpot/mcp@2.15.0-rc.1.116
  • Codex uses mcp-remote against the Penpot /sse endpoint

That is a machine-local operational finding and should be re-verified in other environments.

Current Text Mutation Lab Findings

The current TextMutationLab.V1 in the live LaundryLog Penpot file established a sharper result set.

Verified outcomes:

  • fresh non-component text exports correctly in both png and svg
  • direct text edits on a component-derived slice instance were visible in live shape data but did not appear in exported visuals
  • detaching that instance did not by itself make inherited text edits export correctly
  • renaming the edited inherited text node did not make the export honor the changed text
  • hiding the inherited text node and overlaying a fresh local text node did export correctly

So the current problem appears narrower than \"Penpot text is broken\".

The stronger current reading is:

  • fresh local text is fine
  • inherited text inside this component-derived slice shell is the unstable case for export

Current Component State Lab Findings

The current ComponentStateTokenLab.V1 on the live LaundryLog PATHS page narrowed the problem further.

Verified outcomes:

  • Button.Option instance text edits exported correctly
  • Button.Option detached-instance text edits also exported correctly
  • Input.Text instance text edits exported correctly
  • Input.Text detached-instance text edits also exported correctly
  • a fresh overlay text node exported correctly once it was given an explicit visible size instead of remaining at a 1x1 default

The stronger current reading is now:

  • the export/render problem is not a universal \"component text\" failure
  • it appears narrower, and the current leading suspect is the inherited text inside the recovered slice-card shells
  • detached instances are still a valid escape hatch when we intentionally want to break inheritance
  • detached or local projections are not a trustworthy provenance trail back to a component source by themselves
  • but normal reusable component state work should stay higher on the ladder:
  • component instance
  • variant or explicit component state
  • detached instance
  • fresh local overlay or one-off text

The current strongest structural clue came from comparing generateMarkup results:

  • for the problematic slice shell, generateMarkup(type = "html") included both:
  • live edited HTML text nodes
  • stale base-value text inside embedded SVG fragments
  • for simpler controls like Button.Option and Input.Text, the generated HTML did not show that same mixed HTML plus stale-SVG pattern
  • generateMarkup(type = "svg") for the slice shell followed the stale base-value path, which matches the broken visual export

So the current working explanation is:

  • the slice shell is traveling through a mixed markup path
  • the stale embedded-SVG side is winning in the final export surface
  • this is why live shape data can look correct while exported visuals still show the base component text

Current Mini-Shell Structure Lab Findings

MiniShellStructureLab.V1 pushed the investigation one step further by rebuilding shell-like components from scratch instead of reusing the recovered CommandSlice.Base.V2 lineage.

Verified outcomes:

  • a flat from-scratch shell component exported edited instance text correctly
  • a from-scratch shell with one nested row board also exported edited instance text correctly
  • a from-scratch shell with three nested row boards also exported edited instance text correctly
  • adding the outer heavy stroke, rounded corners, drop shadow, and rounded nested rows still did not reproduce the stale-base-text problem

That means the current best reading is now:

  • nested rows by themselves are not the trigger
  • outer rounded borders and shadows by themselves are not the trigger
  • the problem is likely tied to the specific lineage or internal shape state of the recovered CommandSlice.Base.V2 family rather than to the general slice-shell pattern

Current practical implication:

  • rebuilding the slice-shell component family cleanly from scratch is now a credible corrective path
  • we should not assume the current CommandSlice.Base.V2 behavior defines what a clean Penpot slice-shell component must do

Current Token Caveat

The current slice-title texts in this experiment had no token bindings.

That means the current overlay workaround preserved resolved visual style, but it did not prove token-preserving behavior.

So the current practical warning is:

  • overlay text is acceptable as a current experiment workaround
  • it should not automatically be treated as the final pattern for token-governed component systems
  • for real reusable UI/component work, stay as high as possible on the ladder of:
  • component instance
  • variant/stateful component
  • detached instance
  • fully local overlay or one-off text

The current token-application probe added one more caution:

  • shape.applyToken(...) currently threw a Penpot-side check error in this live plugin path
  • token.applyToShapes(...) completed in a first plain-text probe, but did not produce the expected visible token-binding evidence

So the current working rule is:

  • treat token application through the current live plugin path as still under investigation
  • do not yet assume that a successful-looking token call means the token contract is durably bound and inspectable
  • re-verify token behavior explicitly before we lean on it for component-generation workflows

Current Projection-Build Findings

The current LaundryLog PATHS page work added a more important split than first expected.

Verified outcomes:

  • creating a readable path-row projection directly with createBoard() plus createText("...") is viable for live shape data and exported PNG output
  • for those created text nodes, explicit resize(...) was required before the exported PNG rendered the text reliably
  • however, the newer V2 / V3 / V4 path-row lineage did not paint its text reliably on the live PATHS canvas even though the same text existed in live shape data and exported correctly
  • the older PATH1.Row.V1 lineage did paint its text on the live canvas
  • cloning the visible V1 board preserved a text-bearing lineage that can be evolved further for readable live PATHS work

Current practical implication:

  • keep reusable component bases where they belong
  • treat the readable path-row board as a derived projection surface
  • for now, prefer evolving the visible V1 lineage for the live PATHS page instead of rebuilding the row from fresh createText(...) shells
  • if that projection is intentionally detached from the component ladder, carry that fact explicitly in naming and docs instead of assuming Penpot will retain a reliable back-link

Additional live finding:

  • the explicit Plugin API seam that matches dragging from the Penpot Assets tab is LibraryComponent.instance()
  • the V3 slices on PATHS are real connected component instances
  • current instance text overrides are visible in live shape data, but both the live canvas and the export render path still appear to prefer the master text content in the current test cases

Current strongest repro:

  • PATH1.Row.V6.InstanceBased on the live PATHS page is built only from CommandSlice.Clean.V3 and ViewSlice.Clean.V3 library instances
  • the stored text values on those instances reflect the intended LaundryLog overrides
  • the Penpot canvas still paints the master/default text such as myCommand, myView, and description
  • the export path follows the same stale master-render behavior

So the current issue is no longer \"can instances be created on PATHS\". It is \"instance text overrides are stored but not rendered\".

Current non-fixes already tested:

  • detaching the instance before applying the text overrides
  • forcing a text reflow by resizing the text nodes after the write

In the current lab, both of those still left canvas and export rendering the stale master/default text.

Current Upstream Evidence

Penpot community and GitHub history show that nearby component-override problems are real and recurring, even if the exact current slice-shell export issue has not yet been matched one-for-one.

Currently relevant upstream references:

Current interpretation:

  • override and variant behavior are definitely known Penpot problem areas
  • exporter mismatches are also a known Penpot problem area
  • we have not yet found a published upstream report that exactly matches:
  • \"edited inherited text inside this slice-card shell looks changed in live shape data but exports with the old base text\"
  • until that exact match is found, keep our local lab result recorded as a verified repo finding rather than pretending the upstream evidence is exact

Current Exported File Findings

The exported .penpot file is currently a ZIP archive containing readable JSON structure and related assets.

That makes it useful for:

  • checkpointing
  • offline inspection
  • durable structural comparison
  • sharing a stable snapshot

It is not the live backend state.

Current Open Questions

  • Can whole-page visual export be made reliable through the MCP/plugin path in the current stack?
  • Which live mutation operations are easier through backend API versus plugin context?
  • Do backend update-file mutations appear immediately in an open Penpot UI without manual refresh?
  • Which operations are available only in plugin context and not via backend API?

Current Mutation Comparison

The current comparison is no longer purely inferred.

MCP / Plugin Path

Verified practical fit for:

  • creating new slice-card structures
  • duplicating component bases
  • evolving those duplicates into new visual bases
  • instantiating those bases back into the Event Model board

This is currently the stronger surface for component-driven board work.

Backend API Path

Verified practical fit for:

  • live file and page inspection
  • export and checkpoint generation
  • lower-level mutation surface exposure through update-file

But the current mutation seam is stricter and lower-level.

A first live update-file mutation attempt against a LaundryLog ViewSlice failed because the payload must satisfy Penpot's internal shape schema very precisely. In particular, enum-like stroke values were not accepted when passed as naive JSON strings.

So the current practical rule is:

  • use MCP/plugin for live component-driven visual editing
  • use backend API for inspection, export, and lower-level patch work once the exact schema contract is known

Current LaundryLog Slice Proof

The current live LaundryLog file now includes first-pass recovered slice-language proofs built through the MCP/plugin path:

  • CommandSlice.Base.V2
  • ViewSlice.Base.V2

And visible PATH 1 instances derived from those bases:

  • PATH1: CommandSlice - Set Location (V2 Instance)
  • PATH1: ViewSlice - Ready (V2 Instance)

It now also includes a first readable PATHS page row projection:

  • PATH1.Row.V1

Current practical characteristics of that row:

  • one horizontal path row
  • a short path description above the row
  • detached slice-card shells derived from the V2 bases
  • per-card content rendered through fresh overlay text nodes
  • screen slots still acting as screenshot-like placeholders rather than full embedded screenshots

That proof used a recovered EM-1 slice-card language characterized by:

  • narrow calm slice cards
  • strong outer border and tinted header band
  • small system pill and uppercase slice-type marker
  • title plus short subtitle
  • semantic row containers for Screen, Command, Event, and View
  • stronger colored title bars with lighter structured data panels below

Current Clean Slice Rebuild

The live LaundryLog file now also has a clean slice family rebuilt from scratch instead of inheriting from the older recovered shell lineage.

Current clean components:

  • SliceRow.Screen.Clean.V1
  • SliceRow.Command.Clean.V1
  • SliceRow.Event.Clean.V1
  • SliceRow.View.Clean.V1
  • SliceRow.Ref.Clean.V1
  • CommandSlice.Clean.V1
  • ViewSlice.Clean.V1

Current clean projection:

  • PATH1.Row.V2.Clean

Current working interpretation:

  • the clean slice family follows the EM-1 f63e1af / hosted Slice.html visual language closely enough to act as the current Penpot direction
  • the row components are now an explicit reusable seam beneath the shell components
  • the shell components are now an explicit reusable seam beneath the path-row instances
  • this is a better FnHCI-aligned ladder than the older detached-shell workaround

Current component ladder:

  • row component
  • shell component
  • path-row instance

Current token sets created in the live file:

  • EventModel.Foundation.V1
  • EventModel.Viewport.Tablet.V1
  • EventModel.Viewport.Desktop.V1
  • EventModel.Viewport.Wide.V1

Current practical token rule:

  • the token catalog now exists as a local Penpot design-system seam
  • this pass aligned the clean slice values to those tokens
  • direct token binding on the new slice family is still a deliberate next step rather than something we should pretend is already fully trustworthy

Current Active-Page Targeting Quirk

The current live plugin path exposed an important operational quirk:

  • top-level board creation and top-level reparenting followed the active Penpot page
  • attempts to place or move top-level shapes onto a different page before switching the active page did not land where a normal tree mutation model would suggest

Current practical rule:

  • before page-level creation or cleanup, explicitly switch the active page first
  • then perform top-level board creation or top-level reparenting
  • then re-verify where the shapes actually landed

This matters for future automation because page targeting cannot currently be treated as a purely abstract tree write.

Current Upstream Share Candidates

The best current candidates to share upstream are:

  • a focused plugin/API report about top-level page targeting and cross-page reparenting behavior for live component/main-instance work
  • a focused question or report about just-created token-theme handling on the plugin path, since the first direct theme pass ran into missing activeSets behavior and we fell back to direct set activation

The older slice-shell export/render problem is still worth sharing later, but only after we reduce it to a smaller reproducible case.

Why hold that one a little longer:

  • the new clean slice family works better
  • the old failure now looks tied to a specific recovered component lineage
  • we do not yet have a minimal repro that is clean enough to be a good upstream report

Working Rule

For current Penpot work:

  • treat backend API as the primary live state and export surface
  • treat MCP/plugin as the primary current-file and live Penpot-context surface
  • treat exported .penpot files as explicit checkpoints
  • do not default to whole-page export_shape; prefer board-level or shape-level export until page export is re-verified
  • explicitly target the active page before top-level board creation or top-level reparenting
  • if duplicated component text does not render reliably, prefer overlay text on the detached shell instead of assuming inherited text edits are authoritative
  • keep this comparison note current as new findings emerge