Commit graph

13 commits

Author SHA1 Message Date
Danila Poyarkov b85805e9c7 chore: format sources with oxfmt 2026-05-03 21:03:40 +03:00
Danila Poyarkov 6545f20c53
Refactor architecture boundaries across core, app, and packages (#234)
* refactor(core): decompose editor factory and action modules

Split the monolithic editor factory and large action modules into focused
domain helpers:

- create.ts assembles context through bridge modules (clipboard,
  components, structure, undo) and delegates to graph-reads, graph-events,
  layout-runner, component-sync, and state factory
- structure.ts delegates to group, container-wrap, auto-layout-wrap,
  reorder, and state-toggle helpers
- selection.ts delegates to hit-test, overlays, container navigation,
  and read helpers
- clipboard.ts delegates to subtree-history, images, export, copy,
  fonts, and placement helpers
- shapes.ts delegates to pen actions and section-adopt
- components.ts delegates to focus and instances helpers
- alignment.ts delegates to flip-rotate helper
- text.ts uses explicit TextEditSession for snapshot comparison

New focused modules: nudge, variable-bindings, layout-mode,
page-viewports, tool-registry, color-space

Undo: history/position and history/snapshot helpers, hardened
batch/rollback with nested batch support and configurable limit

* refactor(core): split tool definitions by domain

Split the monolithic tool registry into domain-specific modules:

- read/ — selection, find, pages, fonts, components, nodes, query, jsx
- create/ — basic shapes, components, vector, JSX render
- modify/ — paint, effects, geometry, layout, state, text, update
- structure/ — basic, arrange, batch, hierarchy, replace, tree
- variables/ — bindings, collections, read, values
- vector/ — boolean, path, export, viewport
- analyze/ — colors, typography, spacing, clusters, diff, eval
- describe/ — summaries, tree, roles, layout-issues
- stock-photo/ — providers, requests, apply
- codegen/ — component-map, tokens

Split registry into core/extended tiers; refine schema and AI adapter

* refactor(core): restructure kiwi codec and instance overrides

Reorganize the Kiwi .fig codec into domain subdirectories:

- binary/ — codec, schema, protocol
- fig/ — file, import, parse (core, worker, transfer)
- node-change/ — convert, export-node, serialize, plugin-data
- instance-overrides/ — constraints, dsd, populate, props, resolve,
  symbol-overrides, symbol-props, sync, types

Vendored kiwi-schema/ left isolated

* refactor(core): split profiler, icons, IO, and add subpath exports

Profiler: speedscope-export, capture-session, hud-controller
Icons: api, svg, types, render, create-icons tool
IO: format registry and subpath exports
Canvas/color/text/vector: targeted cleanup

Add deliberate subpath exports: random, xpath, vector, color, canvas,
scene-graph, kiwi, design-jsx, io, tools, editor, layout, canvaskit,
profiler, text, lint, rpc, figma-api, constants

* refactor(vue): decompose canvas input, surface lifecycle, and controls

Canvas surface: gl-surface, kit-loader, render-loop, resize-observer

Canvas input handlers:
- move: drop-target, move-snap, duplicate-drag
- select: select-move, select-hover, select-hit
- resize: resize-rect, resize-vector, resize-start
- transform: rotation, marquee, pan, text-selection
- text-edit: navigation, clipboard, textarea lifecycle
- Shared: click-count, space-key, pan, pan-zoom, draw, raf-scheduler

Editor composition:
- commands split: actions, context, metadata, edit, selection, view
- menu-model split: command-groups, builders, types
- Gradient stop composable reuse in primitive root

Controls: fill, layout, typography, appearance, effects, stroke,
okhcl, prop-scrub, node-props, undo-batch, color-variable-binding
Variables/i18n/document/export helpers

Organize canvas, primitives, controls, editor, and variables into
cohesive module directories with package-local import aliases

Expose MenuActionNode/MenuSeparatorNode from public API

* refactor(app): split document IO, editor session, and automation bridge

Document IO: source-state, naming, writer, reload-source, reload-state,
imported-document, watch-targets, save-targets

Editor session: create, modules, types, accessors, computed, refs
Editor canvas: loader-overlay, collaboration-awareness,
context-selection, menu-actions, menu-model

Automation bridge: eval, tools, exports, files, selection, RPC fallback

AI/ACP: transport, map-update, permission, debug, chat effects/storage
Collab: awareness, graph-bindings, yjs-sync, follow, session, types
Shell keyboard: actions, bindings, clipboard, focus, nudging,
raw-events, registry, reserved, shortcuts, space-tool
Shell menu: app-menu, document-name, entry, files

Demo: colors, effects, helpers, section builders (components,
app-preview, effects, standalone, variables) — document.ts reduced
from 981 to 32 lines as pure orchestrator

Move app modules under src/app/ with organized domain structure:
editor, document, ai, collab, shell, automation, demo, tabs

* refactor(app): decompose UI components with provide/inject context

Split monolithic components using Reka UI-inspired namespace folders
with scoped provide/inject context — no prop drilling:

- CollabPanel/ — context, avatars, share, connected, join
- ColorPickerPanel/ — context, area, format, field groups, sliders
- MobileHud/ — context, action toast, tool badge, file menu, presence
- ProviderSettings/ — context, API key/type, endpoint, tokens, photos
- Toolbar/ — actions, types, desktop, mobile, tool button, flyout
- LayoutSection/ — types, auto-layout, flex, grid, padding, size, clip

Properties helpers: fill-okhcl adapter, fill-label, color-style-row
Menu: entry helpers, document-name rename, stale type removal

* refactor(mcp): split server into focused modules

- browser-rpc — WebSocket client management
- mcp-sessions — session lifecycle
- tool-output — response formatting
- tool-schema — Zod schema generation from ToolDefs
- jsx-preprocess — JSX source transformation
- result — result helpers
- tool-registration — MCP tool wiring
- auth — API key validation
- http-options — CORS/request handling
- stdio-bridge — stdio transport adapter

* refactor(cli): split analyze subcommands and shared helpers

- Analyze subcommands: clusters, colors, spacing, typography
- RPC data loading helper
- Migrate imports to targeted core subpath exports

* refactor(docs): split VitePress config and shared table component

Config helpers: sdk-sidebar, seo, labels, sidebars, locale-theme,
root-theme, locales

Shared SdkDataTable component replaces duplicated table markup in
SdkPropsTable, SdkEventsTable, and SdkSlotsTable

Update contributing and testing docs

* refactor(tauri): decompose desktop entrypoint

Split lib.rs into focused service modules:

- fig_container.rs — .fig archive/compression commands
- fonts.rs — font cache and system font enumeration
- menu.rs — native menu construction
- menu_events.rs — menu event dispatch and devtools toggle
- window.rs — main window show/focus lifecycle

* test: share domain test factories and migrate fixtures

New shared helpers:
- tests/helpers/scene.ts — makeSceneGraph factory
- tests/helpers/vector-network.ts — vertex/segment/network builders
- tests/helpers/fig-traversal.ts — all-node collection, type counts
- tests/helpers/undo.ts — undo test utilities
- tests/helpers/editor-history.ts — editor history test helpers

Migrate render, vector, fig-roundtrip, and undo tests to use shared
factories instead of inline fixture construction

* build: add structural lint rules, split vite config, update docs

Structural lint (oxlint.structure.json + lint/plugin.js):
- 20+ custom rules enforcing package boundaries, lifecycle patterns,
  naming conventions, and import discipline

Vite config split: raw-markdown, canvaskit-assets, pwa, server,
aliases, automation plugins

Remove legacy shims and utils superseded by SDK/core modules
Update AGENTS.md, CONTRIBUTING.md, eval-command docs, tsconfig

* fix(vue): normalize canvas directory casing and remove duplicate export

- Rename Canvas/ to canvas/ in git index to match #vue/canvas/* imports
  (PascalCase was correct for component primitives but canvas/ is a
  non-component domain directory)
- Remove duplicate ./random subpath export in core package.json

* fix: add #vue and #core Vite resolve aliases for dev server

* refactor(core): reduce remaining large modules

Split the remaining large core hotspots into cohesive domain modules while preserving public facades and behavior.

- Extract scene graph types, variables, node defaults, and vector-network helpers
- Decompose canvas renderer orchestration, state, paints, colors, lifecycle, labels, and delegated domain methods into renderer/ and labels/ subfolders
- Split Kiwi node-change, binary variable binding, layout, RPC, vector, JSX export, clipboard, design JSX, and Figma proxy helpers
- Replace collision-driven *Fn import aliases with namespace imports and enforce the pattern in lint

Validation:
- bun run check
- bun --filter @open-pencil/vue build
- bun run test:dupes

* fix(app): forward color input attrs

* fix(app): cover section drawing errors

* fix(editor): undo option-drag duplicates

* docs: document domain subfolder convention

* fix(app): handle undo redo on keydown

* refactor(app): dispatch shortcuts from keydown

* refactor: group prefixed domain modules

* refactor(app): use tinykeys for shortcuts

* refactor(core): group symbol override modules

* refactor(core): group fig kiwi container helper

* refactor(canvas): split overlay rendering modules

* refactor(vue): remove unused internal barrels

* fix(app): lay out demo components before instancing

* fix(app): restore demo badge spacing

* perf(canvas): split scene and overlay rendering

* refactor(vue): wrap wheel gesture lifecycle

* fix(canvas): wait for fonts before hiding loader

* docs: update unreleased changelog
2026-04-30 15:14:19 +03:00
Danila Poyarkov d692953d62 test: fix pre-existing failures and add coverage for new features
- Fix mcp-stdio.test.ts: defer heavy SDK imports to avoid describe()
  registration race when running full suite
- Fix mcp-server.test.ts: add server.close() to properly clean up
  sessions and pending requests between tests (eliminates 8 unhandled
  errors)
- Add mcp-path-scoping tests: path traversal, sibling dir, root prefix
  trick, valid paths
- Add editor-store-path tests: setPlannedFilePath with Unix/Windows
  paths
- Add MCP server tests: open_file/new_document registered when mcpRoot
  is set, absent when null

0 fail, 0 errors, 1108 pass
2026-04-22 17:20:09 +03:00
Danila Poyarkov 7513399fce fix(mcp): coerce string-encoded numbers in tool parameters
MCP clients often serialize numeric arguments as JSON strings.
Previously this caused validation errors on tools like node_move.
Now both the MCP server (zod) and AI adapter (valibot) coerce
string-encoded numbers to numbers, rejecting only genuinely
non-numeric values like 'abc'.

Fixes #207
2026-04-22 14:40:11 +03:00
Anton A S 583b03eb3b Harden ACP transport, MCP server, and add permission dialog
- Extract mapUpdate to testable module, dynamic import for @tauri-apps/plugin-shell
- Add warnings for unhandled ACP content types and empty tool titles
- MCP server: session limit (max 10), fix null WS comparison, guard JSX preprocessing
- MCP server: read version from package.json instead of hardcoded 0.0.0
- MCP tests: use port 0 (OS-assigned) to prevent collision
- Connection error handling with user-friendly messages, stale session recovery
- Agent crash detection via close handler, destroying flag, buildCrashChunks
- Port collision: detect EADDRINUSE in vite-plugin stderr and log clear error
- Production Tauri: spawn openpencil-mcp via shell plugin, orphan reuse via health check
- Permission confirmation dialog (reka-ui AlertDialog) with queue, 60s auto-reject timeout
- Health check in ProviderSelect hides ACP agents when MCP server unavailable
- Move DESIGN_CONTEXT to app constants, add ACP_PERMISSION_TIMEOUT_MS
- 36 tests across 3 files (acp-transport, acp-permission, mcp-server)
- Update CHANGELOG, README, CONTRIBUTING, AGENTS.md
2026-03-15 16:49:43 +03:00
Danila Poyarkov 1092e6b9f5 Unify MCP server: always proxy to live editor via WebSocket
Remove headless SceneGraph mode — the MCP server now always proxies
tool calls to the browser via WebSocket. One server, one architecture:

  HTTP :7600 — /health, /rpc (CLI), /mcp (MCP Streamable HTTP)
  WS   :7601 — browser connects, executes tool calls against live editor

- Delete http.ts (merged into server.ts)
- Delete headless SceneGraph, open_file, save_file, new_document
- Delete canvaskit-wasm dependency (no headless rendering)
- Add ws dependency for WebSocket server
- Rewrite tests with mock browser over real HTTP+WebSocket
- Simplify bridge.ts to 6 lines (just imports and starts the server)
2026-03-15 16:42:46 +03:00
Danila Poyarkov ba31fe7920 Add tests for MCP server makeFigma option
Test that external FigmaAPI skips file lifecycle tools and allows
tool execution without loading a document first.
2026-03-15 16:40:30 +03:00
mcdmags c847eab75c
feat(mcp): add export_image_file tool and fix headless PNG rendering (#108)
* feat(mcp): add export_image_file tool — saves PNG/JPG/WEBP to disk

The existing export_image tool returns base64 inline, which MCP clients
display in chat but don't save to disk. This new tool writes the image
directly to a file path, making exports usable for documentation,
mockup reviews, and CI pipelines.

Parameters:
- path: absolute file path to save (required)
- ids: node IDs to export (optional, defaults to all top-level nodes)
- format: PNG | JPG | WEBP (default: PNG)
- scale: 0.1–4 (default: 2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(mcp): add export_image_file tool and fix headless PNG rendering

**New tool: `export_image_file`**
Saves rendered PNG/JPG/WEBP images directly to a file path on disk.
The existing `export_image` returns base64 inline — useful for chat
but doesn't produce files. AI agents and CI pipelines need actual files.

Path validation runs before the expensive render call. The tool
respects `fileRoot` restrictions.

**Fix: headless PNG rendering now matches the editor**

1. `computeAllLayouts()` called after `render` creates nodes via JSX.
2. Bundled Inter font loaded from disk — tries `../fonts/` (installed
   package) then `../../public/` (project root). The web-based
   `fetch('/Inter-Regular.ttf')` fails in Node.js.
3. Text measurement uses `renderer.measureTextNode()` — the same
   ParagraphBuilder the renderer uses to draw text. Measurement and
   rendering use the same SkiaRenderer instance, same fontProvider,
   same font shaping. Zero hacks.

Export pipeline:
  ensureInterFont() → SkiaRenderer + loadFonts() →
  setTextMeasurer(renderer.measureTextNode) →
  computeAllLayouts() → renderNodesToImage()

**Tests added** (6 new tests in mcp-server.test.ts):
- export_image_file produces valid PNG after render
- export_image_file without ids exports all page children
- export_image_file without document returns error
- export_image_file with out-of-root path returns error
- JPG and WEBP formats produce non-empty output
- export_image_file is listed in tools

* Remove .cue directory and revert .gitignore changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Move computeAllLayouts into renderTree, reuse exportImage in MCP

computeAllLayouts was in the render tool but renderJSX has multiple
callers (render, replace_node, automation server). Moving it into
renderTree ensures layout is always computed after JSX rendering.

export_image_file in MCP server was duplicating export_image logic.
Now delegates to exportImage.execute and just handles the file write.

---------

Co-authored-by: Michael Magsuci <mcdmag@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-14 18:43:50 +03:00
Danila Poyarkov 38e7fdd4ac Fix hit-testing: scope hover to current page, click-through empty containers
- Scope hover hit-test to current page (was searching all pages including
  internal component page, causing ghost hover outlines)
- Frames/sections without visible fills or strokes are click-through
- Groups are always click-through (only children are hittable)
- Clipping parents reject hits outside their bounds
- Instances/components check children before falling back to fill check
- Switch test fixtures from material3.fig (55MB) / nuxtui.fig (82MB) to
  gold-preview.fig (537KB) for fast dev runs, gate heavy fixtures behind
  BUN_HEAVY_TESTS env var (enabled in CI)
2026-03-05 08:56:35 +03:00
Danila Poyarkov 601dc85aa8 Fix fileRoot test: await async exportFigFile 2026-03-03 17:19:49 +03:00
Peter 31d614a272
fix(mcp): harden HTTP transport defaults (#26) 2026-03-03 17:19:08 +03:00
Tela Andrews 274c9f7d61 Add MCP server edge-case tests for find_nodes and Zod validation
Covers a few gaps in the existing MCP test suite:
- find_nodes filtering by type (wasn't exercised at all)
- create_shape with an invalid type enum (Zod enforcement)
- create_shape with a missing required param (Zod enforcement)
2026-03-02 11:16:19 -08:00
Danila Poyarkov 7a5f6b3707 Add MCP server integration tests (closes #19)
13 tests using InMemoryTransport from @modelcontextprotocol/sdk:
- Tool registration (all 76 tools listed with descriptions)
- new_document, open_file, save_file lifecycle
- Tool execution through MCP protocol (create, fill, delete, query)
- Error paths: no document loaded, nonexistent node, invalid file path
- Full workflow: new → create → nest → query → delete

Fix @open-pencil/core dep in MCP package: workspace:* for local dev
(pnpm publish resolves this to the actual version at publish time).
2026-03-02 21:07:25 +03:00