Skip to main content
Glama
README.md
# pulp

**The local-first, agent-driven design canvas.** pulp is an infinite canvas
where drawings are real HTML and CSS, humans and coding agents edit the same
document, and the browser remains the source of truth. Ask Claude or Codex to
design a page and watch it appear live, refine it by hand, save the editable
drawing as a file, or export it as PNG, SVG, HTML, a portable deck, or PDF.

- **Real DOM, not pictures.** Every frame is real flexbox layout rendered by
  the browser. Canvas rendering and export share the same code generator, so
  HTML export is lossless.
- **Local first.** Named workspaces autosave independently in browser storage.
  Editable `.pulp.json` files provide explicit backups and handoff; no cloud
  account or database is required.
- **Agents first.** Eighteen MCP tools let Claude, Codex, and other clients read
  and mutate the document. Multiple agents can share one live workspace or use
  isolated named sessions without fighting over the bridge port.
- **Review is a loop, not a screenshot.** Pin human feedback to a layer or
  canvas position; agents read the workspace inbox over MCP, make the change,
  and resolve it with a visible response. The MCP console keeps a searchable,
  workspace-persistent audit of every received request and response.
- **Rust core, WASM delivery.** The scene graph, command API, undo/redo,
  validation, and HTML codegen are a Rust crate compiled to WebAssembly. The
  app itself is static and deploys to GitHub Pages.

## Browser demo

[![Watch the browser-only Pulp demo](artifacts/pulp-browser-demo-poster.png)](artifacts/pulp-browser-demo.mp4)

The 45-second browser-only walkthrough shows the live canvas, presentation
mode, the MCP request/response history, and the multi-format export menu.

## Quick start

Prerequisites: Node 20+, Rust via rustup with the WASM target, and wasm-pack:

```sh
rustup target add wasm32-unknown-unknown
cargo install wasm-pack --locked
```

Then:

```sh
npm install
npm run build:engine   # rerun after Rust changes
npm run build:mcp      # rerun after MCP changes
npm run dev            # http://localhost:5173
```

Open `http://localhost:5173` for the default workspace, or name one in the
URL:

```text
http://localhost:5173/?workspace=design-notes
```

The workspace chip can switch among drawings already saved by that browser.
Names are normalized to lowercase letters, numbers, `_`, and `-`; use the name
shown in the chip when configuring an agent.

## Connect agents

The repo's `.mcp.json` automatically registers Pulp for Claude Code launched
inside this directory. For a global registration:

```sh
claude mcp add pulp -- node /absolute/path/to/pulp/mcp/dist/index.js
codex mcp add pulp -- node /absolute/path/to/pulp/mcp/dist/index.js
```

With the default editor open, both clients join the `default` session and can
work on the same canvas. The first local MCP process becomes the lightweight
broker; later processes join it automatically, so `EADDRINUSE` is not a user
workflow or an error to work around.

For a named browser workspace, set the MCP session to the same normalized name:

```sh
claude mcp add pulp-design-notes -- \
  env PULP_SESSION=design-notes PULP_CLIENT_NAME=claude \
  node /absolute/path/to/pulp/mcp/dist/index.js

codex mcp add pulp-design-notes -- \
  env PULP_SESSION=design-notes PULP_CLIENT_NAME=codex \
  node /absolute/path/to/pulp/mcp/dist/index.js
```

`PULP_SESSION` controls routing; `PULP_CLIENT_NAME` is the label shown in logs
and the editor's live agent-activity cards. Agents using the same session share
the corresponding editor. Give agents different sessions, with one browser tab
per named workspace, to isolate their documents. All normal sessions share
port 7857 through the broker.

Advanced local experiments can pair `PULP_PORT=7867` with `?port=7867`, or use
an explicit browser bridge such as `?bridge=ws://127.0.0.1:7867`. A hosted HTTPS
page requires an explicitly supplied `wss://` bridge; this repository does not
deploy one.

Once the toolbar says `agent live`, ask an agent something like:

> Design a SaaS landing page in pulp: dark hero, three feature cards, footer.

The agent reads a compact document outline, applies JSON commands, and every
change renders immediately. Human edits use the same engine and undo history,
so the next agent read sees them. The canvas shows the four most recent agent
operations with pending, success, or error state; click a completed targeted
operation to reveal its layer. Open **MCP** for the full searchable request and
response history, including source agent, session, target, timing, filters, and
copyable JSON.

