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.
vue-stream-markdown bundles all 359 shiki grammar chunks by default.
We only need plain text code blocks in chat. Alias shiki to an empty
module — build drops from 20MB/377 files to 11MB/103 files.
- 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)
- Cloudflare Worker + Durable Object server (packages/collab/)
- One DO per room with WebSocket Hibernation for near-zero idle cost
- Yjs sync protocol for document state (CRDT merge)
- Yjs awareness protocol for cursors, selection, presence
- Persists doc state to DO storage
- CORS headers for cross-origin WebSocket upgrade
- Client composable (src/composables/use-collab.ts)
- Y.Doc ↔ SceneGraph bidirectional binding
- Awareness: broadcasts cursor position, selection, name, color
- Auto-join from /share/:roomId URL
- Suppresses echo loops between Yjs and graph updates
- CollabPanel component (src/components/CollabPanel.vue)
- Share button generates room link, copies to clipboard
- Join flow: paste link or room ID
- Avatar stack with colored initials and tooltips
- Name input persisted to localStorage
- Remote cursor rendering in CanvasKit
- Colored arrow cursor + name label pill per remote peer
- Remote selection outlines in peer's color
- Only shows cursors on the same page
- SPA redirect for /share/* routes (Cloudflare Pages _redirects)
- Save As prompts for filename via prompt() before downloading
- Save reuses the filename from last Save As (no repeated prompts)
- Banner in Safari/Firefox explaining the limitation with Chrome link
- Banner is dismissible
Safari requires the <a> element to be in the document for .click()
downloads to work. Also defers URL.revokeObjectURL by 100ms so the
browser has time to start the download. Extracted shared downloadBlob
helper used by both file save and image export fallbacks.
Triggers only when a file handle (browser File System Access API) or
file path (Tauri) exists. Tracks savedVersion to skip redundant writes.
Silent failure — user can always save manually.
Reka-ui Menubar in the left panel header, hidden when running in Tauri
(native menu handles it). Includes Open, Save, Save As, Export, Undo/Redo,
Zoom, Group/Ungroup, Component creation, Bring to front/Send to back,
Auto layout, Text formatting, Alignment.
- sceneNodeToJsx() in @open-pencil/core: converts SceneNode subtree to JSX
- CodePanel.vue with Prism.js highlighting, line numbers, copy button
- Third tab in properties panel: Design | Code | AI
- 14 new tests covering shapes, text, layout, effects, multi-selection
Tool part type is 'tool-create_shape' (name embedded in type field),
not a separate toolName property. Extract via part.type.replace('tool-', '').
Mock transport now emits proper UI message stream protocol:
tool-input-start → tool-input-delta → tool-input-available → tool-output-available.
- Track click count with timing/proximity in onMouseDown
- Double-click in text: select word (was only handled in dblclick)
- Triple-click in text: select all
- Double-click to enter editing: select word at click position
- Add selectLine/selectLineAt to TextEditor
Use markRaw to prevent Vue from deep-proxying the Chat class
instance, which broke its internal Vue refs. Add dedent for
multiline prompts.
Tests use mock transport by default. Set TEST_REAL_LLM=1 and
OPENROUTER_API_KEY to run against real OpenRouter.
Replace src/kiwi/ copies with re-exports from @open-pencil/core.
Extract shared sceneNodeToKiwi into kiwi-serialize.ts, used by
both fig-export.ts and clipboard.ts.
jscpd: 15.6% → 1.7% duplication (35 → 18 clones, -4459 lines)