openpencil/README.md

255 lines
13 KiB
Markdown
Raw Normal View History

2026-02-28 05:12:18 +00:00
# OpenPencil
[![MIT license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![npm](https://img.shields.io/npm/v/@open-pencil/cli?label=%40open-pencil%2Fcli)](https://www.npmjs.com/package/@open-pencil/cli)
[![Discord](https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white)](https://discord.gg/4wXc9fuZfm)
[![GitHub Discussions](https://img.shields.io/github/discussions/open-pencil/open-pencil?logo=github&label=Discussions)](https://github.com/open-pencil/open-pencil/discussions)
Open-source design editor. Opens `.fig` and `.pen` design files, includes built-in AI, and ships as a programmable toolkit with a headless Vue SDK for building custom editors.
2026-06-30 06:53:29 +00:00
> **Status:** Active development. Usable today, with some rough edges as features evolve.
**[Try it online →](https://app.openpencil.dev/demo)** · [Download](https://github.com/open-pencil/open-pencil/releases/latest) · [Documentation](https://openpencil.dev) · [Roadmap](https://openpencil.dev/development/roadmap) · [llms.txt](https://openpencil.dev/llms.txt)
2026-03-01 13:03:25 +00:00
![OpenPencil](packages/docs/public/screenshot.png)
2026-02-28 20:36:55 +00:00
2026-03-08 07:47:42 +00:00
## Installation
**macOS (Homebrew):**
```sh
brew install --cask openpencil
2026-03-08 07:47:42 +00:00
```
Or download from the [releases page](https://github.com/open-pencil/open-pencil/releases/latest), or [use the web app](https://app.openpencil.dev) — no install needed.
fix: explain unsupported browsers instead of a blank window (#745) * fix: explain unsupported browsers instead of a blank window The desktop app on macOS 13 with WebKit older than Safari 17.4 opened an empty window because startup called Promise.withResolvers, which Vite lowers nothing for: build.target only rewrites syntax and never polyfills APIs, and the target itself was an implicit Vite default (#744). Make the supported baseline explicit in src/app/shell/support/baseline.ts and feed it to build.target, a lint rule that rejects newer static built-ins in browser-shipped sources, and the documented system requirements. Replace Promise.withResolvers with a createDeferred() helper. Turn src/main.ts into a small gate that checks sentinel features before dynamically importing the app, so an old engine still evaluates enough code to render platform-specific update guidance: macOS/Safari via Software Update, WebKitGTK and WebView2 on Linux and Windows, and each browser's own update path on the web, with a prefilled bug report link. Render-blocking errors during the first route are captured through app.config.errorHandler and shown the same way instead of leaving the window blank. Desktop facts come from tauri-plugin-os and a webview_version command; the bundle now declares macOS 13 as its minimum system version. * build: enforce the browser baseline from compatibility data Replace the hand-maintained list of built-ins newer than the baseline with two data-driven checks. The app and browser-shipped packages pin their TypeScript lib to ES2023, the last edition Chrome 111, Firefox 128 and Safari 16.4 implement in full, so a newer built-in such as Promise.withResolvers fails type-checking. Web APIs, which lib.dom does not version, go through eslint-plugin-compat under oxlint with the same browsers in settings.browsers, scoped to sources that ship to a browser. A unit test keeps the oxlint browser list and the tsconfig libs derived from src/app/shell/support/baseline.ts, so the three cannot drift apart. * fix: recognise production error codes in the boot observer Vue passes the error reference URL as the errorHandler info argument in production builds instead of the development string, so the observer never classified a setup or render failure as fatal in the shipped app and the boot-failure notice only appeared on the dev server. Match Vue's exported ErrorCodes in both forms, and cover the component-setup path in the E2E spec; the scenario was also verified against a production build.
2026-09-22 10:40:59 +00:00
Requires macOS 13 or later with current Safari updates, Windows 10 or later, or Linux with WebKitGTK 2.40+; the web app needs Chrome 111, Edge 111, Firefox 128, or Safari 16.4 or later. See [system requirements](https://openpencil.dev/getting-started#system-requirements).
## What it does
2026-02-28 05:12:18 +00:00
- **Opens `.fig` and `.pen` files** — read and write native Figma files, open supported Pencil documents from the app or OS file browser, copy & paste nodes between apps
- **AI builds designs** — describe what you want in chat, 100+ tools create and modify nodes. Connect OpenRouter, Anthropic, OpenAI, Google AI, DeepSeek, Z.ai, MiniMax, or compatible endpoints
feat: behaviours and preview mode (#893) * feat: author behaviours on main components A main component or component set can behave as a Switch, Checkbox, Slider, or Tabs, after Reka UI's primitives. The behaviour lives in OpenPencil plugin data: boolean values bind to variant or boolean properties with the values meaning on and off, a number keeps its own range since Figma has no number property, and the control's subcomponents bind to the component's slots. A Behaviour section in the properties panel adds, binds, and removes it, each as one undo step, and flags required bindings that are missing. The canvas-only layout's pill becomes a component that preview will reuse. * feat: preview instances with behaviours on the canvas View > Preview (Cmd+Alt+Enter) puts the canvas in preview: a lone canvas switches to the canvas-only layout with a Previewing pill, and a split canvas previews on its own side. Clicking a Switch or Checkbox flips it, dragging a Slider moves its thumb and range, and clicking a Tabs trigger shows its panel. Preview keeps its state on copies of the instances it touched, in a private graph with the document's ids, and the canvas draws those copies in place of the originals, so the document, undo, autosave, and collaborators never see it. Escape or the pill leaves preview, Reset restores every control, and editing shortcuts, labels, and outlines stay off while previewing. * feat: translate behaviour and preview strings; cover preview with an e2e flow * refactor(vue): reuse VariantDefinitionControl for behaviour property options * refactor: split variant actions and preview interactions by domain Variant authoring was one 706-line closure; it is now graph queries (model), undo snapshots (history), property definition edits (definitions), and the editor facade (index). Preview interactions move into play/kinds, one module per control, registered by behaviour kind so a new kind cannot ship without its contract and interaction. Behaviour contracts are keyed by kind. In the Vue SDK, slot and variant authoring controls get their own folders beside component-props and behaviour, and the app's variant section joins slot/ and behaviour/. * refactor: keep the behaviour model in scene-graph's plugin-data registry Master now defines every OpenPencil plugin-data key in one typed registry in scene-graph. The behaviour schema registers there as a field, and the model and contracts move beside slots, exported from the package root; the @open-pencil/core/behaviours subpath is gone. * feat: interaction states and keyboard focus in preview A behaviour can bind a variant property to the default, hover, pressed, focus, and disabled states; binding it maps values named like those states. Preview switches the instance's copy to the matching variant as the pointer hovers, presses, and releases, keeps other values when the set draws the combination and falls back to rest otherwise, and skips disabled instances. Tab moves visible keyboard focus between controls, Space, Enter, arrows, Home, and End use the focused one, and Escape takes visible focus off before leaving preview. A Button kind covers controls that only have states. * feat: toggle, radio, group, progress, collapsible, and accordion behaviours Radio group, toggle group, and accordion hold their items in a slot; each item is an instance with its own behaviour, so a press inside the slot goes to the group, which turns the pressed item on and the others off through the item's own interaction. Progress shares the slider's number handling through rangeControl, and a collapsible shows and hides its content slot from its trigger, remembering its open state even when no property draws it. Tabs and groups share arrow-key navigation. * feat: text field, textarea, and number field behaviours A behaviour value can now be text, bound to a text property, so preview types into a copy of the field through the same property path the editor uses. A bound Filled value switches to the placeholder variant when the field empties. A number field keeps its own range, shows its value through a text property, and steps from its increment and decrement slots and the arrow keys. Text fields show focus from a click, and the focused control receives every key; Option still types, and only Cmd or Ctrl combinations stay shortcuts. * fix: keep behaviour bindings when saving as .fig Saving as .fig gives component properties new GUIDs, but behaviours kept the old ids in their plugin data, so every binding read as missing after reopening. The export now renames the ids behaviours bind with the same GUIDs, on its own copy of the document. * fix: let previewed controls resize layout imported from .fig Layers from a .fig keep the sizes Figma computed, and auto layout prefers them, so an opened collapsible or accordion item kept its closed height in preview. When preview shows, hides, or retypes a layer in a copy, it drops those sizes from the layer's copied ancestors so auto layout sizes them again; untouched layers keep Figma's sizes. * fix: publish behaviours and other plugin content with library assets Every OpenPencil plugin-data field now declares its role: content that exists only as plugin data (behaviours, OkHCL picks), format copies of node fields written for files, or bookkeeping about where a document or node came from. Library snapshots keep a node's content plugin data, including other plugins' entries, and drop the rest; the asset hash counts the same entries, so a behaviour-only change is offered as an update while a .fig round trip still changes nothing. * feat: name behaviour rows by meaning and create what they need The Behaviour section named every main value "Value" under a "Values" heading, and a component without matching properties left an empty picker with no way forward. Rows are now named for the control (On, Checked, Pressed, Text), rows the control needs or already uses come first, and the optional rest folds under More options; a button keeps its states in view. An empty row creates what it needs in one undo step: a text layer and text property, Off and On variants on a set, or a slot frame for a part. The missing chip names the row it means and takes you there. * fix(dom-css): position free layers, hug content, and round ellipses HTML and Tailwind export stacked the layers of frames without auto layout in block flow, wrote fixed pixel sizes for auto layout frames set to Hug and for auto-sizing text, and drew ellipses as boxes. Layers a parent does not lay out are now absolutely positioned at their coordinates inside a relative frame, hugging axes are left to the content, and ellipses get a 50% radius. * feat: run preview as live Reka UI islands over the canvas Preview simulated controls on the canvas: copies of instances, a handler per kind, its own key routing, and append-only text. It now runs them as real components. Each top-level layer that holds an instance with a behaviour becomes an island: its layers are projected to DOM through dom-css into a shadow root laid over the pane at its pan and zoom, and each behaviour mounts its Reka UI primitives on its layers, so text fields are real inputs and focus, keys, and layout are the browser's. Core's resolvePlayState shows instances in a state on a private graph, so the component's variants draw it, and controls are keyed by layer path so a variant switch keeps their DOM. The canvas leaves island layers to the islands, and the canvas play runtime and its key routing are gone. * fix: derive variant properties from Property=Value component names figma.combineAsVariants and Combine as variants only derived variant properties from slash-separated names, so components named as Figma names variants, such as State=On, Size=Large, became a set with no properties. Both now derive each named property and its values, after the slash form. * feat: script and tool access to behaviours by name Behaviour contracts follow Reka UI's anatomy: tabs keep their triggers in the list slot and their content panels in a panels slot, and a slot of repeated parts names the Reka part of its children. A behaviour spec names component properties and slots instead of ids and resolves to the stored behaviour and back, with errors that list what the component has. Scripts get an `openpencil` global next to `figma`, in the Figma API's style: setBehaviour, getBehaviour with bindValue, bindPart, states, and missing, behaviourKinds, and createSlot. The eval tool, the CLI, and app automation compile scripts through one compileScript, so the CLI now returns the last expression as the others do. MCP and AI chat get set_behaviour, get_behaviour, and create_slot. * feat: write controls in design JSX with Reka UI's element names `<Switch.Root modelValue="State">` renders a main component, or a set when its children are variants, that behaves as a switch, and `<Switch.Thumb>` the slot that draws its thumb, one slot across the set's variants. Inputs become the text property of a field, tab triggers and panels go in their List and Panels slots, and a group's items are `<RadioGroup.Item of={…} />` instances in its Items slot. JSX export writes components with behaviours the same way, so they render back unchanged. The authoring reference documents controls, and the codegen and chat prompts now include it verbatim instead of dedenting its code examples. * chore: format the CLI export test * docs: document slots, behaviours, preview, and the openpencil API The components guide covers slots, behaviours, and preview with its shortcut; scripting covers the openpencil global and eval's last- expression result; the MCP and AI chat pages list the new tools; the features overview, README, and roadmap mention working controls. The chat prompt says how to build a control, and the codegen prompt builds components with behaviours on their Reka UI primitives. * chore: format the eval CLI test * docs: explain behaviours and preview islands, and guide the openpencil API A development page explains the behaviour model, the four authoring surfaces, how preview islands turn a control's state into live Reka UI components, and how to add a kind; the architecture page links it. The Core guide sets the rules for OpenPencilAPI: Figma-only `figma`, OpenPencil features on `openpencil` in the same style, one compileScript, names over ids, and docs with every member. Package READMEs mention the openpencil global, PlayIslands, Reka-named JSX, and the behaviour model. Design JSX's behaviour modules move into a behaviours folder instead of a suffixed sibling. * refactor: center pasted layers through translate centerNodesAt repeated translate's loop, which test:dupes reports on master too. * fix: validate behaviour ranges and guess on and off by name A number value now needs max above min and a positive step: the schema, specs, and the panel reject a range a slider cannot step through. Binding a variant property guesses on and off by value name, as specs do, and a boolean property gets no on/off pair. Part bindings are read through partBinding, a replaced document restarts preview from its designed state, and the e2e preview shortcut uses ControlOrMeta. * feat: make the Behaviour section say what to do next A slider's range fields now carry inline Min, Max, Step, and Start labels. States offers Add state variants, which adds a Default, Hover, Pressed, Focus, and Disabled variant and binds them; Add Off and On variants and Add state variants turn a lone main component into a component set first, and a part's slot can be added to a set, in every variant under one slot id. Rows that could do nothing are gone: no empty pickers and no hints to combine variants by hand, and an unbound Disabled is left to the states. A warning line names what is still needed and replaces the missing chip, and the Switch's main value is called Checked. * fix: keep each slot to one part and keep creating slots at hand A slot draws one part, so the Behaviour section no longer offers a slot another part uses, and specs (the openpencil API, tools, and JSX) reject binding one slot to two parts. A part's picker keeps an action to add a new slot in its footer, so adding the first slot no longer hides it for the other parts.
2026-10-06 13:23:05 +00:00
- **Fully programmable** — headless CLI, XPath queries, Figma Plugin API plus an `openpencil` API via `eval`, MCP server for AI agents, and desktop agent integrations for Claude Code, Codex, and Gemini CLI
2026-03-31 15:34:01 +00:00
- **Lint, convert, and extract tokens** — inspect documents, lint naming/layout/accessibility, convert between supported formats, analyze colors/typography/spacing/clusters, and extract design tokens
- **Components and variants** — create reusable components, group variants into component sets, insert local assets as instances, and switch variants from the inspector
feat: behaviours and preview mode (#893) * feat: author behaviours on main components A main component or component set can behave as a Switch, Checkbox, Slider, or Tabs, after Reka UI's primitives. The behaviour lives in OpenPencil plugin data: boolean values bind to variant or boolean properties with the values meaning on and off, a number keeps its own range since Figma has no number property, and the control's subcomponents bind to the component's slots. A Behaviour section in the properties panel adds, binds, and removes it, each as one undo step, and flags required bindings that are missing. The canvas-only layout's pill becomes a component that preview will reuse. * feat: preview instances with behaviours on the canvas View > Preview (Cmd+Alt+Enter) puts the canvas in preview: a lone canvas switches to the canvas-only layout with a Previewing pill, and a split canvas previews on its own side. Clicking a Switch or Checkbox flips it, dragging a Slider moves its thumb and range, and clicking a Tabs trigger shows its panel. Preview keeps its state on copies of the instances it touched, in a private graph with the document's ids, and the canvas draws those copies in place of the originals, so the document, undo, autosave, and collaborators never see it. Escape or the pill leaves preview, Reset restores every control, and editing shortcuts, labels, and outlines stay off while previewing. * feat: translate behaviour and preview strings; cover preview with an e2e flow * refactor(vue): reuse VariantDefinitionControl for behaviour property options * refactor: split variant actions and preview interactions by domain Variant authoring was one 706-line closure; it is now graph queries (model), undo snapshots (history), property definition edits (definitions), and the editor facade (index). Preview interactions move into play/kinds, one module per control, registered by behaviour kind so a new kind cannot ship without its contract and interaction. Behaviour contracts are keyed by kind. In the Vue SDK, slot and variant authoring controls get their own folders beside component-props and behaviour, and the app's variant section joins slot/ and behaviour/. * refactor: keep the behaviour model in scene-graph's plugin-data registry Master now defines every OpenPencil plugin-data key in one typed registry in scene-graph. The behaviour schema registers there as a field, and the model and contracts move beside slots, exported from the package root; the @open-pencil/core/behaviours subpath is gone. * feat: interaction states and keyboard focus in preview A behaviour can bind a variant property to the default, hover, pressed, focus, and disabled states; binding it maps values named like those states. Preview switches the instance's copy to the matching variant as the pointer hovers, presses, and releases, keeps other values when the set draws the combination and falls back to rest otherwise, and skips disabled instances. Tab moves visible keyboard focus between controls, Space, Enter, arrows, Home, and End use the focused one, and Escape takes visible focus off before leaving preview. A Button kind covers controls that only have states. * feat: toggle, radio, group, progress, collapsible, and accordion behaviours Radio group, toggle group, and accordion hold their items in a slot; each item is an instance with its own behaviour, so a press inside the slot goes to the group, which turns the pressed item on and the others off through the item's own interaction. Progress shares the slider's number handling through rangeControl, and a collapsible shows and hides its content slot from its trigger, remembering its open state even when no property draws it. Tabs and groups share arrow-key navigation. * feat: text field, textarea, and number field behaviours A behaviour value can now be text, bound to a text property, so preview types into a copy of the field through the same property path the editor uses. A bound Filled value switches to the placeholder variant when the field empties. A number field keeps its own range, shows its value through a text property, and steps from its increment and decrement slots and the arrow keys. Text fields show focus from a click, and the focused control receives every key; Option still types, and only Cmd or Ctrl combinations stay shortcuts. * fix: keep behaviour bindings when saving as .fig Saving as .fig gives component properties new GUIDs, but behaviours kept the old ids in their plugin data, so every binding read as missing after reopening. The export now renames the ids behaviours bind with the same GUIDs, on its own copy of the document. * fix: let previewed controls resize layout imported from .fig Layers from a .fig keep the sizes Figma computed, and auto layout prefers them, so an opened collapsible or accordion item kept its closed height in preview. When preview shows, hides, or retypes a layer in a copy, it drops those sizes from the layer's copied ancestors so auto layout sizes them again; untouched layers keep Figma's sizes. * fix: publish behaviours and other plugin content with library assets Every OpenPencil plugin-data field now declares its role: content that exists only as plugin data (behaviours, OkHCL picks), format copies of node fields written for files, or bookkeeping about where a document or node came from. Library snapshots keep a node's content plugin data, including other plugins' entries, and drop the rest; the asset hash counts the same entries, so a behaviour-only change is offered as an update while a .fig round trip still changes nothing. * feat: name behaviour rows by meaning and create what they need The Behaviour section named every main value "Value" under a "Values" heading, and a component without matching properties left an empty picker with no way forward. Rows are now named for the control (On, Checked, Pressed, Text), rows the control needs or already uses come first, and the optional rest folds under More options; a button keeps its states in view. An empty row creates what it needs in one undo step: a text layer and text property, Off and On variants on a set, or a slot frame for a part. The missing chip names the row it means and takes you there. * fix(dom-css): position free layers, hug content, and round ellipses HTML and Tailwind export stacked the layers of frames without auto layout in block flow, wrote fixed pixel sizes for auto layout frames set to Hug and for auto-sizing text, and drew ellipses as boxes. Layers a parent does not lay out are now absolutely positioned at their coordinates inside a relative frame, hugging axes are left to the content, and ellipses get a 50% radius. * feat: run preview as live Reka UI islands over the canvas Preview simulated controls on the canvas: copies of instances, a handler per kind, its own key routing, and append-only text. It now runs them as real components. Each top-level layer that holds an instance with a behaviour becomes an island: its layers are projected to DOM through dom-css into a shadow root laid over the pane at its pan and zoom, and each behaviour mounts its Reka UI primitives on its layers, so text fields are real inputs and focus, keys, and layout are the browser's. Core's resolvePlayState shows instances in a state on a private graph, so the component's variants draw it, and controls are keyed by layer path so a variant switch keeps their DOM. The canvas leaves island layers to the islands, and the canvas play runtime and its key routing are gone. * fix: derive variant properties from Property=Value component names figma.combineAsVariants and Combine as variants only derived variant properties from slash-separated names, so components named as Figma names variants, such as State=On, Size=Large, became a set with no properties. Both now derive each named property and its values, after the slash form. * feat: script and tool access to behaviours by name Behaviour contracts follow Reka UI's anatomy: tabs keep their triggers in the list slot and their content panels in a panels slot, and a slot of repeated parts names the Reka part of its children. A behaviour spec names component properties and slots instead of ids and resolves to the stored behaviour and back, with errors that list what the component has. Scripts get an `openpencil` global next to `figma`, in the Figma API's style: setBehaviour, getBehaviour with bindValue, bindPart, states, and missing, behaviourKinds, and createSlot. The eval tool, the CLI, and app automation compile scripts through one compileScript, so the CLI now returns the last expression as the others do. MCP and AI chat get set_behaviour, get_behaviour, and create_slot. * feat: write controls in design JSX with Reka UI's element names `<Switch.Root modelValue="State">` renders a main component, or a set when its children are variants, that behaves as a switch, and `<Switch.Thumb>` the slot that draws its thumb, one slot across the set's variants. Inputs become the text property of a field, tab triggers and panels go in their List and Panels slots, and a group's items are `<RadioGroup.Item of={…} />` instances in its Items slot. JSX export writes components with behaviours the same way, so they render back unchanged. The authoring reference documents controls, and the codegen and chat prompts now include it verbatim instead of dedenting its code examples. * chore: format the CLI export test * docs: document slots, behaviours, preview, and the openpencil API The components guide covers slots, behaviours, and preview with its shortcut; scripting covers the openpencil global and eval's last- expression result; the MCP and AI chat pages list the new tools; the features overview, README, and roadmap mention working controls. The chat prompt says how to build a control, and the codegen prompt builds components with behaviours on their Reka UI primitives. * chore: format the eval CLI test * docs: explain behaviours and preview islands, and guide the openpencil API A development page explains the behaviour model, the four authoring surfaces, how preview islands turn a control's state into live Reka UI components, and how to add a kind; the architecture page links it. The Core guide sets the rules for OpenPencilAPI: Figma-only `figma`, OpenPencil features on `openpencil` in the same style, one compileScript, names over ids, and docs with every member. Package READMEs mention the openpencil global, PlayIslands, Reka-named JSX, and the behaviour model. Design JSX's behaviour modules move into a behaviours folder instead of a suffixed sibling. * refactor: center pasted layers through translate centerNodesAt repeated translate's loop, which test:dupes reports on master too. * fix: validate behaviour ranges and guess on and off by name A number value now needs max above min and a positive step: the schema, specs, and the panel reject a range a slider cannot step through. Binding a variant property guesses on and off by value name, as specs do, and a boolean property gets no on/off pair. Part bindings are read through partBinding, a replaced document restarts preview from its designed state, and the e2e preview shortcut uses ControlOrMeta. * feat: make the Behaviour section say what to do next A slider's range fields now carry inline Min, Max, Step, and Start labels. States offers Add state variants, which adds a Default, Hover, Pressed, Focus, and Disabled variant and binds them; Add Off and On variants and Add state variants turn a lone main component into a component set first, and a part's slot can be added to a set, in every variant under one slot id. Rows that could do nothing are gone: no empty pickers and no hints to combine variants by hand, and an unbound Disabled is left to the states. A warning line names what is still needed and replaces the missing chip, and the Switch's main value is called Checked. * fix: keep each slot to one part and keep creating slots at hand A slot draws one part, so the Behaviour section no longer offers a slot another part uses, and specs (the openpencil API, tools, and JSX) reject binding one slot to two parts. A part's picker keeps an action to add a new slot in its footer, so adding the first slot no longer hides it for the other parts.
2026-10-06 13:23:05 +00:00
- **Working controls** — give a component a Reka UI behaviour (switch, slider, tabs, text field, …) and preview it live over the canvas, with real inputs, focus, and keyboard
- **Image vectorization** — convert image layers into editable vector layers with Recraft or fal.ai
2026-03-31 15:34:01 +00:00
- **Design-to-code export** — export selections as JSX/Tailwind, generate token outputs, and map designs into component-oriented code workflows
- **Vue SDK for custom editors** — headless components and composables for embedding OpenPencil into other apps or building workflow-specific editing surfaces. [Read the SDK docs →](https://openpencil.dev/programmable/sdk/)
- **Real-time collaboration** — P2P via WebRTC, no server, no account. Cursors, presence, follow mode
- **Auto layout & CSS Grid** — flex and grid layout via Yoga WASM, with gap, padding, alignment, track sizing
- **~15 MB desktop app** — Tauri v2 for macOS, Windows, Linux. Also runs in the browser as a PWA
## CLI
```sh
npm install -g @open-pencil/cli
# or: bun add -g @open-pencil/cli
```
### Inspect design files
Browse node trees, search by name or type, dig into properties — all without opening the editor:
```sh
openpencil tree design.fig
openpencil find design.pen --type TEXT
openpencil node design.fig --id 1:23
openpencil info design.fig
```
```
[0] [page] "Getting started" (0:46566)
[0] [section] "" (0:46567)
[0] [frame] "Body" (0:46568)
[0] [frame] "Introduction" (0:46569)
[0] [frame] "Introduction Card" (0:46570)
[0] [frame] "Guidance" (0:46571)
```
### Query with XPath
Find nodes by type, attributes, and structure:
```sh
openpencil query design.fig "//FRAME[@width < 300]" # Frames under 300px
openpencil query design.fig "//SECTION//TEXT" # Text inside sections
```
### Export, convert, and import
Render to PNG, JPG, WEBP, SVG, PDF, PPTX, HTML, JSX, Storybook stories, or `.fig`, convert between document formats, and turn HTML/CSS into editable layers:
```sh
openpencil export design.fig # PNG
openpencil export design.fig -f jpg -s 2 # JPG at 2x
openpencil export design.fig -f tailwind-jsx # Tailwind JSX
openpencil export design.fig -f storybook # Storybook stories per component
openpencil convert design.pen design.fig # Between document formats
openpencil import card.html --css card.css # HTML/CSS → editable .fig
2026-06-03 17:36:27 +00:00
```
### Lint and analyze
2026-06-03 17:36:27 +00:00
Catch naming, layout, and accessibility issues, and audit a design system's real palette, type scale, spacing, and repeated components:
```sh
openpencil lint design.fig --preset strict # Naming, layout, accessibility
openpencil analyze colors design.fig # Also typography, spacing, clusters
openpencil variables design.fig # Variables and collections
```
```
#1d1b20 ██████████████████████████████ 17155×
#49454f ██████████████████████████████ 9814×
#ffffff ██████████████████████████████ 8620×
#6750a4 ██████████████████████████████ 3967×
```
### Script with the Figma Plugin API
`eval` runs JavaScript against the document with Figma's Plugin API; `-w` writes the result back:
```sh
openpencil eval design.fig -c "figma.currentPage.selection.forEach(n => n.opacity = 0.5)" -w
```
### Control the running app
Omit the file argument and the CLI works on the document open in the editor:
```sh
openpencil tree # Inspect the live document
openpencil documents list # Also open, new, save, close, activate
openpencil tool call get_selection # Run any MCP tool
openpencil undo # Undo the newest automation change
openpencil settings set appearance.theme light # Change editor settings
```
Every command supports `--json`. See the [CLI reference](https://openpencil.dev/reference/cli) for all commands and options.
## AI & MCP
### Built-in chat
Press <kbd>⌘</kbd><kbd>J</kbd> (<kbd>Ctrl</kbd><kbd>J</kbd> on Windows and Linux) to open the AI assistant. It has 100+ tools that can create shapes, set fills and strokes, manage auto-layout, work with components and variables, run boolean operations, analyze design tokens, and export assets. Bring your own API key for OpenRouter, Anthropic, OpenAI, Google AI, DeepSeek, Z.ai, MiniMax, or compatible endpoints. No backend, no account.
Not every provider works in the browser, and not every model streams tool calls correctly. See [BYOK provider & model compatibility](packages/docs/programmable/byok-provider-compatibility.md) for measured results — contributions welcome.
### Coding agents (desktop)
Use Claude Code, Codex, or Gemini CLI directly in the chat panel. The agent connects to the editor's MCP server and uses all 100+ design tools. Requires the desktop app and the agent CLI installed locally.
feat(ai): add guided AI setup for providers, coding agents, and Pi (#916) * feat(storybook): prototype guided AI setup and task assignments * refactor(storybook): adopt shared control foundations * refactor(storybook): build AI setup on current settings foundations Move the prototype to settings/ai-setup and compose SettingsSection, SettingsGroup, SettingsRow, AppAlert, AppBadge, and AppCheckbox instead of local section, status, and badge markup. Replace the nonexistent danger color and raw amber with the error and warning tokens. * feat(ui): add a shared radio group AppRadioGroup wraps the Reka radio group with typed options, labels each radio by its option text with any description as its accessible description, and supports all arrow keys unless an orientation is set. The AI setup wizard uses it for the spending choice, named by the step heading, and shares the choice card style with its checkboxes. * fix(ui): draw unchecked checkboxes on the field background AppCheckbox filled its box with the surface (text) color, so unchecked boxes were nearly black in the light theme and nearly white in the dark theme. Use the panel field background and accent hover border shared with the radio group and switch. * refactor(storybook): drop the simplified AI connections panel AI setup has two modes: skippable guided onboarding for most people and the existing advanced settings for power users, both editing the same model settings. Remove the third, simplified connections and tasks panel. The wizard's Advanced settings action and the returning-user screen now stand in for ModelsPanel, which offers Run guided setup. Removing Gateway's own Back button also fixes the blank screen it led to. * feat(ai): plan guided AI setup from the model catalog planOnboarding proposes design and vision models from the access a person already has, using the real provider and agent catalog, and falls back to OpenRouter only when pay-as-you-go is allowed. applyOnboardingPlan merges a confirmed plan into the model settings, reusing matching connections and profiles, keeping roles it was not asked about, and dropping only the empty fresh-install profile. * refactor(ai): share model provider display names Move the provider, agent, and Pi display-name lookup out of the model settings workflow so guided setup can reuse it. * feat(ai): offer guided AI setup over the real model settings Guided setup asks what AI should help with, what access the person already has, and whether pay-as-you-go models are allowed, then proposes design and vision models from the provider and agent catalog. Connections reuse the provider key field and connection test, keys are saved through the credential manager, and the confirmed plan is merged into the model settings, keeping anything configured by hand. Saving reports saved, partial, or failed like the profile editor. A fresh install whose model settings are still the empty placeholder is offered setup once with a skippable welcome; existing setups never see it. Settings → AI & agents can run it again, and Advanced settings hands off to the model editor. The Storybook fixtures for agents, OpenRouter sign-in, Vercel AI Gateway, and the local server are replaced by the real flow, with all copy translated. Browser tests start with the offer dismissed through the shared Playwright storage state; the first-run spec clears it. * fix(ai): keep configured models and credentials safe in guided setup Running guided setup again planned from the catalog defaults, so it replaced hand-configured design and vision models and dropped their settings; it now keeps a configured model while its access is still selected or onboarding cannot offer that provider, and keeps vision when nothing new covers it. Reused profiles must have the capabilities the plan relies on, and an explicit vision assignment without image input is cleared. A server's saved key and its status now apply only to the connection at the address being entered, and its connection test uses that connection's API type. Entered keys are copied before saving, so closing setup mid-save no longer drops them, and the Models list refreshes key status after setup saves a key. Servers that do not check keys get a hint to enter any value, and a step that only keeps configured models says so instead of showing nothing. * test(ai): check key status right after guided setup The Models list must show a key saved by guided setup as connected without reopening Settings. * feat(ai): sign in to OpenRouter from guided setup OpenRouter can now be connected with its OAuth PKCE flow instead of a pasted key. In the browser, sign-in opens in a popup that returns to a static callback page on the app's origin, which relays the redirect to the editor over a BroadcastChannel, so the editor never navigates away. The desktop app opens the system browser and receives the redirect on a one-shot 127.0.0.1 listener, the localhost callback OpenRouter documents. Either way the editor checks the state, exchanges the code for a key directly with OpenRouter, fills it in, and runs the connection test. Waiting, blocked pop-ups, cancellation, expiry, and failures are reported in the step, which keeps the pasted-key path. Setup no longer offers Clear for a saved key, since removing keys belongs to the advanced settings, and the service worker leaves /oauth/ pages to the network. * fix(settings): report unreadable model keys as unavailable A saved key the browser credential store could not read, for example one left from an older session on the same origin, rejected the model status refresh and the startup credential check, which surfaced as a global error toast. Each read failure now marks only that connection as unavailable. * feat(ai): map every role in guided setup and verify OpenRouter sign-in Guided setup now proposes a model for design, vision, review, and fast work, and the review step is a map of those roles with every suitable model from the connected providers and a "Use recommended setup" shortcut. Fast work defaults to the provider's catalog model tagged as fast; behind an agent, review and fast work use the API model chosen for vision. Review and fast work are never asked about, so a configured choice, including none, stays unless it follows a design model it can no longer follow. The pay-as-you-go question only appears when the access already selected leaves a requested role without a model, so choosing OpenRouter or another account no longer asks it. After signing in with OpenRouter, setup checks the key with OpenRouter's key endpoint, which costs no credits, instead of a text generation test. The step then says it is signed in, names the key, warns when the account has no credits yet, and offers another account in place of the key field and test button. * feat(ai): offer OpenRouter only for goals nothing selected covers The separate pay-as-you-go step asked an abstract question even when the selected access already covered every goal. The connect step now names a goal nothing selected can cover, such as visual review behind a coding agent or a local server, and offers to add OpenRouter for it; once added, it says OpenRouter fills the gap and can be removed again. Setup can finish without visual review, but not without a design model. A local or company server can be marked as able to read images, which lets it cover visual review and makes it the preferred vision model over a paid account. * feat(ai): show provider logos and more providers in guided setup Guided setup shows monochrome logos for coding agents, API accounts, and local servers, from LobeHub's MIT-licensed static SVG set loaded as an `ai` icon collection, so they follow the theme like Lucide icons. DeepSeek, Z.ai, and MiniMax are offered under "More providers", and a local server can start from the Ollama or LM Studio address instead of typing it. * feat(ai): guide coding agent setup in guided setup Choosing Claude Code, Codex, or Gemini CLI in the desktop app now checks whether the agent's ACP program and OpenPencil's MCP server, which agents use to reach the canvas, are installed. An allowlisted agent_lookup command finds the program on the same widened PATH as the MCP lookup. The card shows install commands only for what is missing, checks again on request, links a new setup guide, and copies a prompt that asks an agent the person already uses to install both, confirm they are on PATH, and sign in. In the browser, the agent section links to the desktop app. * fix(ai): space the More providers toggle like a group heading The toggle sat flush against the account cards above and below it; it now reads as a group heading with the same rhythm as the other sections. * feat(ai): detect and install coding agents in guided setup Adopt the local agent discovery from #847. A desktop agent_lookup command reports each agent's own CLI, its ACP adapter, npm, and OpenPencil's MCP server on the widened PATH without starting any of them, and the app can install a missing adapter or the MCP server with npm, limited by the shell capability to those exact packages and the MCP version that matches the app. Guided setup now tells "installed but the OpenPencil adapter is missing" apart from "not installed", offers one-click installs, links each vendor's own setup guide, and keeps the manual commands and setup prompt for the browser, missing npm, or a failed install. Codex install instructions move to @agentclientprotocol/codex-acp, which replaces @zed-industries/codex-acp. Kiro CLI support from the same pull request is left for a separate change, since it needs ACP transport work. Co-authored-by: GitttHomie <134371845+GitttHomie@users.noreply.github.com> * build(app): resolve LobeHub icons with import.meta.resolve The architecture lint forbids createRequire in ESM build code. * test(app): seed AI setup specs through storageState Follows the storage seeding used by other browser specs and the import type rule. * feat(ai): set up Pi with its own sign-ins in guided setup Pi now runs with the providers signed in to in the Pi CLI and Pi's default model. The Harness companion reuses ~/.pi/agent; the app reads only Pi's settings.json for the default model and never auth.json. A saved key is still used as an AI Gateway key. Guided setup offers Pi next to the other coding agents. On the desktop it checks the Harness companion, the MCP server, and Pi's default model, and installs the companion with one click through npm. Agent discovery now reads the installed versions of the MCP server and the Harness companion from their package.json without starting them. Setup flags a version that does not match the app and shows the update command for the package manager that installed it, instead of reporting the server as installed and failing at the first message. * feat(ai): check agent companions before a chat starts A Pi chat without the Harness companion, or any agent chat whose companion or MCP server version does not match the app, failed when the process started and showed only the generic request error. The chat now checks the companions through agent discovery first and names the fix, with an action that opens guided setup. Pi sign-in and model problems use the same path. The Pi model editor shows the same companion, MCP server, and default model status as guided setup, and no longer requires a model ID, since Pi falls back to the default model set in Pi. Supersedes the companion detection in #566, which ran the companion to read its version and required an exact version match. * fix(harness): start Pi sessions with MCP tools and keep unsent messages Pi chats in the desktop app always configure OpenPencil's MCP server, and three companion problems stopped them: - @ai-sdk/harness-pi imports pi-mcp-adapter, which publishes TypeScript sources. Node refuses to strip types under node_modules, so the companion now strips them through a module load hook limited to TypeScript dependencies. Bun runs them as is. - pi-mcp-adapter imports @earendil-works/pi-tui, declared only as an optional peer, so npm left it out. The companion depends on it at the version pi-coding-agent uses. - Pi reports live-process resume, yet the service handed it state saved by an earlier session, and the just-bash sandbox cannot resume, so every later session with that ID failed. Live-process backends now start fresh and drop saved state. When a chat cannot start, the composer now keeps the typed message instead of discarding it. * fix(harness): keep companion stdout for protocol messages Pi prepares the packages listed in a person's Pi settings with npm, which inherits the companion's stdout, and libraries log through console.log. Both landed in the JSONL protocol stream, where the app discarded them with warnings. The companion now keeps the real stdout for protocol messages, sends other stdout writes to stderr, and quiets npm on success through its environment. The type-stripping hook no longer prints Node's experimental warning, and the app logs companion stderr as diagnostics rather than errors, since failures arrive as protocol errors. Document Pi in the coding agents guide, the AI chat page, and the README: guided setup installs the companion, Pi uses the Pi CLI's sign-ins and default model, an AI Gateway key is optional, and the companion needs Node.js 22.15 or later. * test(harness): keep the pi-tui pin in step with pi-coding-agent The companion depends on pi-tui only because pi-mcp-adapter imports it while declaring it optional (nicobailon/pi-mcp-adapter#805). Upgrading @ai-sdk/harness-pi moves pi-coding-agent, and a pin left behind would make npm install a second, mismatched pi-tui. The test fails until the pin matches. * test(ai): follow the model catalog in guided setup tests The plan and apply tests repeated catalog default and fast model IDs, so master's model update broke them without any change in setup behavior. They now read those models from the catalog. * test(ai): keep the model catalog helper with the shared test helpers Unit test homes under tests/app accept only *.test.ts files, so the onboarding tests' catalog helper moves to tests/helpers/ai. * docs: tighten the guided setup and Pi changelog entries Name every provider and server preset guided setup offers, describe the role step as it now works, and shorten the Pi entry. * test(ai): type the guided setup test stubs for the test typecheck Master now typechecks the test suites: fetch fakes go through fetchStub, the chat ref is shallow like the real one, and mocks declare the arguments the tests inspect. * test: type the tabs module in the closed-documents spec The spec imported the tabs module by its served URL without a type, which fails the test type check on master. * test(ai): assert outcomes instead of copy in guided setup tests Drop the setup-prompt test, which checked prompt prose, the onboarding wrapper cases that restated discovery, and the coversGoals case. Story plays and the OpenRouter E2E flow now assert controls and saved models instead of sentences and catalog model names, and the fast-model helper checks the planned model's catalog entry instead of recomputing the choice. tests/AGENTS.md states the rule. * feat(ai): return desktop OpenRouter sign-in through a deep link The desktop app ran a hand-written HTTP server on a localhost port to receive OpenRouter's redirect, and OpenRouter labels apps with a localhost callback by host and port. OpenRouter now redirects to a page on the web app that opens openpencil://oauth/openrouter with the same query, and the desktop shell forwards that link to the webview as an oauth-callback event. The attempt that started sign-in checks the state and exchanges the code with its PKCE verifier, which never leaves the app. * feat(ai): ask OpenPencil's companions for their version The desktop app read a companion's version by following its executable's symlink up to a package.json. That only worked for the Unix npm and bun layouts: Windows .cmd and .exe shims and version-manager shims such as Volta and mise are not links into the package, so the version was always unknown and an outdated companion went unreported. The MCP server, stdio bridge, and Harness companion now print their version for --version, and the app runs each installed one with --version --help under a timeout. A release older than --version prints its help or exits without a version line, which reads as outdated. A bun global install on Windows now gets the bun update command too. * fix(ai): ask for a Pi sign-in when Pi has none readPiAccount returned an account whenever a home folder existed, so a chat with no Pi sign-in reached the Harness and failed with a provider error instead of the guided pi-sign-in fix. It now reports whether Pi's auth.json exists, without reading it, and the capability allows that one check. * fix(ai): keep the attachments of a message that was not sent A message that never reached the chat came back to the composer as text only: its image previews were revoked and its referenced layers dropped. The composer now takes back the whole submission, or releases the previews when newer text replaced it. A message counts as sent once the chat holds it, so a failure after that no longer hands it back to be sent twice. * refactor(app): read the app version from one constant Four modules each derived the app version from the build define with the same test fallback. * refactor(ai): report chat submission errors from their own module Reverting turns from master and keeping unsent drafts together took useChatSubmission past the composition-root limit. The test for reverted turns now passes the setup messages the submission reports. * refactor(app): keep the app version with the runtime config Tools typecheck src/constants.ts through app imports without the Vite defines, so the version constant moves to src/app/runtime/version.ts. * test(desktop): check npm installs of the companions at the app's release version The scope test named the companion packages and version literally, so it would keep passing if the app requested something else. It now builds them from the app's package names and the release version a build embeds. * refactor(ai): parse OpenRouter, Pi, and sign-in callback data with Valibot The OpenRouter key info and code exchange checked their JSON with typeof chains, Pi's settings parsed JSON in a try before validating it, and the desktop sign-in trusted the shell's callback payload as typed. Each now goes through one schema. * fix(ai): take back a message whose images could not be prepared A message with images appears in the chat before its images are prepared, so a preparation failure counted as sent: the draft did not come back and its previews were already revoked. A message now counts as sent once it is dispatched; a failure before that removes the shown message and hands the draft back, and the composer's previews are revoked only after dispatch. * fix(ai): restore an unsent message only in its own conversation Switching conversations while a message was being sent could restore it into the newly opened one. The draft now comes back only if the conversation is unchanged, and its previews are released otherwise. * refactor(app): keep one app version constant The update window added an APP_VERSION to src/constants.ts beside the one in src/app/runtime/version.ts. The tools typecheck reaches src/constants.ts without the Vite defines, so the update window now reads the runtime one. --------- Co-authored-by: GitttHomie <134371845+GitttHomie@users.noreply.github.com>
2026-10-07 08:46:57 +00:00
Pi works too: choose it in guided setup (**Settings → AI & agents → Run guided setup**), which installs the `@open-pencil/harness` companion with one click and uses the providers you signed in to in Pi. The companion needs Node.js 22.15 or later; see [Coding agents](packages/docs/programmable/coding-agents.md#pi).
**Setup (Claude Code):**
1. Install the ACP adapter: `npm install -g @agentclientprotocol/claude-agent-acp`
2. Add MCP permission to `~/.claude/settings.json`:
```json
{
"permissions": {
"allow": ["mcp__open-pencil__*"]
}
}
```
3. Open the desktop app → <kbd>⌘</kbd><kbd>J</kbd> → select **Claude Code** from the provider dropdown
### MCP server
Connect Claude Code, Cursor, Windsurf, or any MCP client to inspect, modify, and export design documents headlessly. 100+ tools. [Full docs →](https://openpencil.dev/programmable/mcp-server)
**Stdio** (Claude Code, Cursor, Windsurf):
```sh
npm install -g @open-pencil/mcp
claude mcp add --scope user open-pencil -- openpencil-mcp
```
For other MCP clients:
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
**HTTP** (scripts, CI):
```sh
openpencil-mcp-http # Unix socket on macOS/Linux + http://127.0.0.1:7600/mcp
```
Set `OPENPENCIL_MCP_ROOT` to limit file access to one directory; it defaults to the current working directory.
### [AI agent skill](skills/open-pencil/SKILL.md)
Teach your AI coding agent to use OpenPencil — inspect designs, export assets, analyze tokens, modify .fig files:
```sh
npx skills add open-pencil/open-pencil
```
Works with Claude Code, Cursor, Windsurf, Codex, and any agent that supports [skills](https://skills.sh).
For documentation-aware agents, the docs site publishes [llms.txt](https://openpencil.dev/llms.txt), [llms-full.txt](https://openpencil.dev/llms-full.txt), and per-page Markdown files generated from the VitePress docs.
## Collaboration
2026-02-28 05:12:18 +00:00
Share a link to co-edit in real time. No server, no account — peers connect directly via WebRTC.
1. Click the share button in the top-right panel
2. Share the generated link (`app.openpencil.dev/share/<room-id>`)
3. Collaborators see your cursor, selection, and edits in real time
4. Click a peer's avatar to follow their viewport
## Why
Figma is a closed platform that actively fights programmatic access. Their MCP server is read-only. [figma-use](https://github.com/dannote/figma-use) added full read/write automation via CDP — then [Figma 126 killed CDP](https://forum.figma.com/report-a-problem-6/remote-debugging-port-not-working-in-figma-desktop-126-1-2-50858). Your design files are in a proprietary binary format that only their software can fully read. Your workflows break when they decide to ship a point release.
2026-02-28 05:12:18 +00:00
OpenPencil is the alternative: open source (MIT), reads .fig files natively, every operation is scriptable, and your data never leaves your machine.
2026-05-22 17:02:05 +00:00
See the [roadmap](https://openpencil.dev/development/roadmap) for product direction and current Figma compatibility gaps.
## Community
- **[Discord](https://discord.gg/4wXc9fuZfm)** — chat, quick questions, and showing a problem live
- **[GitHub Discussions](https://github.com/open-pencil/open-pencil/discussions)** — [Q&A](https://github.com/open-pencil/open-pencil/discussions/categories/q-a) for help, [Ideas](https://github.com/open-pencil/open-pencil/discussions/categories/ideas) for feature proposals, [Show and tell](https://github.com/open-pencil/open-pencil/discussions/categories/show-and-tell) for what you built; maintainers post [Announcements](https://github.com/open-pencil/open-pencil/discussions/categories/announcements) there
- **[Issues](https://github.com/open-pencil/open-pencil/issues)** — reproducible bugs; report security problems through a [private advisory](https://github.com/open-pencil/open-pencil/security/advisories/new)
## Contributing
```sh
bun install
2026-08-20 05:15:54 +00:00
bun run dev:portless # Web editor at https://open-pencil.localhost
bun run tauri dev # Desktop app (requires Rust)
```
[CONTRIBUTING.md](CONTRIBUTING.md) covers setup, quality gates, pull requests, and commits. [AGENTS.md](AGENTS.md) maps the repository and links the guide inside each package. Desktop builds need [Rust](https://rustup.rs/) and the [Tauri v2 prerequisites](https://v2.tauri.app/start/prerequisites/); run `bun run tauri build`.
### Tech stack
| Layer | Tech |
| ------------- | --------------------------------------------------------------------------------- |
| Rendering | Skia (CanvasKit WASM) |
| Layout | Yoga WASM (flex + grid via [fork](https://github.com/open-pencil/yoga/tree/grid)) |
| UI | Vue 3, Reka UI, Tailwind CSS 4 |
| File format | Kiwi binary + Zstd + ZIP |
| Collaboration | Trystero (WebRTC P2P) + Yjs (CRDT) |
| Desktop | Tauri v2 |
| AI/MCP | Vercel AI SDK (multi-provider BYOK), MCP SDK, Hono |
2026-03-02 09:27:05 +00:00
## Acknowledgments
Thanks to [@sld0Ant](https://github.com/sld0Ant) (Anton Soldatov) for creating and maintaining the [documentation site](https://openpencil.dev).
2026-02-28 05:12:18 +00:00
## License
OpenPencil is licensed under the [MIT License](./LICENSE).
Copyright (c) 2026 Danila Poyarkov and OpenPencil contributors.