diff --git a/PLAN.md b/PLAN.md
index e6071ae19..dd9e5e855 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -4,22 +4,22 @@ Open-source, AI-native design editor. Think Figma, but you can self-host it, ext
## Why
-- Figma is proprietary and expensive
+- Figma is proprietary, expensive, and actively fights programmatic access
- Penpot is SVG-based (slow for complex documents)
-- Pencil.app showed AI-native design is possible, but it's closed source
-- None of the existing tools treat AI as a first-class citizen
+- No existing tool combines open source + Skia rendering + AI-native design + .fig compatibility
## Positioning
-| | OpenPencil | Figma | Penpot | Pencil.app |
-|---|---|---|---|---|
-| Open source | ✅ | ❌ | ✅ | ❌ |
-| Rendering | Skia (WASM) | Skia (WASM) | SVG | Skia (WASM) |
-| AI-native | ✅ MCP | ❌ Plugins only | ❌ | ✅ MCP |
-| Self-hosted | ✅ | ❌ | ✅ | ❌ |
-| .fig import | ✅ | N/A | ❌ | ❌ |
-| Desktop | Tauri | Electron | Browser | Electron |
-| Collaboration | CRDT | Proprietary | WebSocket | Proprietary |
+| | OpenPencil | Figma | Penpot |
+|---|---|---|---|
+| Open source | ✅ MIT | ❌ | ✅ |
+| Rendering | Skia (WASM) | Skia (WASM) | SVG |
+| AI-native | ✅ 26 tools | ❌ Plugins only | ❌ |
+| .fig import/export | ✅ | N/A | ❌ |
+| Desktop | Tauri (~7 MB) | Electron | Browser |
+| Collaboration | P2P (WebRTC + CRDT) | Proprietary | WebSocket |
+| Self-hosted | ✅ | ❌ | ✅ |
+| Cost | Free forever | $15/editor/mo | Free (self-host) |
## Reusable assets from figma-use
@@ -78,7 +78,7 @@ We've built a substantial toolkit in figma-use that transfers directly:
│ │ Editor (Web) │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────┐ │ │
-│ │ │ React UI │ │ Skia CanvasKit (WASM, 7MB) │ │ │
+│ │ │ Vue 3 UI │ │ Skia CanvasKit (WASM, 7MB) │ │ │
│ │ │ │ │ - Vector rendering │ │ │
│ │ │ - Toolbar │ │ - Text shaping │ │ │
│ │ │ - Panels │ │ - Image processing │ │ │
@@ -105,16 +105,17 @@ We've built a substantial toolkit in figma-use that transfers directly:
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────────────┐ │
-│ │ MCP Server (TS/Bun) │ │
+│ │ AI Tools (26 ToolDefs) │ │
│ │ │ │
-│ │ batch_get ── batch_design ── screenshot ── get_layout │ │
-│ │ get_guidelines ── get_style_guide ── variables │ │
+│ │ create_shape ── set_fill ── render (JSX) ── get_node │ │
+│ │ set_layout ── find_nodes ── eval_code ── ... │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────────────┐ │
-│ │ Collaboration (opt) │ │
+│ │ Collaboration (P2P, no server) │ │
│ │ │ │
-│ │ CRDT sync ── Cursors ── Comments ── Version history │ │
+│ │ Trystero (WebRTC) ── Yjs (CRDT) ── y-indexeddb │ │
+│ │ Cursors ── Presence ── Follow mode │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
@@ -544,222 +545,70 @@ Available when 2+ nodes selected. Shown in options bar and right-click menu.
| Format | Import | Export |
|--------|--------|--------|
-| .fig (Figma) | ✅ via Kiwi decoder | ❌ |
-| .pen (Pencil) | ✅ | ❌ |
-| .openpencil | ✅ | ✅ |
-| .svg | ✅ | ✅ |
-| .png | ✅ (image fill) | ✅ (1x, 2x, 3x) |
+| .fig (Figma) | ✅ Kiwi binary codec | ✅ Kiwi + Zstd + thumbnail |
+| .png | ✅ (image fill) | ✅ (1x, 2x, 3x) via CLI and editor |
| .jpg | ✅ (image fill) | ✅ |
-| .pdf | ❌ | ✅ |
-| .jsx/.tsx | ❌ | ✅ (React components) |
-| CSS | ❌ | ✅ (design tokens, styles) |
-| Storybook | ❌ | ✅ (stories + components) |
+| .webp | — | ✅ |
+| .svg | planned | planned |
+| .pdf | — | planned |
+| Figma clipboard | ✅ copy/paste between apps | ✅ |
-### AI (MCP) — 118 tools
+### AI tools — 26 tools
-The editor exposes its **entire** API through MCP. Not a dumbed-down subset — every operation available to a human is available to AI. Ported from figma-use.
+Tools are defined once in `packages/core/src/tools/schema.ts` as framework-agnostic `ToolDef` objects. Available in the in-app AI chat (via Vercel AI SDK adapter) and in the CLI via the `eval` command.
-#### Create (14 tools)
+#### Read (7 tools)
| Tool | Description |
|------|-------------|
-| `create_frame` | Create a frame |
-| `create_rect` | Create a rectangle |
-| `create_ellipse` | Create an ellipse |
-| `create_text` | Create a text node |
-| `create_line` | Create a line |
-| `create_polygon` | Create a polygon |
-| `create_star` | Create a star |
-| `create_vector` | Create a vector path |
-| `create_component` | Create a component |
+| `get_selection` | Get selected nodes |
+| `get_page_tree` | Get page node tree |
+| `get_node` | Get node by ID |
+| `find_nodes` | Find nodes by name or type |
+| `list_pages` | List all pages |
+| `list_variables` | List design variables |
+| `list_collections` | List variable collections |
+
+#### Create (4 tools)
+
+| Tool | Description |
+|------|-------------|
+| `create_shape` | Create a shape (frame, rect, ellipse, line, polygon, star, text) |
+| `render` | Render JSX to design nodes |
+| `create_component` | Convert frame to component |
| `create_instance` | Create a component instance |
-| `create_section` | Create a section |
-| `create_page` | Create a page |
-| `create_slice` | Create a slice |
-| `create_icon` | Create an icon from Iconify |
-#### Set / Modify (18 tools)
+#### Modify (10 tools)
| Tool | Description |
|------|-------------|
-| `set_fill` | Set fill color (hex or variable ref) |
+| `set_fill` | Set fill color |
| `set_stroke` | Set stroke color and weight |
-| `set_stroke-align` | Set stroke alignment (inside/center/outside) |
-| `set_radius` | Set corner radius (uniform or per-corner) |
-| `set_opacity` | Set opacity |
-| `set_rotation` | Set rotation angle |
-| `set_blend` | Set blend mode |
-| `set_visible` | Set visibility |
-| `set_locked` | Set locked state |
-| `set_text` | Set text content |
-| `set_text-resize` | Set text auto resize mode |
-| `set_font` | Set font properties |
-| `set_font-range` | Set font properties for a text range |
-| `set_effect` | Set effect (shadow, blur) |
-| `set_image` | Set image fill from file |
+| `set_effects` | Set effects (shadow, blur) |
+| `update_node` | Update node properties (position, size, text, etc.) |
| `set_layout` | Set auto-layout properties |
| `set_constraints` | Set resize constraints |
-| `set_minmax` | Set min/max width and height |
-| `set_props` | Set instance component properties |
+| `rename_node` | Rename a node |
+| `reparent_node` | Move node to a different parent |
+| `clone_node` | Duplicate a node |
+| `delete_node` | Delete a node |
-#### Node Operations (15 tools)
+#### Organize (4 tools)
| Tool | Description |
|------|-------------|
-| `node_get` | Get node properties |
-| `node_tree` | Get node tree with properties |
-| `node_children` | Get child nodes |
-| `node_ancestors` | Get ancestor chain to root |
-| `node_bounds` | Get bounding box |
-| `node_bindings` | Get variable bindings for fills/strokes |
-| `node_move` | Move a node |
-| `node_resize` | Resize a node |
-| `node_rename` | Rename a node |
-| `node_clone` | Clone node(s) |
-| `node_delete` | Delete node(s) |
-| `node_set-parent` | Reparent a node |
-| `node_replace-with` | Replace node with another node or JSX |
-| `node_to-component` | Convert frame(s) to component(s) |
-| `find` | Find nodes by name or type |
+| `select_nodes` | Set selection |
+| `group_nodes` | Group nodes |
+| `ungroup_node` | Ungroup |
+| `switch_page` | Switch to a different page |
-#### Variables & Collections (11 tools)
+#### Escape hatch (1 tool)
| Tool | Description |
|------|-------------|
-| `variable_list` | List all variables |
-| `variable_get` | Get variable by ID |
-| `variable_find` | Find variables by name pattern |
-| `variable_create` | Create a variable |
-| `variable_set` | Set variable value for mode |
-| `variable_bind` | Bind variable to node property |
-| `variable_delete` | Delete a variable |
-| `collection_list` | List variable collections |
-| `collection_get` | Get collection by ID |
-| `collection_create` | Create a variable collection |
-| `collection_delete` | Delete a collection |
+| `eval_code` | Execute code with Figma-compatible plugin API |
-#### Components (4 tools)
-
-| Tool | Description |
-|------|-------------|
-| `component_add-prop` | Add property to component |
-| `component_edit-prop` | Edit component property |
-| `component_delete-prop` | Delete component property |
-| `component_combine` | Combine components into a component set (variants) |
-
-#### Styles (4 tools)
-
-| Tool | Description |
-|------|-------------|
-| `style_list` | List local styles |
-| `style_create-paint` | Create a paint/color style |
-| `style_create-text` | Create a text style |
-| `style_create-effect` | Create an effect style |
-
-#### Boolean Operations (4 tools)
-
-| Tool | Description |
-|------|-------------|
-| `boolean_union` | Union shapes |
-| `boolean_subtract` | Subtract shapes |
-| `boolean_intersect` | Intersect shapes |
-| `boolean_exclude` | Exclude shapes |
-
-#### Vector Paths (5 tools)
-
-| Tool | Description |
-|------|-------------|
-| `path_get` | Get vector path data |
-| `path_set` | Set vector path data |
-| `path_move` | Move all path points by offset |
-| `path_scale` | Scale path from center |
-| `path_flip` | Flip path horizontally or vertically |
-
-#### Groups (3 tools)
-
-| Tool | Description |
-|------|-------------|
-| `group_create` | Group nodes |
-| `group_ungroup` | Ungroup nodes |
-| `group_flatten` | Flatten nodes into single vector |
-
-#### Analyze (5 tools)
-
-| Tool | Description |
-|------|-------------|
-| `analyze_colors` | Analyze color palette usage |
-| `analyze_typography` | Analyze typography usage |
-| `analyze_spacing` | Analyze spacing values (gap, padding) |
-| `analyze_clusters` | Find repeated patterns (potential components) |
-| `analyze_snapshot` | Generate accessibility tree snapshot |
-
-#### Export (6 tools)
-
-| Tool | Description |
-|------|-------------|
-| `export_node` | Export node as image |
-| `export_screenshot` | Screenshot current viewport |
-| `export_selection` | Export selection as image |
-| `export_jsx` | Export node as JSX component |
-| `export_storybook` | Export components as Storybook stories |
-| `export_fonts` | List fonts used in the current page |
-
-#### Diff (5 tools)
-
-| Tool | Description |
-|------|-------------|
-| `diff_create` | Create a diff patch between two nodes/trees |
-| `diff_show` | Show diff between current state and provided props |
-| `diff_apply` | Apply a diff patch |
-| `diff_visual` | Create visual diff between two nodes as PNG |
-| `diff_jsx` | Show JSX diff between two nodes |
-
-#### Pages & Viewport (7 tools)
-
-| Tool | Description |
-|------|-------------|
-| `page_list` | List all pages |
-| `page_current` | Get current page |
-| `page_set` | Switch to page by ID or name |
-| `page_bounds` | Get bounding box of all objects on page |
-| `viewport_get` | Get viewport position and zoom |
-| `viewport_set` | Set viewport position and zoom |
-| `viewport_zoom-to-fit` | Zoom to fit nodes |
-
-#### Selection & Connectors (6 tools)
-
-| Tool | Description |
-|------|-------------|
-| `selection_get` | Get selected nodes |
-| `selection_set` | Set selection |
-| `connector_create` | Create a connector between two nodes |
-| `connector_get` | Get connector details |
-| `connector_set` | Update connector properties |
-| `connector_list` | List connectors on page |
-
-#### Meta & Escape Hatch (7 tools)
-
-| Tool | Description |
-|------|-------------|
-| `status` | Check connection status |
-| `lint` | Lint design for consistency and accessibility |
-| `import` | Import SVG |
-| `render` | Render JSX to design nodes (Frame, Rect, Ellipse, Text, Line, Star, Polygon, Vector, Group, Icon) |
-| `get_components` | Get all components |
-| `get_styles` | Get all local styles |
-| `get_pages` | Get all pages |
-| `font_list` | List available fonts |
-| `comment_watch` | Wait for new comment and return its content |
-| `eval` | Execute JavaScript in editor context |
-
-**AI workflow:**
-1. AI reads structure: `node_tree`, `find`, `analyze_*`
-2. AI creates/modifies: `create_*`, `set_*`, `node_*`, `render` (JSX)
-3. AI verifies visually: `export_screenshot` → inspects the image
-4. AI iterates: `diff_create` to see what changed, fix issues
-5. AI exports: `export_jsx`, `export_storybook` for developer handoff
-
-### Prototyping
+### Prototyping (planned)
| Feature | Description |
|---------|-------------|
@@ -771,15 +620,6 @@ The editor exposes its **entire** API through MCP. Not a dumbed-down subset —
| Preview | Play prototype in browser |
| Device frames | iPhone, Android, Desktop, custom sizes |
-### Comments
-
-| Feature | Description |
-|---------|-------------|
-| Pin comments | Click anywhere on canvas to leave a comment |
-| Threads | Reply to comments |
-| Resolve | Mark comments as resolved |
-| Mentions | @mention team members |
-
---
---
@@ -1180,663 +1020,95 @@ We already have the full Kiwi codec from figma-use. The `.openpencil` format wil
Migration from .fig: decode with our Kiwi codec → re-encode as .openpencil.
-### Collaboration (CRDT)
+### Collaboration (P2P CRDT)
-The multiplayer model follows what we observed in Figma's protocol:
+Fully peer-to-peer via Trystero (WebRTC). No relay server — signaling via MQTT public brokers, data direct between peers.
-- Each client gets a `sessionID` from the server
-- Every node GUID is `{sessionID}:{localID}` — no conflicts
-- Changes are `NodeChange[]` messages broadcast to all clients
-- Position strings use fractional indexing for z-ordering (no conflicts on concurrent reorder)
-- Server is a relay (doesn't interpret node data, just broadcasts)
-
-We use Yjs for the CRDT layer, mapping NodeChange operations to Yjs documents. This gives us:
-- Offline editing with automatic merge on reconnect
-- Conflict-free concurrent edits
-- Built-in awareness protocol (cursors, selection, viewport)
+- Yjs CRDT for document state sync
+- Awareness protocol for cursors, selections, presence, follow mode
+- y-indexeddb for local persistence — room survives page refresh
+- ICE servers: Google STUN + Cloudflare STUN + Open Relay TURN (TCP + UDP)
+- Secure room IDs via `crypto.getRandomValues()`
+- Stale cursors cleaned on peer disconnect
## Tech Stack
| Layer | Choice | Why |
|-------|--------|-----|
| Rendering | Skia CanvasKit WASM | Same as Figma/Pencil, proven performance |
-| UI | React + TypeScript | Ecosystem, hiring, figma-use renderer reusable |
-| Styling | Tailwind CSS | Fast iteration |
-| State | Zustand | Lightweight, no boilerplate |
-| Layout | Yoga WASM | Flexbox + Grid (soon), battle-tested |
-| Desktop | Tauri v2 | ~5MB vs Electron's ~100MB, Rust backend |
-| AI | MCP (TypeScript) | Protocol-level AI integration |
-| Collaboration | Yjs (CRDT) | Proven real-time sync library |
+| UI | Vue 3 + TypeScript | Reactivity, composables, SFC ecosystem |
+| Styling | Tailwind CSS 4 | Fast iteration |
+| State | Vue reactivity + composables | Native to the framework, zero boilerplate |
+| Layout | Yoga WASM | Flexbox, battle-tested (CSS Grid blocked on upstream) |
+| Desktop | Tauri v2 | ~7MB vs Electron's ~100MB, Rust backend |
+| AI | Vercel AI SDK + OpenRouter | Client-side tool use, no backend |
+| Collaboration | Trystero (WebRTC) + Yjs (CRDT) | P2P sync, no server required |
| File format | Kiwi binary + Zstd | Compact, fast, .fig compatible |
-| Build | Bun | Fast bundling, native TS |
+| Build | Bun + Vite 7 | Fast bundling, native TS |
## Phases
-### Phase 1: Core engine (3 months)
+### Phase 1: Core engine ✅
SceneGraph, Skia rendering, basic shapes, selection, zoom/pan, undo/redo.
-**Deliverable:** draw rectangles, ellipses, frames, move them, undo, zoom.
+### Phase 2: Editor UI + Layout ✅
-**Validation:**
+Properties panel, layers panel, toolbar, Yoga layout integration, text editing, pen tool with vector networks.
-| Test | Pass criteria |
-|------|---------------|
-| SceneGraph unit tests | CRUD nodes, parent-child, z-ordering, hit testing — 100% coverage of core ops |
-| Skia rendering tests | Render each primitive (rect, ellipse, line, star, polygon, vector path) → compare PNG output against reference images, <0.1% pixel diff |
-| Transform tests | Rotation, scaling, nested transforms → verify computed bounding boxes match expected values |
-| Undo/redo tests | Create 50 random operations, undo all → empty canvas. Redo all → identical state. Property-based testing with fast-check |
-| Performance benchmark | 10,000 nodes: scene graph ops <1ms, full render <16ms (60fps), pan/zoom stays interactive |
-| Memory test | Create 10,000 nodes, delete all → verify no WASM memory leak (CanvasKit paint/path objects freed) |
-| Integration smoke | Open browser → canvas loads → draw rect → move it → undo → redo. Manual but scripted with Playwright |
+### Phase 3: File format + Import/Export ✅
-### Phase 2: Editor UI + Layout (3 months)
+.fig import and export via Kiwi binary codec, Figma clipboard interop, PNG/JPG/WEBP export.
-Properties panel, layers panel, toolbar, Yoga layout integration, constraints, text editing.
+### Phase 4: Components + Variables ✅
-**Deliverable:** functional editor with auto-layout, text, and property editing.
+Components, instances, component sets with live sync and override preservation. Variables with collections, modes, color bindings, alias chains.
-**Validation:**
+### Phase 5: AI integration ✅
-| Test | Pass criteria |
-|------|---------------|
-| Yoga layout unit tests | Horizontal, vertical, nested, wrap, gap, padding, justify, align — verify computed positions match CSS flexbox reference (compare against browser rendering of equivalent CSS) |
-| Yoga ↔ Figma mapping tests | For each `stack*` field value, verify the Yoga adapter produces identical layout to Figma's auto-layout. Test corpus: 50+ layout configs exported from Figma |
-| Grid layout tests | (If Yoga Grid has landed) Template columns/rows, gap, track sizes — compare against CSS Grid reference |
-| Text editing tests | Type, select, bold range, change font size mid-text, undo text change — verify textData roundtrips correctly |
-| Constraint tests | Pin left, right, center, scale — resize parent frame → verify child positions match expected constraint behavior |
-| Panel integration tests | Layers panel reflects tree structure after every operation. Properties panel shows correct values for selected node. Playwright E2E |
-| Keyboard shortcuts | Every shortcut in the tools table fires the correct action. Automated with Playwright key simulation |
-| Accessibility | All panels keyboard-navigable, ARIA roles correct. axe-core audit passes |
+In-app AI chat (⌘J) with OpenRouter, 10 tools, streaming responses with tool call timeline. JSX renderer. Code panel with JSX export.
-### Phase 3: File format + Import (2 months)
+### Phase 6: Collaboration ✅
-.openpencil format, .fig import (from figma-use Kiwi codec), .svg import/export, PNG/PDF export.
+P2P real-time collaboration via Trystero (WebRTC) + Yjs CRDT. Cursors, presence, follow mode. No server required.
-**Deliverable:** open and save files, import from Figma.
+### Phase 7: Distribution ✅
-**Validation:**
+Tauri v2 desktop app (macOS, Windows, Linux). Web app at app.openpencil.dev. CLI published on npm. Documentation site at openpencil.dev. GitHub Actions CI for builds and deploys.
-| Test | Pass criteria |
-|------|---------------|
-| Roundtrip test | Create document → save .openpencil → close → reopen → all nodes, properties, and blobs identical (byte-level comparison of serialized NodeChange[]) |
-| .fig import corpus | Import 20+ .fig files of varying complexity. For each: parse succeeds, all nodes present in scene graph, no data loss on known fields |
-| Figma pixel-perfect (T0) | Basic shapes .fig → import → render → compare against Figma export. <0.1% diff on rectangles, ellipses, solid fills, positioning |
-| Figma pixel-perfect (T1) | Auto-layout .fig files → import → render → <0.5% diff (layout positions must match) |
-| SVG import | 50 SVG files from real-world icon sets → import → verify path data preserved, fills/strokes correct |
-| SVG export | Export 20 nodes as SVG → re-import → visual diff <0.1% |
-| PNG export | Export at 1x, 2x, 3x → verify dimensions are exact, pixel content matches canvas rendering |
-| PDF export | Export 5 frames → open in PDF viewer → visual sanity check (automated with pdf2png + pixelmatch) |
-| Schema versioning | Open .openpencil v1 file with v2 schema → migration runs without data loss |
-| Fuzz test | Feed 1000 random byte sequences to .openpencil parser → no crashes, only clean error messages |
+### What's next
-### Phase 4: Components + Variables (2 months)
-
-Components, instances, overrides, variants, variables, collections, modes/themes.
-
-**Deliverable:** design system creation workflow.
-
-**Validation:**
-
-| Test | Pass criteria |
-|------|---------------|
-| Component create/instantiate | Create component → create 10 instances → override text on instance 3 → verify main component unchanged, instance 3 has override, others inherit |
-| Override propagation | Change main component fill → all instances update except those with fill override |
-| Variant switching | Create component set with 3 variants → swap variant on instance → correct child structure rendered |
-| Component props | Add TEXT prop → set default → verify instance shows default → override on instance → verify override |
-| Nested instances | Instance containing instance containing instance → override deeply nested text → verify correct propagation |
-| Variables CRUD | Create collection → add 10 variables (color, number, string, boolean) → bind to node properties → verify rendered output uses variable values |
-| Theme switching | 2 modes (light/dark) → switch mode → all variable-bound properties update → render matches expected |
-| Variable binding roundtrip | Save file with variable bindings → reload → bindings intact, resolved values correct |
-| .fig component import | Import .fig with components → instances resolve correctly → overrides preserved |
-
-### Phase 5: AI integration (2 months)
-
-In-app AI chat + MCP server. Two interfaces to the same tool set: chat panel for interactive design, MCP for external agents.
-
-**Deliverable:** AI can design full interfaces through the chat panel or MCP.
-
-#### In-app AI chat
-
-**No backend.** Everything runs in the browser. The user provides their own OpenRouter API key. The app calls OpenRouter directly via `fetch()` — no proxy server, no sidecar, no extra process.
-
-```
-Browser (Vue)
- ChatPanel.vue
- useChat() ──stream──→ OpenRouter (SSE)
- tool exec (local) Claude Sonnet
-```
-
-Stack: `ai@6.0.0-beta` + `@ai-sdk/valibot` + `@openrouter/ai-sdk-provider` + `valibot`. All client-side — `ai/vue` `useChat()` composable with `fetch` transport directly to OpenRouter.
-
-**Why no backend:** All tools execute client-side (editor store, canvas). A backend would add SSE roundtrip latency on every tool call (10-20 per message), require a Tauri sidecar for production, and add failure modes — all to protect an API key that the user owns. Desktop apps (1Password, Cursor, etc.) use the same model.
-
-**API key storage:**
-- **Tauri (desktop):** `tauri-plugin-stronghold` — encrypted vault on disk, key loaded into JS memory at runtime for fetch headers. Never in source code, never in plaintext on disk.
-- **Browser mode:** `localStorage` with a clear warning. Acceptable for a local-first design tool — no worse than any SPA with user-provided keys.
-
-**Chat panel UI:** Replaces right sidebar (properties panel) when active, toggled with `⌘J`. Both share the same splitter slot. Tool calls render inline as a collapsible timeline (like beebro-chat's `ToolTimeline`) with icons, status, expandable params/results. Screenshots and visual diffs render inline as thumbnails.
-
-#### AI workflow (from figma-use)
-
-```
-1. Read → snapshot (compact a11y tree), get_page_tree, find_nodes
-2. Create → render (JSX), set_* for edits
-3. Verify → screenshot → AI inspects the image
-4. Iterate → diff_create to see what changed, fix issues
-5. Export → export_jsx for developer handoff
-```
-
-**`render` (JSX) is the primary creation tool** — not individual `create_frame`/`create_text` calls. One JSX call creates an entire component tree:
-
-```jsx
-
-
- Card Title
- Lorem ipsum
-
-```
-
-Individual `set_*` tools are for surgical edits only. JSX renderer ported from figma-use's `packages/cli/src/render/`, lives in `@open-pencil/core` (no DOM deps, works headless too).
-
-**Guardrail/diff tools** for the self-correcting loop:
-
-| Tool | Purpose |
-|------|---------|
-| `screenshot` | Render viewport/node to PNG → AI inspects visually |
-| `snapshot` | Compact text representation of the design tree |
-| `diff_create` | Structural diff between two nodes (unified patch) |
-| `diff_visual` | Pixel-level diff → image showing differences |
-| `diff_show` | Preview property changes before applying |
-| `diff_apply` | Apply a diff patch to nodes |
-
-#### Comment pins as AI context
-
-Users pin comments on canvas — AI context annotations, not collaboration comments. When chatting, comments visible in viewport are automatically included in the system prompt with their positions and attached node IDs.
-
-```typescript
-// Stored on SceneGraph, not individual nodes (can pin to empty canvas)
-interface CommentPin {
- id: string
- text: string
- x: number; y: number
- nodeId?: string
- resolved: boolean
- createdAt: number
-}
-```
-
-#### Implementation order
-
-1. ✅ JSX renderer in `@open-pencil/core` (port from figma-use)
-2. ✅ Chat panel UI — `ChatPanel.vue`, `useChat()`, message list, tool timeline, `⌘J` toggle
-3. Code panel — "Code" tab in properties panel, JSX export with syntax highlighting
- - `sceneNodeToJsx()` in `packages/core/src/render/export-jsx.ts` (reverse of renderer.ts)
- - `CodePanel.vue` with Prism.js highlighting, copy button, reka-ui ScrollArea
- - Third tab in PropertiesPanel: Design | Code | AI
- - Subtree export: full JSX tree for selection, multi-selection separated by blank lines
-4. API key settings — input UI, Stronghold storage (Tauri) / localStorage (browser)
-5. Core tools — render, set_fill, set_layout, set_text, snapshot, get_selection
-6. Screenshot + diff tools
-7. Comment pin system
-8. Full tool set (remaining 118 tools)
-
-#### Validation
-
-| Test | Pass criteria |
-|------|---------------|
-| Tool coverage | All 118 tools callable via chat and MCP. Automated: call each with valid args → no errors |
-| JSX render E2E | Render 10 JSX snippets of varying complexity → all nodes created with correct properties |
-| Screenshot loop | AI creates layout → screenshots → detects overlap → fixes → screenshots again → resolved in ≤3 iterations |
-| Diff roundtrip | diff_create between two nodes → diff_apply on source → source matches target |
-| Chat UX | Send message → tool calls stream in real-time → expandable timeline → final text renders |
-| Comment context | Pin 3 comments → send chat message → system prompt includes all 3 with correct positions |
-| Error handling | Invalid tool args → clean error in chat, no crash. Network failure → retry/abort UI |
-
-### Phase 6: Polish + Distribution (2 months)
-
-Prototyping, comments, Tauri desktop app, PWA, VS Code extension, documentation, public launch.
-
-**Deliverable:** shippable product.
-
-**Validation:**
-
-| Test | Pass criteria |
-|------|---------------|
-| Tauri build | Builds on macOS (Apple Silicon + Intel), Windows (x64), Linux (x64). Installer <15MB |
-| Tauri CanvasKit | CanvasKit WASM loads correctly in Tauri's WKWebView/WebView2/WebKitGTK. WebGL2 context created. Render test passes |
-| Tauri file I/O | Open/save .openpencil via native file dialog. Import .fig via drag-and-drop |
-| PWA | Install as PWA → offline capable (IndexedDB) → opens files via File System Access API |
-| Prototype preview | Create 3 frames with click transitions → preview mode → click → navigates → back works |
-| Comments | Add pin comment → reply → resolve → verify persistence across save/load |
-| Performance audit | Lighthouse score >90. First paint <2s. 1000-node document stays at 60fps |
-| Cross-browser | Chrome, Safari, Firefox — all E2E tests pass |
-| Full Figma compat suite | Run the pixel-perfect test corpus (all tiers). T0: 100% pass. T1: >95% pass. T2: >80% pass |
-
-**Total: ~14 months** (or faster with parallel tracks)
-
----
-
-## Stack Validation
-
-Every piece needs to work together. Here's the proof-of-concept checklist before committing to the stack:
-
-### PoC 1: CanvasKit + Tauri (Week 1)
-
-**Question:** Does CanvasKit WASM work in Tauri's webview on all platforms?
-
-**Test:**
-1. `bun create tauri-app poc-canvaskit` with React template
-2. Load `canvaskit-wasm` (7MB) — verify WASM instantiation works
-3. Draw 1000 rectangles with random fills → measure FPS
-4. Test WebGL2 context on macOS (WKWebView), Windows (WebView2), Linux (WebKitGTK)
-
-**Risk:** Linux WebKitGTK has historically been behind on WebGL2. Mitigation: CanvasKit has a CPU fallback, or we ship with a minimum WebKitGTK version requirement.
-
-**Risk:** WASM file size (7MB). Tauri serves assets via custom protocol (`tauri://`), not HTTP — should be fine but need to verify no loading issues. Mitigation: bundle canvaskit.wasm as a Tauri resource, load via `asset:` protocol.
-
-### PoC 2: Yoga WASM in browser (Week 1)
-
-**Question:** Does `yoga-layout` (v3.2.1, 224KB npm) work alongside CanvasKit WASM?
-
-**Test:**
-1. In the same Tauri app from PoC 1, `import { Yoga, Align } from 'yoga-layout'`
-2. Create a Yoga tree matching a Figma auto-layout (horizontal, gap:16, padding:20, 3 children with fill_container)
-3. Calculate layout → read computed positions → render with CanvasKit at those positions
-4. Compare screenshot against the same layout in Figma
-
-**Risk:** Two WASM modules (CanvasKit 7MB + Yoga 45KB) in one page. Should be fine — they use separate WASM memories. Verify no conflicts.
-
-**Risk:** Yoga Grid PR not merged yet. Mitigation: start with Flexbox only (covers 90% of design tool use). Grid can be added later without architecture changes.
-
-### PoC 3: Kiwi codec + .fig parsing (Week 2)
-
-**Question:** Can we extract the figma-use Kiwi code from git history and use it standalone?
-
-**Test:**
-1. Extract `multiplayer/schema.ts`, `multiplayer/codec.ts`, `multiplayer/protocol.ts` from git commit `9216dcc^`
-2. Package as `@openpencil/kiwi` with `kiwi-schema` dependency
-3. Parse 5 real .fig files → decode NodeChange[] → verify node count matches Figma API response
-4. Re-encode decoded data → verify byte-level roundtrip (decode → encode → decode → same data)
-
-**Risk:** `kiwi-schema` npm package (by Figma co-founder Evan Wallace) — need to verify it's still maintained and handles all field types correctly. Mitigation: we can vendor and patch if needed.
-
-### PoC 4: Kiwi → SceneGraph → CanvasKit pipeline (Week 2)
-
-**Question:** Can we go from .fig bytes to rendered pixels?
-
-**Test:**
-1. Parse .fig → NodeChange[]
-2. Build SceneGraph from NodeChange[]
-3. For each visible node: map properties to CanvasKit draw calls
-4. Render to offscreen CanvasKit surface → export PNG
-5. Compare against Figma REST API export of the same file
-
-**Target:** <1% pixel diff on a simple .fig file (frames, rectangles, text, solid fills).
-
-### PoC 5: Yjs + SceneGraph (Week 3)
-
-**Question:** Can Yjs efficiently sync our NodeChange-based scene graph?
-
-**Test:**
-1. Represent each node as a `Y.Map` inside a `Y.Map` (flat structure keyed by GUID)
-2. Two clients connected via `y-websocket`
-3. Client A creates 100 nodes → Client B receives all 100
-4. Client A moves node → Client B sees updated position
-5. Both clients offline-edit → reconnect → merge without conflicts
-6. Measure sync latency (<50ms for single property change over localhost)
-
-**Risk:** Y.Map per node could be memory-heavy for large documents (100K+ nodes). Mitigation: benchmark memory usage. If too high, consider a custom Yjs type or chunked sync.
-
-**Risk:** Yjs has Y.UndoManager — we need to verify it works with our undo model or if we need to keep our own inverse-command stack alongside.
-
-### PoC 6: Tauri + MCP server (Week 3)
-
-**Question:** Can the MCP server run as a sidecar/embedded process in Tauri?
-
-**Test:**
-1. Package the MCP server as a Bun binary (or TS compiled with Bun)
-2. Launch as Tauri sidecar process
-3. MCP client (Claude Desktop or test harness) connects → calls `create_frame` → node appears in editor
-
-**Alternative:** Run MCP as an in-process API (no sidecar). The editor's web code calls MCP tool handlers directly via JS imports. This is simpler and eliminates IPC latency.
-
-**Risk:** Tauri sidecar requires bundling a Bun/Node runtime. Mitigation: compile MCP server to a standalone binary with `bun build --compile`, or run it in-process.
-
-### PoC 7: Full vertical slice (Week 4)
-
-**Question:** Can a user open a .fig file, see it rendered, edit a property, undo, and save?
-
-**Test:**
-1. Tauri app with CanvasKit canvas + minimal React UI
-2. Import .fig → render scene graph
-3. Click to select a rectangle → properties panel shows fill color
-4. Change fill → CanvasKit re-renders → undo → original fill restored
-5. Save as .openpencil → reopen → identical
-
-This PoC validates the entire stack end-to-end in 4 weeks, before committing to 14 months of development.
+- **Multi-file / tabs** — open multiple documents in tabs within a single window
+- **Code signing** — Apple & Azure certificates for properly signed desktop binaries
+- **.fig compatibility** — improving import/export fidelity across a larger set of real-world files
+- **Port all figma-use tools** — bring the full 118-tool set from [figma-use](https://github.com/dannote/figma-use) into the editor (currently 26/118) for complete AI agent design capabilities
+- **CI tools** — design linting, code export, visual regression in pipelines via the headless CLI
+- **Prototyping** — frame transitions, interaction triggers, preview mode
+- **SVG/PDF export** — vector export formats
---
## CLI & Headless Mode
-The editor should be fully controllable by AI agents and usable in CI without a GUI.
+The CLI (`@open-pencil/cli`) runs headless in Bun — loads the engine directly, no window, no Tauri, no WebGL. CanvasKit WASM CPU software rasterizer enables PNG export without a display server.
-### Architecture
+### Monorepo structure (implemented)
-Two modes of operation:
-
-**Attached** — CLI connects to a running OpenPencil instance via WebSocket. The app starts a WS server on a configurable port. `eval` runs JS in the app's context with full access to the editor store, scene graph, renderer, and CanvasKit. This is how interactive AI workflows work (create, modify, screenshot, iterate).
-
-**Headless** — CLI loads the engine directly in Bun, no window, no Tauri, no WebGL. For linting, analysis, .fig validation, rendering, CI pipelines. CanvasKit WASM has a CPU software rasterizer (`CanvasKit.MakeSurface(w, h)`) so even PNG export works without a display server.
-
-**Why not a Tauri CLI?** Tauri is a GUI framework — no headless mode. A Bun CLI that imports the engine directly starts instantly and works in CI (Docker, GitHub Actions) without X11/Wayland.
-
-### Monorepo structure
-
-Convert to a Bun workspace monorepo:
+Bun workspace with three packages:
```
-package.json — workspace root: { "workspaces": ["packages/*"] }
+package.json — workspace root
packages/
- core/ — engine: scene-graph, layout, codec, types, renderer
- src/
- types.ts — ← from src/types.ts
- scene-graph.ts — ← from src/engine/scene-graph.ts
- layout.ts — ← from src/engine/layout.ts
- renderer.ts — ← from src/engine/renderer.ts
- fonts.ts — ← from src/engine/fonts.ts
- color.ts — ← from src/engine/color.ts
- vector.ts — ← from src/engine/vector.ts
- snap.ts — ← from src/engine/snap.ts
- undo.ts — ← from src/engine/undo.ts
- clipboard.ts — ← from src/engine/clipboard.ts
- fig-export.ts — ← from src/engine/fig-export.ts
- canvaskit.ts — ← from src/engine/canvaskit.ts (+ headless init path)
- constants.ts — ← rendering/engine constants from src/constants.ts
- kiwi/ — ← from src/kiwi/ (codec, fig-import, fig-file, schema, protocol)
- index.ts — public API barrel
- package.json — name: @open-pencil/core, no DOM deps
- cli/ — CLI: open-pencil command
- src/
- index.ts — citty main (like figma-use)
- headless.ts — headless document context (load file → SceneGraph + CanvasKit CPU)
- attached.ts — WebSocket client for attached mode
- commands/
- eval.ts — attached: run JS in editor context
- lint.ts — both: design linter (port from figma-use)
- find.ts — both: find nodes by name/type/query
- export.ts — both: PNG/SVG/PDF (headless uses CPU rasterizer)
- node.ts — both: get/tree node info
- create.ts — attached: create nodes
- set.ts — attached: set node properties
- screenshot.ts — both: render page to PNG
- diff.ts — headless: visual diff two files
- analyze/
- colors.ts — both: color palette analysis
- spacing.ts — both: spacing consistency
- mcp.ts — both: start MCP server
- package.json — name: @open-pencil/cli, bin: { "open-pencil": "./src/index.ts" }
- linter/ — design lint rules (same pattern as figma-use/packages/linter)
- src/
- core/
- types.ts — LintMessage, LintConfig, LintResult, Rule, RuleContext
- linter.ts — Linter class: loads rules, walks SceneNode tree, collects messages
- rule.ts — defineRule() helper
- rules/ — one file per rule
- no-default-names.ts
- pixel-perfect.ts
- consistent-spacing.ts
- consistent-radius.ts
- no-hardcoded-colors.ts
- no-empty-frames.ts
- prefer-auto-layout.ts
- touch-target-size.ts
- color-contrast.ts
- config/
- presets.ts — recommended, strict, accessibility
- index.ts
- package.json — name: @open-pencil/linter, depends on @open-pencil/core
- mcp/ — MCP server for AI agents
- src/
- index.ts — MCP protocol handler, tool definitions
- tools.ts — maps MCP tool calls → core/CLI operations
- package.json — name: @open-pencil/mcp
- app/ — ← current src/ (Vue + Tauri desktop app)
- src/
- components/ — Vue UI components (unchanged)
- composables/ — Vue composables (unchanged)
- stores/editor.ts — imports from @open-pencil/core instead of ../engine/
- constants.ts — UI-only constants (colors kept, but types/engine constants → core)
- demo.ts
- ...
- desktop/ — ← current desktop/ (Tauri Rust)
- package.json — name: @open-pencil/app, depends on @open-pencil/core
+ core/ — @open-pencil/core: scene graph, renderer, layout, codec, tools
+ cli/ — @open-pencil/cli: headless CLI (info, tree, find, export, analyze)
+ docs/ — VitePress documentation site
+src/ — Vue 3 + Tauri desktop editor (imports from @open-pencil/core)
+desktop/ — Tauri v2 Rust backend
+tests/
+ e2e/ — Playwright visual regression
+ engine/ — bun:test unit tests
```
-### Implementation steps
-
-**Step 1: Bun workspace setup**
-- Add `"workspaces"` to root `package.json`
-- Create `packages/core/package.json`, `packages/cli/package.json`, etc.
-- All packages use `"type": "module"` and TypeScript
-
-**Step 2: Extract `@open-pencil/core`**
-
-The engine files are already clean — zero DOM imports. The work is:
-1. Move `src/engine/*.ts` → `packages/core/src/`
-2. Move `src/types.ts` → `packages/core/src/types.ts`
-3. Move `src/kiwi/` → `packages/core/src/kiwi/`
-4. Split `src/constants.ts`: engine constants → `packages/core/src/constants.ts`, UI constants stay in app
-5. Remove `@/` alias imports, use relative imports within core
-6. `canvaskit.ts` gets a headless init path: `initCanvasKit({ locateFile })` that works without `/canvaskit.wasm` served by Vite
-7. `fig-export.ts` IS_TAURI branch stays — but make the non-Tauri fflate path the default, Tauri path optional
-8. Create `index.ts` barrel exporting SceneGraph, SceneNode, SkiaRenderer, layout functions, codec, types
-
-After this step: `import { SceneGraph, SkiaRenderer } from '@open-pencil/core'` works from CLI, tests, and app.
-
-**Step 3: Update app to import from core**
-
-Replace all `@/engine/` and `@/types` imports in app code with `@open-pencil/core`:
-- `src/stores/editor.ts` — biggest consumer
-- `src/components/*.vue` — type imports
-- `src/composables/*.ts` — renderer, scene graph types
-- `src/demo.ts` — SceneGraph, SceneNode
-- Existing tests continue to work (`bun test`)
-
-**Step 4: WebSocket bridge in the app**
-
-Add a WS server to the running app (opt-in, e.g. `--ws-port 9333` or menu toggle):
-- Tauri side: use `tauri-plugin-localhost` or spawn from Rust, OR just do it in JS (the webview runs a full JS runtime)
-- Simpler: WS server in the Vue app itself using the browser's `WebSocket` API... no, the *app* needs to be the server
-- Best approach: tiny WS server in Rust (Tauri command) that forwards messages to the webview via Tauri events
-- OR: use the Vite dev server in dev mode, add WS endpoint. In production, Tauri Rust side runs a `tungstenite` WS server
-
-Protocol (JSON-RPC 2.0):
-```json
-{"jsonrpc":"2.0","id":1,"method":"eval","params":{"code":"editor.graph.getNode('0:1')"}}
-{"jsonrpc":"2.0","id":2,"method":"screenshot","params":{"width":1920,"height":1080}}
-{"jsonrpc":"2.0","id":3,"method":"createNode","params":{"type":"RECTANGLE","x":0,"y":0,"width":100,"height":100}}
-```
-
-The webview handler: receives JSON-RPC, calls editor store methods, returns results. `eval` uses `new Function()` with the store in scope.
-
-**Step 5: Headless document context**
-
-`packages/cli/src/headless.ts`:
-```ts
-import CanvasKitInit from 'canvaskit-wasm'
-import { SceneGraph, SkiaRenderer, readFigFile } from '@open-pencil/core'
-
-export async function loadDocument(filePath: string) {
- const data = await Bun.file(filePath).arrayBuffer()
- const graph = new SceneGraph()
- await readFigFile(new File([data], filePath), graph)
- return graph
-}
-
-export async function renderDocument(graph: SceneGraph, opts: { width: number, height: number }) {
- const ck = await CanvasKitInit()
- const surface = ck.MakeSurface(opts.width, opts.height)!
- const renderer = new SkiaRenderer(ck, surface)
- renderer.viewportWidth = opts.width
- renderer.viewportHeight = opts.height
- // ... set zoom to fit, render
- renderer.render(graph, new Set())
- return surface.makeImageSnapshot().encodeToBytes(ck.ImageFormat.PNG, 100)!
-}
-```
-
-**Step 6: CLI commands**
-
-Use `citty` (same as figma-use) for command framework:
-```
-open-pencil lint design.fig --preset recommended
-open-pencil export design.fig --format png --page "Home"
-open-pencil node tree design.fig
-open-pencil find design.fig --type RECTANGLE --name "Button*"
-open-pencil diff v1.fig v2.fig --output diff.png
-open-pencil screenshot --port 9333 --output screen.png
-open-pencil eval --port 9333 "editor.select(['0:5'])"
-open-pencil mcp --port 9333
-```
-
-Headless commands take a file path. Attached commands take `--port`.
-
-**Step 7: Linter**
-
-Port the figma-use linter architecture but adapt types from `FigmaNode` → `SceneNode`:
-- `Linter` class walks the `SceneGraph` tree
-- Rules get `SceneNode` + `RuleContext` (same pattern: `defineRule`, `context.report()`)
-- Start with rules that make sense for our node model: `no-default-names`, `pixel-perfect`, `consistent-spacing`, `no-empty-frames`, `prefer-auto-layout`, `touch-target-size`
-- Presets: recommended, strict
-- Output: text (with colors) and JSON
-
-**Step 8: MCP server**
-
-Wrap CLI operations as MCP tools. Two modes:
-- **Headless MCP**: load a file, expose read-only tools (lint, find, analyze, export)
-- **Attached MCP**: connect to running app, expose all tools (create, set, eval, screenshot)
-
-Tool definitions generated from command schemas (like figma-use's `mcp-tools.json`).
-
-### Dependencies
-
-| Package | New deps | Existing (from app) |
-|---------|----------|---------------------|
-| core | — | canvaskit-wasm, yoga-layout, fflate, fzstd, culori |
-| cli | citty, consola | — |
-| linter | — | @open-pencil/core |
-| mcp | @modelcontextprotocol/sdk | @open-pencil/core, @open-pencil/cli |
-| app | — | vue, reka-ui, tailwindcss, @tauri-apps/*, @open-pencil/core |
-
-### Types & shared code
-
-The linter, CLI, MCP, and app all need the same node/document types. These live in `@open-pencil/core` — it's the single source of truth:
-
-```
-@open-pencil/core exports:
- types.ts — GUID, Color, Vector, Matrix, Rect (primitives)
- scene-graph.ts — SceneNode, SceneGraph, NodeType, Fill, Stroke, Effect,
- LayoutMode, LayoutSizing, VectorNetwork, etc.
- layout.ts — computeLayout, computeAllLayouts
- renderer.ts — SkiaRenderer, RenderOverlays
- color.ts — parseColor, colorToHex, etc.
- fonts.ts — loadFont, listFamilies (browser + fs paths)
- vector.ts — vectorNetworkToPath, encode/decode blobs
- snap.ts — computeSelectionBounds, computeSnap, SnapGuide
- undo.ts — UndoManager
- clipboard.ts — parse/build clipboard HTML
- fig-export.ts — exportFigFile
- kiwi/ — codec, fig-import, fig-file, schema, protocol
-```
-
-The linter doesn't define its own `FigmaNode` — it works directly with `SceneNode` from core. No adapter layer, no duplication.
-
-The JSON-RPC protocol types (for the WS bridge between CLI↔app) live in a shared file within `@open-pencil/core`:
-
-```ts
-// core/src/rpc.ts
-interface RpcRequest {
- jsonrpc: '2.0'
- id: number
- method: string
- params?: Record
-}
-
-interface RpcResponse {
- jsonrpc: '2.0'
- id: number
- result?: unknown
- error?: { code: number; message: string }
-}
-
-type RpcMethod =
- | 'eval'
- | 'screenshot'
- | 'getNode'
- | 'getTree'
- | 'createNode'
- | 'updateNode'
- | 'deleteNode'
- | 'select'
- | 'find'
- | ...
-```
-
-Both the app's WS server and the CLI's WS client import these types from `@open-pencil/core`.
-
-### Tests
-
-Tests live alongside the package they test. Each package has its own `tests/` dir:
-
-```
-packages/
- core/
- tests/
- scene-graph.test.ts — ← existing tests/engine/scene-graph.test.ts (76 tests)
- layout.test.ts — ← existing tests/engine/layout.test.ts
- fig-import.test.ts — ← existing tests/engine/fig-import.test.ts
- renderer.test.ts — NEW: headless render → PNG, compare snapshots
- fig-roundtrip.test.ts — NEW: import .fig → export .fig → reimport, verify lossless
- linter/
- tests/
- linter.test.ts — rule execution, preset loading, report format
- rules/ — one test file per rule (same pattern as figma-use)
- cli/
- tests/
- headless.test.ts — load .fig file headless, verify scene graph
- commands.test.ts — CLI command integration tests (invoke via subprocess)
- app/
- tests/
- e2e/ — ← existing Playwright tests (create-shapes, layers-panel)
- helpers/ — ← existing CanvasHelper
-```
-
-All unit tests run with `bun test`. No Playwright, no browser needed. The test commands:
-
-```
-bun test — all unit tests across all packages
-bun test packages/core — core only
-bun test packages/linter — linter only
-bun test packages/cli — CLI only
-bun test --project openpencil — Playwright e2e (app must be running)
-```
-
-New test categories to add:
-
-| Category | Package | What it validates |
-|----------|---------|-------------------|
-| Headless render | core | `MakeSurface` → render scene → PNG snapshot matches expected |
-| Fig roundtrip | core | .fig import → export → reimport produces identical scene graph |
-| Linter rules | linter | Each rule produces expected messages on crafted test nodes |
-| Linter presets | linter | Preset loads correct rule set, severity overrides work |
-| CLI integration | cli | `open-pencil lint test.fig` exits 0/1, outputs expected format |
-| CLI headless render | cli | `open-pencil export test.fig --format png` produces valid PNG |
-| Visual diff | cli | `open-pencil diff a.fig b.fig` detects pixel differences |
-
-Snapshot tests for rendering: store expected PNGs in `packages/core/tests/snapshots/`. Use pixel-level comparison (allow small tolerance for anti-aliasing across platforms). CanvasKit CPU rasterizer is deterministic on the same platform, so CI snapshots only need per-platform baselines.
-
### Kiwi decoder performance — investigated
We benchmarked the Rust `kiwi-schema` crate (by Evan Wallace, 1.5M downloads) against our JS decoder:
@@ -1852,13 +1124,13 @@ A faster Rust/WASM decoder would require code generation (like protobuf's `prost
**The real bottleneck was O(n²) `getChildren()` in `importNodeChanges`** (scanning all parent entries for each node). Fixed by building a `childrenMap` index upfront → **69x speedup** (37s → 535ms for material3).
-### Risks — validated
+### Risks — all validated
-All three risks have been tested and eliminated:
-
-- **CanvasKit WASM in Bun** ✅ — `MakeSurface(100, 100)` creates a CPU surface, renders shapes, produces valid PNG. Zero issues.
-- **Font loading in headless** ✅ — `readFileSync` loads system fonts (e.g. `/System/Library/Fonts/SFNS.ttf`), `FontMgr.FromData()` + `ParagraphBuilder` renders anti-aliased text. For CI (Linux), bundle a default font (Inter or similar).
-- **Core extraction complexity** ✅ Low — engine files have zero DOM imports, no circular deps. Only 2 engine files reference `@/constants` (renderer + fig-export). ~30 import statements in app code need `@/engine/` → `@open-pencil/core`. Mechanical move.
+- **CanvasKit WASM in Bun** ✅ — CPU surface works headless for CLI export
+- **Font loading in headless** ✅ — system fonts loaded via `readFileSync` + `FontMgr.FromData()`
+- **Core extraction** ✅ — engine is `@open-pencil/core` with zero DOM deps, used by both app and CLI
+- **Kiwi codec performance** ✅ — JS decoder is 3.5–5.4x faster than Rust kiwi-schema crate (JIT-compiled `new Function()` vs dynamic `Value` with HashMap)
+- **P2P collaboration** ✅ — Trystero + Yjs works without a relay server
---
@@ -1992,4 +1264,4 @@ Full Figma-compatible shortcut map. Implemented shortcuts marked with ✅.
---
-*Created: 2026-02-26*
+*Created: 2026-02-26 · Updated: 2026-03-02*
diff --git a/README.md b/README.md
index f8eec1625..98856b0e2 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,14 @@ Open-source, AI-native design editor. Figma-compatible, AI-first, fully local.
**[Try it online →](https://app.openpencil.dev)** · [Download](https://github.com/open-pencil/open-pencil/releases/latest) · [Documentation](https://openpencil.dev)
+### What's next
+
+- Multi-file / tabs support
+- Code signing (Apple & Azure certificates for properly signed binaries)
+- Improving .fig compatibility across a larger set of files
+- Porting all [figma-use](https://github.com/dannote/figma-use) tools (118 → 26 currently) for full AI agent design capabilities
+- CI tools — design linting, code export, visual regression in pipelines
+

## Why