- Resume persisted outbox jobs at startup without dropping writes when credentials are unavailable
- Serialize IndexedDB revisions and guard sync completion against concurrent saves
- Reconcile tombstones without reviving deleted documents and bound S3 metadata requests
- Require explicit CORS configuration instead of overwriting bucket rules during connection tests
- Keep ACP agents exclusive to the Design role and normalize invalid model limits
- Add one themed placeholder primitive for page, panel, dialog, and setup states
- Vertically center chat, code, assets, variables, and storage placeholders
- Let Vue replace the static boot splash instead of mutating global DOM from views
- Show one centered setup action instead of simultaneous error and empty states
- Refresh configuration after closing Settings without requiring route remount
- Keep document creation and refresh unavailable until setup is complete
- List local and remote documents from an explicit storage workspace route
- Open remote .fig documents into deduplicated storage-bound editor tabs
- Create locally durable documents only after storage configuration is complete
- Cover unconfigured guidance and mocked S3 document opening end to end
Co-authored-by: Rob Coenen <753704+rcoenen@users.noreply.github.com>
Fill the three biggest gaps that blocked authoring real designs in JSX:
- mask prop on any node: <Ellipse mask> / mask="luminance" / mask="vector"
sets isMask + maskType so sibling content clips (Figma's mask model)
- <svg> element rendering inline SVG markup into vector nodes, reusing the
iconify path pipeline (extractPaths + parseSVGPath + scalePathInfos)
- Instance overrides: <Instance of="X" overrides={{ 'childName:prop': v }} />
applies child overrides by name and records them so component sync keeps them
Adds gap tests and the shared scalePathInfos/finishIconRender helpers to
avoid duplicating the iconify pipeline.
- Register .fig and .pen files with desktop bundles
- Open associated files on startup and via single-instance handoff
- Reuse the existing Tauri file open flow without broad filesystem scopes
IS_TAURI is evaluated at module load time and may be false if
__TAURI_INTERNALS__ isn't injected yet. This prevented the
automation WebSocket from connecting on Windows and some Linux
configurations.
Previously the error was silently caught with console.error, leaving
Windows users with no indication why the automation bridge was missing.
Now shows a visible toast with install instructions.
Browser validation found 2 runtime issues:
1. TooltipProvider only wrapped EditorView, but <Tip> components in
AppToast rendered outside it → moved TooltipProvider to App.vue
to wrap everything including toasts.
2. SDK components call useEditor() but the app never provided the
editor via the SDK's injection system → added provideEditor(store)
in EditorView.vue so SDK primitives can access the editor.
Validated: zero console errors, canvas renders, demo shapes visible.
MobileHud now uses useCollabInjected() directly — same pattern as
CollabPanel — and handles share/join/disconnect internally.
Removes broken event handlers that were never defined in EditorView.
Sort classes using the same algorithm as prettier-plugin-tailwindcss,
configured for Tailwind v4 (reads src/app.css for theme).
Also sorts class strings inside twMerge() and tv() calls.
Eliminates 2 nested ternaries that conflicted with the formatter
(oxfmt strips parentheses that the lint rule requires).
Bugs fixed:
- Export response now wrapped in {ok, result} like all other commands
- btoa(String.fromCharCode(...data)) replaced with loop to avoid
RangeError on large images
- JSX export uses dedicated export_jsx RPC command instead of eval
with undefined selectionToJSX reference
Issues fixed:
- connectAutomation cleanup on component unmount via onUnmounted
- Bridge state encapsulated in startAutomationBridge closure instead
of module-level lets
- Previous browser WS closed on new connection to prevent leaks
- walkNodes returns boolean for early termination, find stops at limit
- Unused imports removed from node.ts (fmtList, nodeToData)
- Hand-rolled hex in node.ts replaced with colorToHex from core
- makeFigmaFromStore extracted to shared figma-factory.ts, used by
both ai/tools.ts and automation/server.ts
- Unused limit param removed from AnalyzeColorsArgs
- Unused groupBy param removed from AnalyzeTypographyArgs
- Extract shared RPC command handlers into packages/core/src/rpc/
(info, pages, tree, find, node, variables, analyze colors/typography/spacing/clusters)
- Add automation bridge (src/automation/bridge.ts) that runs in Vite/Bun process,
listens on localhost:7600 (HTTP) and :7601 (WebSocket)
- Add browser-side handler (src/automation/server.ts) that connects via WebSocket,
executes commands against live EditorStore with requestRender()/flashNodes()
- Supports eval, tool execution, and image export via RPC
- Refactor all CLI commands: file arg is now optional — omit it to connect
to the running app instead of loading a .fig file
- Bearer token auth generated per session, exposed via /health endpoint
- Vite plugin starts the bridge automatically during dev
- / shows empty canvas
- /demo loads demo content (for app.openpencil.dev landing link)
- Title template: '{page} — OpenPencil' via useHead
- 'Try it online' links in README and docs point to /demo
useEditorStore() now returns a Proxy that delegates to the current
active store at access time. No more store() wrapper functions,
try/catch guards, or passing stores through function parameters.
Open multiple documents in tabs within a single window, like Figma.
- Tab bar appears when 2+ documents are open, with close (×) and new (+) buttons
- ⌘N / ⌘T create a new empty tab
- ⌘W closes the active tab (last tab closes and reopens as Untitled)
- ⌘O opens .fig files in a new tab (reuses current tab if Untitled with no changes)
- Middle-click closes a tab
- Each tab has its own SceneGraph, UndoManager, viewport, and selection state
- Switching tabs remounts the canvas to rebind the Skia renderer to the active store
- Keyboard shortcuts, menu actions, and AI tools all operate on the active tab
Header row with app icon, double-click-to-rename document name, and
sidebar toggle button. Shows in both Tauri and browser.
When panels are hidden, a floating bar on the canvas provides the same
controls to restore the UI.
- AppToast component: top-center blue pill (info), red pill (errors)
- Global error handler catches window.error + unhandledrejection
- Link copied toast on share and copy link button
- use-toast composable: toast.show(message, variant)