44 KiB
Progress
Last updated: 2026-10-24.
Phase 3: local-first design and whiteboard tool
Goal: grow paper-rs into a Paper / Figma / Excalidraw-class tool. Figma and Paper are studied
clean-room (behavior only, analysis/FIGMA_NOTES.md, analysis/PAPER_ARCHITECTURE.md).
Excalidraw is MIT and studied from source (analysis/EXCALIDRAW_NOTES.md); ported algorithms
are credited in THIRD_PARTY.md.
| Milestone | Status | Evidence |
|---|---|---|
| Analysis notes | ✅ done | analysis/FIGMA_NOTES.md, analysis/EXCALIDRAW_NOTES.md, fixtures in tests/fixtures/excalidraw/ |
| P1: local projects, home screen, autosave, atomic writes, versioned format | ✅ done | src/project/, src/session.rs, src/io/atomic.rs, tests/projects.rs::m1_*, milestone1_home.png, milestone1_trash.png |
| P2: multiple pages, infinite canvas per page | ✅ done | src/editor/pages.rs, src/ui/pages.rs, tests/projects.rs::m2_*, milestone2_pages.png |
| P3: Figma core | ✅ done | src/layout.rs, src/editor/{structure,vector_ops,text_edit}.rs, src/render/{draw,tess}.rs, src/ui/inspector.rs, tests/figma.rs, tests/perf.rs, milestone3_figma_core.png, milestone3_canvas.png |
| P4: Excalidraw core | ✅ done | src/render/rough.rs, src/connector.rs, src/editor/whiteboard.rs, src/library.rs, src/ui/library.rs, tests/excalidraw.rs, milestone4_whiteboard.png, THIRD_PARTY.md |
| P5: inspector, layers drag-reorder, command palette, shortcuts | ✅ done | src/ui/{inspector,palette}.rs, src/ui/mod.rs (layers drag, help), src/editor/{commands,shortcuts}.rs, src/macos.rs, tests/workflow.rs, milestone5_palette.png, milestone5_shortcuts.png, milestone5_inspector.png |
| P6: import/export (PNG/SVG/PDF, .excalidraw, SVG), cross-project paste | ✅ done | src/io/{pdf,export,excalidraw,svg_import,clipboard}.rs, inspector Export section, CLI --export*/--import, tests/io.rs, milestone6_*.png |
| P7: MCP server for AI agents (Paper-compatible tools, HTTP + stdio bridge) | ✅ done on Linux; ⏳ Mac run pending (steps below) | analysis/paper-mcp.md, src/mcp/, src/bin/paper-rs-mcp.rs, tests/mcp.rs, milestone7_mcp.png, milestone7_mcp_live_xvfb.png |
P7 notes: MCP server
-
Study (
analysis/paper-mcp.md): Paper's desktop app serves MCP over streamable HTTP on127.0.0.1:29979/mcp(hono + MCP SDK), forwards calls into the renderer's editor, guards against browsers and DNS rebinding, and ships a Gopaper mcpstdio relay. 29 tools; the write path iswrite_html(HTML + inline CSS → layers) andupdate_styles(CSS maps). Studied with REA (behaviour only; raw dumps in gitignoredanalysis/raw/). -
Server (
src/mcp/server.rs, rmcp 3.5): streamable HTTP on127.0.0.1:29980/mcp(PAPER_RS_MCP_PORT,PAPER_RS_MCP=0to disable), on its own Tokio thread. Calls are sent to the winit loop (UserEvent::Mcp) and run on the UI thread viamcp::exec, each as oneEditor::edittransaction (one undo step, live repaint, autosave). Edits to other pages go to that page's own document and history (Editor::with_page) without switching the view. -
Bridge (
paper-rs-mcp): rmcp stdio server that lists tools offline and relays calls to the app over HTTP, reconnecting after app restarts. -
Tools (
src/mcp/tools.rs): 53 tools at the time (61 now), using Paper's names and argument shapes where there is an equivalent (see README), pluscreate_shape,select_nodes,undo,redo.write_html(src/mcp/{html,css}.rs,exec.rsBuilder) maps flex/block layout onto auto layout with CSS stretch semantics, text styles and rich-text runs, backgrounds, gradients, borders, images, inline SVG and clones. -
Verified:
tests/mcp.rs(12 tests: every tool on a headless session, rmcp client over HTTP and through the bridge, browser/Origin rejection, a raw 2026-07-28 request through the bridge, screenshots). End to end on Linux: the real app under Xvfb, driven by Claude Code (claude -pwith the server added over HTTP, then over the stdio bridge) drew a frame with a heading, paragraph, button and sticky note that appeared live in the window (docs/screenshots/milestone7_mcp_live_xvfb.png). That run found a bug the rmcp-only tests missed (bridge results lackedresultTypefor 2026-07-28 clients), fixed in01e0836. The macOS target type-checks (cargo check --target aarch64-apple-darwin --all-targets). -
Mac verification steps (not run here: no access to the Mac from this machine). In
/Users/Shared/projects/paper-rson the Mac:export CARGO_TARGET_DIR=~/.cache/paper-rs-target # keep build output out of the synced tree cargo build --release $CARGO_TARGET_DIR/release/paper-rs & # app; log says "MCP server listening on http://127.0.0.1:29980/mcp" curl -s http://127.0.0.1:29980/x # → {"status":"not_found",...The MCP endpoint is /mcp."} # HTTP client claude mcp add --transport http --scope user paper-rs http://127.0.0.1:29980/mcp claude -p "Use the paper-rs MCP tools: create_file 'Mac check', open_file it, create a 480x320 \ artboard 'Hello', and write_html a heading 'Hello from the Mac' into it." \ --allowedTools "mcp__paper-rs__*" # stdio client (through the bridge) claude mcp remove paper-rs --scope user claude mcp add --scope user paper-rs -- $CARGO_TARGET_DIR/release/paper-rs-mcp claude -p "In paper-rs, add a yellow sticky note saying 'stdio works' next to the Hello artboard." \ --allowedTools "mcp__paper-rs__*" # Codex works the same way: # codex mcp add paper-rs --url http://127.0.0.1:29980/mcp # codex mcp add paper-rs -- $CARGO_TARGET_DIR/release/paper-rs-mcpDone when the "Mac check" project is open in the window with the "Hello" frame and its heading (and the sticky note) visible, and ⌘Z removes each agent edit in one step.
-
Not carried over: comments, design tokens, AI image generation, video export, teams; moving layers between pages; remote image URLs (see README Known gaps).
2026-10-28: overflow lint rule and get_bounds
lint_designhas anoverflowrule (src/mcp/lint_overflow.rs): layers past their artboard or clipping parent, and text taller than its box. Scrolling frames and what is pinned in them are exempt; it is never auto-fixed. It is a pure function over a node set, so a scope filter can pass any subset.- New tool
get_bounds {nodeIds?, pageId?, depth?}(src/mcp/bounds.rs): every layer's rect in one read (about 2000 rows per call, thencontinueWith). Tool count is now 87 (one more than before; if another branch also adds a tool, recount). Tests:tests/mcp_bounds.rs.
2026-10-28: agent reads no longer look like edits
- Read-only tool calls (
get_node_info,get_children, ...) have honest verbs ("Looking at the design") and light the "Working" pill for 1.5 s instead of 6 s, so it goes out right after a sweep ends. - A sweep of reads draws at most one layer outline (a read outlines only when no other read ran in the last 2 s) and no longer pans the follow-agent camera; edits behave as before. "Is this a read" comes from
mcp::tools::edits_doc.
2026-10-28: 0.9.0 (presentations, critique, several agents, lighter chats)
- Export the screens as a presentation PDF: cover, contents and a slide per screen (File menu, palette,
--format presentation,export_presentation). critique_designreviews finished work through personas (vision, UX, flow, QA, visual, accessibility, copy, end-user);design_lessonskeeps the findings the person approved for every later design.- Several agents on one project: named CLI agents (
--as), conflict reports andifRevon writes,claim_nodes/release_claims/list_claims, scoped delegate builds and per-agent undo. 94 tools. - Screens keep their device height and scroll what does not fit; sections group screens on the canvas; pages can carry icons.
- UI kits for shadcn, MUI, Mantine, Bootstrap and Chakra;
write_htmlfollows the browser more closely. - Memory: chat screenshots move to files (kept 14 days), thumbnails stay small, canvas images decode only for the GPU. A long chat dropped from about 1 GB to about 400 MB.
- Fix: long answer options in a question card wrap instead of widening the whole chat.
2026-10-28: 0.8.4 (demo video, fresh screenshots)
- README opens with a time-lapse of designing a habit-tracker app (
docs/demo.gif, full videodocs/demo.mp4), and every screenshot is retaken from that design. - Fix: the Performance overlay reads its settings from the project config folder, so
PAPERCRAFT_HOMEisolates it too.
2026-10-28: 0.8.3 (menus, panel grips, narrow windows)
- Menus open 6 px below their button, are at least 180 px wide and show an icon beside each item. Below 1100 px the hamburger opens every menu in one popup.
- Side panels: a slim rounded grip in the gap resizes them (double click resets) and
/hides or shows both. Panels size to their content and step aside when the window is too narrow for the canvas. - Sharper window buttons (pixel-snapped glyphs); the hamburger morphs into a back arrow while the File / Edit strip slides open and shut.
- Fixes: lint Fix buttons no longer cover the issue text; review rows keep their badge and revert button off the hover edge; the Export button stays on one line.
2026-10-28: 0.8.2 (refresh-rate cap, accurate RAM/CPU, toolbar)
- Settings > Performance > Refresh rate is now a real frame cap applied live (30 to 360 Hz, 60 by default); Auto draws as fast as the display allows and is the default on macOS.
- Performance Monitor: RAM is the private working set and CPU is divided by logical cores on Windows, so both match Task Manager.
- Windows and Linux toolbar: a hamburger button shows or hides the File / Edit strip (remembered); the close button uses the theme's danger colour on the acrylic surface, with hover animation and a pointing-hand cursor.
2026-10-28: 0.8.1 (Performance Monitor on Windows, Design tab speed)
- Performance Monitor: RAM (working set) and CPU now show on Windows; before they were
n/athere. The overlay and slow-frame badge judge frames against a refresh rate you pick in Settings > Performance: 30 to 360 Hz (144, 165 and 240 included), 60 by default. - Design tab: the lint and the open-feedback scan are kept until the document changes instead of walking every node each frame; a 12.6k-node page no longer drops to a few FPS while the tab is open.
- Windows and Linux: window buttons in the toolbar; controls and acrylic come from craft-ui.
2026-10-27: 0.8.0 (screens, packed pages, canvas performance)
- Screens (
specs/screens): a frame can be declared a screen or an overlay (drawer, modal, sheet) withset_screens, inspector toggles, or Declare N screens on the page. Agent edits to layers hidden behind an overlay or opaque layer in their screen are refused unlessallowHidden; partly covered ones are reported with their visible share.get_basic_infolists screens,get_node_infotells a layer's visibility,lint_designskips hidden layers, On click lists screens, nested screens get canvas labels. - Pages and saved versions are stored as zstd-compressed compact JSON (
.json.zst, project format 3): the 68 MB Interview project is 1.3 MB on disk and loads faster. A legacy.jsonstill reads and is replaced on the next save; older builds refuse format 3. - Canvas performance: the editor logs which layers each change touched, so the scene cache re-tessellates only those and keeps no copy of the document (443 → 346 MB on Interview). Zoom holds its geometry until it settles, tiny layers are skipped, vertices are 24 bytes, redraws follow vsync, the idle pointer no longer redraws, freed memory goes back to the system when idle, off-screen frame labels are skipped, and the layers panel keeps its row heights between frames (panning a large page: ~9 → ~0.7 ms a frame).
- Frame labels: none on frames too narrow on screen, overlapping labels are dropped (selected and larger frames first), long names end in an ellipsis.
- Dropdowns size to their longest option (capped), keep the name of
A / B / Nameoptions, scroll when long and show the full name on hover. - Settings: a Performance page (FPS, frame time, RAM, CPU, GPU, optional overlay and slow-frame badge), agent logos in the agent rows; the Chat page no longer collapses.
2026-10-25: 0.8.0 (properties modal, row merges)
- Properties modal (inspector, on a main: Edit properties…): a tab per property (name, kind, layer, default, delete); an options property as a Table (a row per option, a column per layer · style; colours picked from the project's variables with a swatch, visibility as a toggle, icon, image and text fields; add or remove options and columns) or as Source (each option's changes as JSON with syntax highlighting); templates from the + Property menu; Save replaces them all. The inline list is name, kind and a remove button. The
statustemplate writes colours the project has as variables as those variables. - Canvas overlays (agent glow, agent outlines, review outlines, the agent-elsewhere chip) sit below modals instead of showing through them. Two segmented controls in one row no longer share widget ids (the second did not respond).
merge_variantskeeps what each moved instance shows (texts that came from its old variant), carries nested instances' property values (a badge's Tone) in the options, and takestextLayersfor state texts only.
2026-10-25: 0.8.0 (prototype links tool, merge icon colours)
set_prototype_links: agents can link a layer to a screen for Present mode (back arrows, rows, tabs); before, only the inspector could.merge_variantscompares an icon's colour (keeping its variable), now that icons are compared as one unit.
2026-10-25: 0.8.0 (data safety fixes)
- Sync no longer throws away an instance written over a nested one the way older versions stored it (its own root, no swap): it becomes a swap of the main's child at that place. Projects built before #11 lost such layers (a checked checkbox, a status badge) on the next edit of the outer main.
- Icons: the HTML parser keeps attribute names in lowercase, so the icon and plugin code never removed the SVG's original
viewBox; since #14 that made<x-paper-icon>(and icon swaps) scale twice and come out tiny. Unreleased builds only; 0.7.3 is not affected. merge_variantsrefuses merges that would leave one option and changes nothing then.
2026-10-25: 0.8.0 (merge variants, pinned bars)
merge_variants: per-state components (Tab bar / Hoje,Status / Sucesso...) become one component with an enum property. Each variant's differences from the base, matched by layer path (fills, colours, borders, weight, radius, visibility, icons; texts on request), become its option; every instance on every page moves to the base with its value; the base takes the family name; the emptied variants are deleted. A variant with layers the base lacks is left out and reported.- Absolute children of a scrolling screen stay pinned while its content scrolls (a tab bar, a floating button), and do not add to the scroll range.
2026-10-25: 0.8.0 (fix #22)
- #22: a hug column whose width comes from its parent (stretched or filled) re-measures its height once its text wraps at that width, and the parent measures again, instead of keeping a height measured while the text was narrow.
2026-10-25: 0.8.0 (component properties)
- Main components carry properties (
Node.props):text,bool,iconandenum, whose options are changes in the overrides format (a layer name,$rootor a pathItem > Label, to a text or styles that may holdtext,iconandvisibility), so they restyle, retext, show, hide or re-icon any layer. Instances keep their values (prop_values) as overrides, so main edits keep them; changing the definitions re-applies every instance on every page. - Templates:
text(a property per text layer),active(which item of a tab bar, nav or segmented row is active) andstatus(options with a colour tint the root and colour the label and icon). set_component_props,set_instance_props,props='{...}'on<x-paper-instance>, properties inget_components,{Prop}in code link templates, and an inspector section (a control per property on an instance; Texts and Active item templates on a main). Icon swaps keep the layer's name and link (achildrenoverride); recoloured icon shapes inside instances now keep their colour after a sync.- Spec:
specs/component-props/spec.md.
2026-10-25: 0.8.0 (scrolling screens)
- Screens keep their size and scroll what overflows: a frame's
scrollflag (CSSoverflow: auto|scroll,overflow-y-autoin the Tailwind export) makes it fixed height and clipped, and the wheel over it scrolls its content on the canvas and in Present, then pans once the content ends. The offset is view state, not saved; hit-testing and overlays follow it. - Toggles: a scroll button beside each screen's name (shown when it scrolls, overflows, or is hovered), Scroll overflow on a selected frame, and Screens scroll on the page for every screen at once. A fit-content screen that grew goes back to its min height.
- Agents are told to keep screens at their size and use overflow:auto for long content.
2026-10-25: 0.8.0 (fixes #19, #20, #21)
- #19: a clipping frame draws its border after its children, so filled rows no longer hide it. Clipping stays rectangular: children's square corners still show past a rounded border.
- #20: a border written with a colour variable (
1px solid var(--border)) binds the stroke, a literal one unbinds it, and reads showvar(--name)for bound strokes.find_nodessays when it stopped at 500 matches. - #21: hug (fit-content) frames grow around growing children (CSS flex-basis auto) instead of staying at their min height; with short content the min height stays and the growing child fills it, so a pinned footer works.
2026-10-25: 0.8.0 (follow agents, skeletons)
- Follow agents (Settings, Chat; on by default): in Design and Chat mode alike, the canvas pans, and switches page, to the layer any agent changes (the chat, Claude Code over MCP, a helper). Moving the canvas yourself pauses it for a moment; off, a chip offers the agent's page.
- Skeletons: the moment the model starts writing a tool call (
ToolDraftandToolInputevents from Claude Code's stream and the API), the canvas shows a shimmering skeleton over its target, or where a new artboard goes, labelled with what it is doing, until the call has run. Fresh agent content comes in through a skeleton that fades, so an outline never frames empty space. - Top bar: the title and the agent pill leave room measured from the buttons on the right, so nothing overlaps at narrow widths.
2026-10-25: 0.8.0 (fixes #14 to #18)
- #14:
write_htmlfits an<svg>viewBox into its width and height like a browser (centred, aspect kept). - #15: a note on something placed straight in its artboard treats the artboard as the screen: tidy keeps cards off it instead of in its free space.
- #16:
not-an-instanceno longer matches look-alikes with a different icon; its fix names the<x-paper-instance>to write (swap_instance refuses plain frames). - #17: instances with their own text re-measure it when the main's text style changes, so hug buttons keep fitting their labels (also covers the size reset after a swap).
- #18:
lint_designreports borders whose hex equals a color variable, andfixbinds them (stroke), so a mode switch reaches them.
2026-10-25: 0.7.3 (QA sub-agent, helper agents, auto mode, mid-turn messages, isolated dev settings)
- QA sub-agent: Claude Code chats always get a Sonnet
qaagent that lints and screenshots built frames and returns only findings, so screenshots stay out of the main context. - Helper agents (
src/mcp/delegate.rs, Settings, Chat, Helper):delegatehandsqa,readorbuildtasks to a cheaper agent CLI (pi with MiniMax M3.1 Flash, opencode, an Ollama-backed agent, any command taking{prompt}). It drives PaperCraft throughpapercraft-cli, runs off the UI thread, and only its last 6,000 characters come back. With a helper set, the chat agent orchestrates: keeps the brief and taste, delegates review, bulk reading and fully specified edits. - Dev config isolation:
PAPERCRAFT_HOMEalso isolates the chat settings ($PAPERCRAFT_HOME/.config), so a second copy for testing never touches the installed app's settings. - Live check in an isolated copy:
delegateran the pi helper (MiniMax M3.1 Flash) as QA, a message added mid-turn went Waiting then Seen, and the agent drafted #13. - Fixes #10, #11, #12, #13:
- #10:
move_nodesto another page keeps mains' component keys and relinks every instance to moved nodes; newrename_filerenames a project. - #11: swapped nested instances persist as swap overrides (
InstanceLink.swap), preserving component identity across outer component updates and copies. - #12:
move_nodesto another page takes the moved nodes' annotations along; detached notes resolve asDetached #n. - #13:
get_computed_stylesreports a text colour bound to a variable asvar(--name).
- #10:
- Auto mode (the composer's Auto chip): when a turn ends on
ask_userquestions, the first (recommended) option of each is sent and the chat goes on, until Stop or a turn hits its budget. The agent is told to put its recommendation first, default what only the user can know, and end each turn with next steps. - The Present button uses a play icon drawn like the icons beside it. Composer chips wrap instead of running under Stop and Add. Top bar: the agent pill drops its text when it would run under the buttons on the right.
- While a chat turn works, the composer's Add (or Enter) queues a message (
src/mcp/steer.rs). It rides on the agent's next PaperCraft tool result for that chat, so every backend reads it mid-turn; the composer shows each as Waiting or Seen. What no tool call picked up is sent as the next message when the turn ends, or goes back into the box after Stop.
2026-10-24: 0.7.2 (UI kits, code links, issues from the app, design playbook)
- Code links (Figma Code Connect style): a main component carries its library import and a React and an egui template (
set_code_link). The React, Tailwind, inline JSX and egui exports write linked instances as that code, with overrides filled in and imports gathered once; the AI spec lists them as Library Components with a "use it, do not rebuild it" rule and stops describing their inner layers. - UI kits (
src/ui_kits.rs,apply_ui_kit): shadcn/ui, Ant Design, React Bits and egui, each with Light and Dark variables, tokens and components in its look and density, all linked. The chat brief asks for the code library and starts from its kit. - Issues from the app (
src/issues.rs): the agent'sreport_issuedraft card (Send or Discard) and a Report a PaperCraft issue form, filed on the repository in Settings (token in the keychain, else thetealogin), de-duplicated by title against open and closed issues. - design-playbook in the chat: with the
playbookCLI installed, the brief asks whether to follow the playbook and the agent looks rules up withdesign_rules, citing ids in its report. - Top bar: project name and page picker on one centred row; the two-row title had outgrown the bar and covered the canvas.
- Evidence:
src/ui_kits.rs,src/issues.rs,src/mcp/codegen.rs(linked_components_export_as_library_code),specs/ui-kits/spec.md.
2026-10-24: 0.7.1 (rename fix)
- Enter, Escape and clicking away end a rename again (projects on the home screen, library assets, versions). The fields took focus back before checking whether they had lost it.
2026-10-24: 0.7.0 (images, research, plugins, fewer tokens)
- Images in chat: paste from the clipboard (Cmd/Ctrl+V with an image-only clipboard now reaches the composer), thumbnails in the composer and in sent messages, a lightbox, image-only messages. Attachments are sent at most 1568 px (originals kept);
zoom_imagereads a region of the original. - Brief and finish:
ask_userdraws questions as a card of clickable answers (palette swatches, reference image tiles, the recommended answer marked). The chat rules ask for a brief before the first design, variables and components first, sizes from the agreed density, a short report and next steps as a card. - Web research (the composer's Web chip):
fetch_reference_images(public addresses only, icons, logos and backdrops dropped, social cards last), a References gallery that dims what the agent did not keep; on Claude Code a Haikuresearchersub-agent does the searching. - Image generation, opt-in (Settings, Chat, Images): Codex (offered once when signed in with a ChatGPT plan), Google Flow through
gf, or any command ({prompt},{aspect},{ratio},{out}, passed as data). - Plugins (Settings, Plugins): Avataaars, DiceBear, Iconify, Lorem Picsum, Flags, QR codes, placed with
<x-plugin use="…">, cached on disk. SVG import draws masked groups as images (Avataaars). - Tokens: lean Claude Code sessions (42.7k to 20.6k prompt tokens), deferred tool schemas and context editing on the API, a fresh session with a recap past 120k (or on demand from the meter), per-turn cost for resumed Claude Code sessions, a 1M context meter, a turn budget. Model and effort pickers for Claude Code.
- Also: page switcher in the Chat top bar, steps stay folded while a turn runs (no flicker),
lint_designasks 24 px targets on desktop screens, the installer keepspapercraft-mcpcurrent. - Evidence:
src/mcp/{refs,imagegen}.rs,src/plugins.rs,src/io/svg_import.rs,src/ui/{chat,chat_msgs,chat_agents,settings}.rs,src/chat/{anthropic,claude_code}.rs,tests/chat_tools.rs,specs/chat-tokens/spec.md.
2026-10-22: 0.6.1 (agents via ACP, annotations)
- Agents are found through the login shell's PATH (read once at startup, 3 s limit, merged with the app PATH and the usual folders), so an app opened from the Dock finds Claude Code, Codex and the rest. No install instructions anywhere; a missing agent shows Not found with Set path… and Docs.
- ACP backend (
src/chat/acp.rs): any agent that speaks the Agent Client Protocol over stdio, with PaperCraft's MCP server, streamed updates, permission flow, cancel and load. Registry as data insrc/chat/agents.rs: native agents found on PATH, npx agents off until enabled, custom agents by name, command and arguments. Verified here: OpenCode works; Gemini CLI's server currently refuses ACP clients; Cursor needscursor-agent login; Pi works throughpi-acpafter enabling. - Agent panel over the chat column (Detected, Enable via npx, Not found, API endpoints, Custom, search), a Connect an agent setup card, a sign-in card, and multiple
@ Selectionchips. - Settings → Chat redesigned: one agents list with details, endpoints, defaults, scrolling page on one label grid.
- Send bug: a turn that cannot start keeps the typed text and shows an error row with Retry, never a silent no-op (
a_failed_start_keeps_the_text_and_shows_an_error). - Annotations: notes with leader lines on a page, six categories with keys (style comes only from the category), project and per-page templates with built-ins and a Customize modal, view modes (⌘⇧A), Annotate tool (N), side view, inspector section, Annotate with AI, six MCP tools (
annotatebatches), and comments inget_design_specand the code exports. Exports and Present leave annotations out. - Evidence:
src/chat/{path,agents,acp}.rs,src/ui/{chat_agents,annotations,annotations_canvas}.rs,src/model/annotation.rs,src/editor/annotations.rs,src/mcp/tools.rs,tests/annotations*.rs,specs/chat-agents/,specs/annotations/,docs/chat.md,docs/annotations.md.
2026-10-21: Chat mode
- Chat layout next to the canvas (⌘J,
Design | Chat), with three backends: Claude Code and Codex through their CLIs (claude -pstream-json,codex exec --json) against the app's MCP server, and the Anthropic API with a key in the OS credential store. One event stream for all three; tool calls run through the same MCP dispatch, so runs, review and revert behave as before. - Thread: markdown replies, thinking row, tool cards, step groups, plan from
TodoWrite, approval for destructive tools on the API backend, run checkpoints with Restore to here, usage line and context meter,@ Selectionand image attachments, Stop. - Chats live in the project's
chats/folder. Settings → Chat holds CLI paths, the default backend and the API key. - Offline CLI fix: an offline save writes no camera, and a top-level shape created without a position goes to the next free spot instead of the origin.
- Evidence:
src/chat/,src/ui/chat.rs,src/ui/chat_msgs.rs,src/ui/chat_md.rs,src/ui/settings.rs,src/mcp/exec.rs,tests/fixtures/chat/,specs/chat/,docs/chat.md.
2026-10-21: Agent runs and change review
- Agent editing calls group into runs, snapshot each edited page as
Before agent run N, and end on 30 seconds idle,finish_working_on_nodes, or project switch. Runs persist underruns/and keep the newest 50. - Agent view lists runs and supports review, whole-run revert, per-change undo and accept all. Review adds canvas outlines and a finish toast; palette commands review or revert the last run.
- Id-paired page diffs power review; History version compare UI remains todo. Completed with full restore (including variables, modes and tokens), tool edit classification, and wasm support.
2026-10-28: Incremental lint
Agents re-linted whole pages after every edit. A bounded change log (src/editor/changes.rs, the
last 10000 layer changes, each with the client that made it) now records which layers every
commit, undo and redo touched; variable edits count as touching the layers bound to them. Write
tools return rev, get_basic_info reports it, get_changes {since, page?, exclude?, by?} lists
the changes, and lint_design {since | changedOnly} checks only those layers plus their
ancestors, descendants and siblings (src/mcp/incremental.rs), with covered saying what ran.
Tests: tests/incremental.rs, unit tests next to the code, the attribution check in
tests/mcp.rs::parallel_sessions_get_their_own_runs.
2026-10-21: Design libraries and offline CLI
- Variables are now collections with modes, slash groups, four types and aliases, stored in
design.json(project version 2; 0.4.0 refuses migrated projects). Each page keeps a derived cache; every edit is one undo step per page, and the page-only removals are gone. Per-page active mode per collection. - Fields bind to variables (stroke, text color, size, gap, padding, radius, font size, line height, opacity, visible) in the inspector and with
bind_variable; gradients do not bind yet. - Instances work across pages (
component_key, source page or library); deleting a main flags its instances. - Libraries: publish a snapshot (
library.json), enable, update with review (overrides kept), disable, and keep working when the source is missing. Variables modal, Libraries modal and Assets view in the UI;create_collection,delete_variables,bind_variableand five*_librarytools in MCP (61 tools). papercraft-cli call --offlineandpapercraft-cli shellrun the tools in-process on the project store, refuse a project the app has open, and fall back to the software adapter for screenshots. Seedocs/agents.md.- Evidence:
src/variables.rs,src/editor/design.rs,src/libraries.rs,src/cli.rs,tests/{libraries,cross_page_components,variables_ui,assets_panel,cli_offline}.rs, specs inspecs/design-libraries/andspecs/cli-offline/.
P6 notes
- Export (
io/export.rs): scope = selection, each frame, page or all pages; format = PNG (1–4x, transparent or white), SVG or PDF. Several PNG/SVG outputs go into a chosen folder (named after the frames/pages); PDF is always one file with one page per item. Inspector Export section, ⌘⇧E / ⌘⌥⇧E / ⌘⌥P, copy as PNG (⌘⇧C) / SVG, and the CLI. - PDF (
io/pdf.rs): hand-written PDF 1.7 writer over the shared draw ops — paths (quadratics elevated), solid fills, axial/radial shadings (stitched functions for 3+ stops), images as Flate-compressed RGB XObjects with an alpha soft mask, strokes with caps/joins/dashes, ExtGState opacity and all 16 blend modes, frame clipping. Verified by structure tests and by rasterizing with pdftoppm. - Excalidraw import (
io/excalidraw.rs): scenes and clipboard JSON (rectangles, ellipses, diamonds, text, lines, arrows with bindings/elbows/heads, freedraw with pressures, images fromfiles, frames, sticky notes with their text, groups → group layers, rotation baked into paths, hand-drawn style kept with the element's seed and fill style);.excalidrawlibv1/v2 into the library. - SVG import (
io/svg_import.rs, usvg): paths with transforms, fills (solid, linear, radial), strokes, opacity, embedded raster images, nested SVG and text outlined with Inter. - Clipboard (
io/clipboard.rs): copy writes apaper-rs/clipboardJSON payload with image bytes to the system clipboard (arboard), so layers paste between pages, projects, windows and restarts; paste also takes Excalidraw JSON, SVG markup, images and plain text. Our own last copy pastes from the internal clipboard (offset on the same page). - Drag-and-drop of
.svg,.excalidraw,.excalidrawliband images onto the canvas, and ⌘O imports into the current page.
P5 notes
- Inspector: grouped macOS-style sections (header actions, alignment icons, position & size with per-corner radii, constraints / sizing, auto layout, text, fills with gradients and images, stroke, layer blend/opacity, hand-drawn, arrow, sticky). Number fields scrub and coalesce into one undo step; edits inside instances become overrides.
- Layers tree: drag rows to reorder — above / below a row, or into a frame or group (the middle of the row); the canvas position is kept and the move is one undo step.
- Command palette (⌘K, also ⌘⇧/): fuzzy search over every bound action, all tools, pages, library items and layers (select + zoom). Arrow keys, Enter, Esc.
- Shortcuts: one binding table drives dispatch, the grouped help panel (⌘/ or ?), the egui menu bar and the native macOS menu (⌘ chords become key equivalents; single keys are shown in titles). Added Tab/⇧Tab sibling selection, ⇧Enter parent, ⇧H/⇧V flip, ⌘⌥C/⌘⌥V copy/paste properties, ⌥-drag duplicate (one undo step), PageUp/PageDown pages, ⌘R rename, ⌘\ hide UI, ⇧0 zoom 100%, Excalidraw number keys. 100+ bindings, checked for conflicts by a test.
- Deferred: Figma's number-key opacity (the digits pick tools, as in Excalidraw), drag of multiple layers at once in the tree, customizable shortcuts.
P4 notes
- rough.js 4.6.4 port (
render/rough.rs): RNG, lines, curves, ellipses, SVG paths, solid, hachure, cross-hatch, zigzag and dots fills, plus Excalidraw's element → options mapping (adjustRoughness, dash patterns, fill weight, hachure gap). The test compares ten generator calls against real rough.js output (tests/fixtures/rough/, produced by running the MIT library under Node) to 1e-6, so the same seed draws the same wobble. Per-node toggle (⌘⌥H or the inspector), sloppiness 0/1/2, fill style, re-roll seed; with nothing selected the toggle sets the default for new shapes. - Arrows (
connector.rs): aConnectornode with start/end bindings (node, normalized fixed point, inside/orbit). Orbit ends stop at the target outline +5 + stroke/2, aimed at the other end's focus, and re-route in the fixup pass whenever a bound layer (or an ancestor) moves, resizes or is deleted. Straight, curved (Catmull-Rom) and elbow routes; elbow uses a grid + A* with Excalidraw's bend penalty and segment-count heuristic. Heads: arrow, triangle, dot, bar, diamond. Endpoint handles rebind; the midpoint handle bends. Labels on shapes defer to the shape when binding. - Quick diagram shapes: diamond tool (D), polygon/star, and quick-connect "+" knobs on a selected shape that duplicate it 80 px away and connect the two with a bound arrow.
- Sticky notes (S): colored note with padding 16, text wraps and the font shrinks (to 12) to fit, six colors.
- Laser pointer (K): a trail that fades over 1 s and shows the last 50 points, eased like Excalidraw; the event loop animates only while points are visible, then idles.
- Library: a shared
library.jsonin the projects folder (starter flowchart items on first use), a Library tab with tessellated previews, add-selection, rename, remove, insert (fresh ids, arrow bindings and instance links remapped). - Number keys 1–0 select tools like Excalidraw (1 select … 0 eraser).
- Deferred depth: Excalidraw's dynamic-AABB "side hack" in elbow routing, arrow labels and text bound inside shapes, multi-point arrows by clicking, freedraw with perfect-freehand (paper-rs keeps its own ink), rough rendering of text/ink, collaboration.
P3 notes
- Renderer rework. Nodes become backend-neutral draw ops (fills with solid / linear /
radial / image paints, strokes, glyph runs) in
render/draw.rs. The GPU path tessellates them in world space and caches per node (keyed by content, world rect, inherited opacity and zoom octave). Unchanged frames reuse the item list; the world vertex buffer is only re-uploaded when the item list changes, so panning costs one uniform write. Gradients use a ramp atlas; images are mipmapped textures; multiply/screen/darken/lighten have their own blend pipelines (premultiplied output). The SVG exporter consumes the same ops. - Model v3: fill layers, blend, stroke align/dash/caps/joins, per-corner radii, rich text
spans, auto layout, layout-child sizing and constraints, component flag and instance links,
groups, vector paths, polygons, stars. Page format v3 with a v2→v3 migration (
fill→fills). - Fixup pass (
Editor::fixup) runs inside every transaction: normalizes group bounds, syncs instances of edited components (respecting per-property overrides recorded by the inspector, text edits and drags), and re-runs auto layout from the outermost auto-layout ancestor. It undoes together with the edit. - Resize restores the drag-start snapshot each move and re-applies constraints/group scaling from it, so repeated resizes never drift.
- 10k elements:
tests/perf.rsbuilds 10 000 nodes (rects, ellipses, stars, text), all in view. Release: first frame ~140 ms (CPU tessellation of everything), cached frame ~3 ms, editing one node re-tessellates one node, panning re-uploads nothing. - Deferred depth: rounded-corner frame clipping, live (non-destructive) booleans, on-canvas gradient handles, rotation, variants, layout grids, min/max sizes, baseline alignment, and exact GPU compositing for the remaining 11 blend modes and group opacity.
P2 notes
- Each page is its own infinite canvas with its own undo history, selection and camera. The
active page lives in the editor; the others are parked in their slot (
PageStash). - Pages panel at the top of the left sidebar: click to switch, + to add, double-click to rename, right-click for Rename / Duplicate / Delete, drag to reorder. The last page can't be deleted.
- Paste on another page (or in another project) keeps the original position; paste on the same page offsets by 16 px.
- Deleting a page isn't undoable (the page file is removed at the next save).
P1 notes
- Projects live in
~/Documents/paper-rs/<name>/(PAPER_RS_HOMEoverrides):project.json(manifest: id, name, created/modified, ordered pages, active page),pages/<id>.json, andthumbnail.png. Deleting moves the folder to.trash/with atrashinfo.json, restorable from the Trash view. - Every write is temp file + fsync + rename + directory fsync (
io::atomic). Stale temp files from a crash are removed when the project opens. - Formats carry
format+version; loading runs a chain ofValue -> Valuemigrations. paper-rs 0.1.paper-rsfiles are page format v1 and migrate to v2 (tested). - Autosave: changes are written 1 s after the first unsaved edit (bounded latency even while editing, never during a drag), on page switch, when leaving the project and on quit. The event loop sleeps until the autosave deadline, so idle CPU stays at zero. Thumbnails refresh at most every 20 s and when leaving the project.
- Known gap: typing inside an open text edit is saved when the edit ends (it is one undo step).
Phase 3 verification log (2026-10-11)
cargo build --release: no warnings.cargo clippy --all-targets: clean.cargo fmt.cargo test: 99 unit tests + 42 integration tests (projects, figma, excalidraw, workflow, io, perf, headless), all passing; GPU tests on Mesa lavapipe.cargo check --target aarch64-apple-darwinand--target x86_64-pc-windows-gnupass (the macOS menu is generated from the shared menu table and type-checks).- Real window under Xvfb: idle CPU measured from
/proc/<pid>/statover 5 s after startup — 0 ticks on the home screen and 0 ticks in the editor. Autosave, tooltips and the laser trail wake the loop only while they need to. - 10k elements (
tests/perf.rs, release): cached frame ~3 ms, first frame at a new zoom octave ~140 ms, one edited node re-tessellates one node, panning uploads nothing. - Not yet run on Mac hardware (no Mac available to this session); see README Known gaps.
Phase 2 (paper-rs 0.1) record
| Phase / milestone | Status | Evidence |
|---|---|---|
| Phase 1: reverse engineering | ✅ done | analysis/PAPER_ARCHITECTURE.md. REA scoped run is summarized in the appendix; raw output in analysis/raw/ (gitignored). |
| Phase 2: design | ✅ done | analysis/DESIGN.md |
| M1: window + infinite canvas, smooth pan/zoom | ✅ done | tests/headless.rs::m1_*, editor scenario tests, m1_*.png, x11_window_xvfb.png |
| M2: freehand sketch with smoothing | ✅ done | ink.rs tests, m2_freehand_sketch_with_smoothing, m2_sketch.png |
| M3: shapes, frames, text | ✅ done | scenario tests, m3_shapes_frames_and_text, m3_*.png |
| M4: select/move/resize, multi-select, snapping | ✅ done | scenario tests, m4_select_move_resize_snap, m4_*.png |
| M5: layers panel, undo/redo | ✅ done | history.rs tests, undo_redo_round_trips_a_whole_session, m5_layers_and_undo_redo, m5_*.png |
M6: save/load .paper-rs, PNG/SVG export |
✅ done | io/* tests, m6_save_load_and_export, m6_svg_export_matches_gpu_render, m6_* |
| M7: keyboard shortcuts | ✅ done | editor/shortcuts.rs (conflict test), README table, in-app ? help |
| README (build/run, parity table, gaps) | ✅ done | README.md |
Verification log
cargo build --releasepasses with no warnings.cargo clippy --all-targetsis clean.cargo testruns 51 unit/scenario tests and 8 headless GPU tests, all passing. The GPU tests ran on Mesa lavapipe (llvmpipe, Vulkan) on the headless VPS.- The real windowed app was launched under Xvfb (X11) with the release binary and screenshotted.
It needed
libxkbcommon-x11andlibxcb-xkbplaced in~/.local/lib, since the VPS has no root access. cargo check --release --target aarch64-apple-darwinand--target x86_64-pc-windows-gnuboth pass.- The SVG export was rasterized with resvg and compared with the GPU PNG export: 0.004% of pixels differ.
Notes
- Commit history: the core model, the editor and the app/renderer/I/O layer landed as three feature commits. Each one builds in release mode and has its own tests. Milestones 1–7 are spread across them because the layers depend on each other: the app needs I/O, and the overlay needs the editor.
- REA's full-asar analysis stalled on
node_modules/typescript/lib/_tsc.jsafter 20+ minutes at 2.6 GB RSS. A scoped run on Paper's own sources finished in 65 s.
Next steps (not started)
- Image nodes (textured quads) and drag-and-drop of image files, to match
paper-asset:. - Per-node mesh cache keyed by revision and zoom bucket.
- Rotation, plus a bezier pen tool.
- Multiple pages, and system clipboard interop (SVG/PNG/text).
- Run and tune on macOS hardware, especially trackpad scroll feel and tablet pressure.