Press `C` or open **Review** to attach feedback to the selected layer or place
a pin on the canvas. Open items are stored per workspace. Agents consume them
with `pulp_list_feedback` and close the loop with `pulp_resolve_feedback`; their
name and response appear on the feedback card. Because MCP stdio is
client-initiated, a pin is a durable inbox item for the agent's next read—it
does not wake an idle Claude/Codex process by itself.

Agent onboarding is in [AGENTS.md](AGENTS.md). Claude and Codex also receive
the matching `pulp-design` skill from `.claude/skills/` and `.agents/skills/`.

## Local-first files and workspaces

Each `?workspace=<name>` has its own autosave key,
`pulp.doc.<normalized-name>`, on the current browser origin. Autosave is
debounced after document mutations and its state appears in the toolbar.

- **Save** downloads the complete editable document as `<title>.pulp.json`.
- **Open** validates a `.pulp.json` file, replaces the current drawing, and
  autosaves it into the current workspace.
- **PNG / SVG / HTML** export the selected frame as a 2x raster image, scalable
  wrapper, or lossless standalone DOM page.
- **Presentation HTML** turns every root frame into a portable keyboard-driven
  deck. **Print / PDF** opens the same frames one per print page.

Browser storage is convenient working state, not a durable backup. Save a
`.pulp.json` file before clearing site data, changing browsers, or handing a
drawing to another machine. Invalid or structurally broken document files are
rejected without replacing the current canvas.

## How it fits together

```text
browser workspace (?workspace=design-notes)
  editor UI -> Rust/WASM document engine -> local autosave / .pulp.json / HTML
       ^
       | session "design-notes" over ws://127.0.0.1:7857
       |
shared MCP broker
  ^                 ^
  | stdio           | stdio
Claude Code       Codex (and other MCP clients)
```

Every mutation goes through the same engine command API. The document is plain
JSON; a patch looks like:

```json
{ "cmd": "set_props", "id": "n4",
  "props": { "style": { "fill": "#0b0b10", "shadow": null } } }
```

Patches deep-merge: only fields present in the command change, and `null`
clears a field.

## Editor controls

| Action | Input |
|---|---|
| Pan | Space+drag, middle-drag, or scroll |
| Zoom | Pinch or ⌘/Ctrl+scroll |
| Actual size | `0` |
| Fit selection / fit all | `1` / `2` |
| Create frame | `F` |
| Create rectangle / ellipse | `R` / `O` |
| Create sticky note / text | `N` / `T` |
| Select | Click; Shift+click for multi-select |
| Move / resize | Drag a frame or absolute child; drag selection handles |
| Undo / redo | ⌘/Ctrl+Z / Shift+⌘/Ctrl+Z |
| Duplicate / delete | ⌘/Ctrl+D / Delete |
| Clear selection | Escape |
| Toggle Layers / Inspector | `[` / `]` |
| Focus mode | `\` |
| Pin review feedback | `C`; submit with ⌘/Ctrl+Enter |
| Present frames as slides | `P`; then arrows/space/Page Up/Page Down; `Esc` exits |
| Rename | Double-click in the Layers panel |
| Reveal on canvas | Alt+double-click in the Layers panel |

On initial load, Pulp fits all frames into view. Creation shortcuts place new
objects in the active frame, creating a frame first when necessary.

## Verification

```sh
npm run test:engine       # native Rust engine tests
npm run test:mcp-multi    # shared broker, isolation, failover, timeout tests
npm run test:mcp-tools    # MCP tool registration, including feedback tools
npm run test:feedback     # workspace feedback persistence and isolation
npm test                  # both suites
npm run build             # WASM engine + app + MCP builds
npm run check             # tests, then the full production build
```

With a local editor open on the matching session, the destructive end-to-end
smoke test exercises the MCP tools and creates a sample frame:

```sh
node mcp/smoke.mjs
```

## Deploy

Pushing to `main` runs the test suites, builds the engine and app, and deploys
`app/dist` to GitHub Pages. The hosted editor, named workspaces, local autosave,
file Open/Save, all export formats, and presentation mode work without a backend. Agent control defaults
to local development because a hosted HTTPS page cannot open an insecure local
WebSocket; an explicit `wss://` bridge is an opt-in advanced configuration. On
HTTPS, the app also registers its manifest and offline shell; after the assets
have been cached by an online visit, it can reopen without the network.

## Layout

```text
engine/   Rust/WASM document model, commands, validation, history, codegen
app/      Vite/TypeScript editor, canvas UI, workspaces, local files, bridge client
mcp/      MCP tools, shared multi-session WebSocket broker, smoke tests
```