- Move CODEGEN_PROMPT to codegen.md, loaded via raw-md bundler plugin
- Add JSX_REFERENCE as jsx-reference.md with full prop/tag/example docs
- Copy JSX Reference button (book icon) in Code panel header
- Multi-root JSX: try parsing as-is, wrap in fragment on failure
- Component and Instance tag aliases in JSX renderer
- renderJSX returns RenderResult[] to support fragments
- raw-md plugin for both tsdown and Vite
Co-authored-by: sld0Ant <sld0Ant@users.noreply.github.com>
- Use applyStyleToRange from text/style-runs.ts instead of naively
appending runs (which created overlaps and invalid state)
- Apply fontWeight from style name (e.g. 'Bold' → 700)
- Apply color as fills on the style override
- Previously the color param was accepted but silently ignored
Fixes#214
MCP clients often serialize numeric arguments as JSON strings.
Previously this caused validation errors on tools like node_move.
Now both the MCP server (zod) and AI adapter (valibot) coerce
string-encoded numbers to numbers, rejecting only genuinely
non-numeric values like 'abc'.
Fixes#207
create_vector now validates the path JSON before accepting it:
checks vertices have numeric coordinates and segment indices are
in range. Returns a clear error message instead of silently
creating a malformed node that crashes on save.
The scene graph also normalizes vectorNetwork on updateNode as
a safety net for other code paths.
The estimateTextSize() fallback (fontSize × 0.6 × charCount) produced
wildly inaccurate results, causing 26K node mismatches on material3.fig.
Nodes imported from .fig files already carry Figma's computed dimensions —
use those instead. Falls back to the estimate only for newly-created
nodes with the 100×100 default size.
Reduces headless layout mismatches by ~56% (26,299 → ~11,669), with
TEXT node mismatches dropping 92%.
Fixes#212
Co-authored-by: Alexander Bonin <3462438+alex4o@users.noreply.github.com>
* fix(layout): resolve design token variables before passing to yoga-layout
applyPadding() and applyAutoLayout() passed variable references like
$--spacing-lg directly to yoga-layout's setPadding()/setGap() which
expects numeric values, causing a crash.
Now resolves variable references via ctx.resolveNumber() before
assignment, following the same pattern already used by parseSize()
and applyCornerRadius() in the same file.
Fixes open-pencil/open-pencil#201
* test(layout): add regression tests for variable padding/gap resolution (#201)
- Move use-external-link.ts → utils/external-link.ts (plain function, not a composable)
- Remove non-null assertions on providerDef.keyURL (v-if already guards)
- Remove test scaffolding from production global.d.ts
External links (<a target="_blank">) in the AI panel triggered
"Command plugin:shell|open not allowed by ACL" on Tauri desktop.
Root cause: Tauri v2 intercepts <a target="_blank"> via the shell plugin,
but only the opener plugin had ACL permissions. Replace all external links
with programmatic openUrl() from @tauri-apps/plugin-opener, which is already
whitelisted via "opener:default" in capabilities.
- Extract openExternalLink() composable for Tauri/browser switch
- Replace <a target="_blank"> with <button @click> in ProviderSetup/Settings
- Add E2E test verifying window.open is called in browser context
Fixes#193
Co-authored-by: Danila Poyarkov <dev@dannote.net>
Add nodeToXPath() to core — builds a unique XPath selector for any node
by walking up the tree and using name predicates (or positional index
when names collide among same-type siblings). Properly escapes names
containing quotes using XPath concat() or double-quote delimiters.
UI: "Copy node ID" and "Copy XPath" items in the canvas right-click
"Copy/Paste as" submenu. Also i18n the entire submenu — all labels now
use menuMessages with translations for de, es, fr, it, pl, ru, zh-CN.
CLI: `open-pencil selection` command (app-mode only) returns selected
nodes with id, name, type, size, and xpath. Connects to the running
app via the automation WebSocket.
Automation: new `selection` RPC handler that returns selected nodes
with computed XPath selectors.
- Add packages/mcp/src/stdio.ts — proper stdio MCP server that
connects to the running app via WebSocket
- openpencil-mcp bin now points to stdio entry (was HTTP)
- openpencil-mcp-http bin added for the HTTP server
- Extract registerTools() from server.ts to share between transports
- Move index.ts banner output to stderr (prevents stdout corruption)
- Update Tauri shell spawn to use openpencil-mcp-http
- Update all docs (EN + 6 translations): correct binary names, source
paths, port number
Fixes#194
Nudge: Arrow keys move selection by 1px, Shift+Arrow by 10px.
Consecutive nudges within 300ms are batched into a single undo
entry so Cmd+Z undoes the entire sequence at once.
Fix duplicate redo creating nodes with new IDs, breaking
subsequent undo entries that reference the original IDs. Now
snapshotSubtree/restoreSubtree preserve node IDs across redo.
Fix delete undo not restoring children — deleteSelected only
snapshotted top-level nodes, so undoing a frame delete lost all
its children. Now snapshots the full subtree.
Fix draw tool not committing resize to undo — shapes created via
draw had their final dimensions lost on redo because handleDrawUp
never recorded the resize. Now uses beginBatch/commitBatch to
combine create+resize into a single undo entry.
Clean up use-keyboard.ts — remove shouldPreventDefault and opaque
PREVENT_* sets, move tool switch and nudge to dedicated keydown
listener, add shift() helper.
Closes#182
* chore: remove unused pi context file
- Delete .pi/context.md
- Remove an unused local context artifact from the repository
* Add clipboard text v4 export
* fix(clipboard): rebase v4 export branch onto master
- Resolve conflicts after the core text module reorganization
- Fix clipboard text export imports and bundled font asset loading
- Update raw binary expectations for textUserLayoutVersion 4
- Re-run check and unit tests after the rebase
styleRuns were updated on the graph during editing (via syncText) but
commitTextEdit only captured text changes. Now both text and styleRuns
are snapshotted at startTextEditing and restored on undo.
commitTextEdit compared prevText from the scene graph node, but
syncText already updates the node on every keystroke during editing.
This made prevText === newText, so the undo entry was never pushed.
Fix: capture the original text when startTextEditing is called and
use that snapshot for the undo comparison.
Matches Figma behavior: pressing Enter when a single TEXT node is
selected starts text editing mode and selects all text. Uses rAF
to defer text editing start so the Enter keydown event completes
before the textarea is created and focused, preventing the newline
from being inserted into the selected text.
Closes#181
* fix(acp): show install errors for missing agent CLIs (#172)
* Use npm for install commands, toasts for errors, tighten error matching
- Replace bun add -g with npm i -g (universal baseline)
- Replace initError inline banner with toast.show() (consistent with rest of app)
- Remove overly broad 'not found' / 'no such file' from isMissingCommandError
* Refactor toast API to toast.info/warning/error
- Add bezier math utilities for VectorNetwork manipulation
- Enable resuming pen drawing from existing open path endpoints
- Allow closing open paths by dragging endpoints together
- Add comprehensive vector editing capabilities for curves and paths
- Zero transforms for auto-layout children (Figma computes positions)
- Write bordersTakeSpace from strokesIncludedInLayout
- Always write explicit lineHeight, defaulting to ceil(fontSize * 1.2)
- Normalize font family in derivedTextData fontMetaData and fontName
- Extend normalizeFontFamily to strip optical size suffixes (9pt, 18px)
- Extract computeExportTransform() helper for lint compliance
- Add 20 unit tests covering all fixes
Fixes#134
TEXT nodes created via SceneGraph.createNode() defaulted to empty
fills, causing text to be invisible when .fig files are opened in
Figma. Now defaults to a solid black fill matching Figma's behavior.
Closes#133
Strip optical size suffixes (e.g. "DM Sans 9pt" → "DM Sans") and
"Variable" suffixes when writing fontName.family to .fig files.
This ensures Figma recognizes the font instead of showing a
"Missing font" dialog.
Closes#131
Each RPC tool call created a fresh FigmaAPI instance, so switch_page
set currentPageId on a throwaway object. Sync the page change back
to the editor store after tool execution.
Fixes#112
* fix(set_layout): default to HUG sizing when enabling auto-layout
When `set_layout` transitions a frame from `layoutMode: 'NONE'` to
auto-layout, it now sets `primaryAxisSizingMode` and
`counterAxisSizingMode` to `'AUTO'` (which maps to HUG internally).
Previously these remained at `'FIXED'`, causing the frame to keep its
original dimensions instead of shrinking/growing to fit children. This
made `h="hug"`, `justify="end"`, and `grow={1}` appear broken when
containers were created via the `set_layout` MCP tool (as opposed to
the JSX `render` path, which already defaulted to HUG).
The fix only applies when `direction` is provided and the frame was
previously in `layoutMode: 'NONE'` — updating spacing or alignment on
an existing auto-layout frame does not reset sizing modes.
* docs: add changelog entry for set_layout HUG sizing fix