Skip to content

About

Shared Excalidraw workspace for humans and coding agents

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

xcld-collab

A shared whiteboard for you and your coding agents. Agents draw (usually as Mermaid), you mark up the drawing in a real canvas (Excalidraw), and the agent reads your changes back as a precise list: "relabeled X", "rewired A → B", "added note near Y". No more describing diagram feedback in prose.

Everything runs locally in one Docker container. Boards are plain files in ./boards, so any agent that can read and write files can join in.

Status: prototype. Flowcharts only. See Coming soon.

Getting started

You need

  • Docker: Docker Desktop (Windows/macOS) or Docker Engine with the Compose plugin (Linux). Check it with docker compose version.
  • git.
  • Windows: PowerShell (5.1 or 7). macOS/Linux: bash and jq.

1. Build

git clone https://github.com/Hack4Impact/xcld-collab
cd xcld-collab
.\build.ps1          # macOS/Linux: ./build.sh

The first build compiles Excalidraw from source, so go get a coffee. Rebuilds reuse the cache. The build ends with .env updated (XCLD_TAG=...); that's how Compose knows which image to run.

Builds install the app from the committed app/package-lock.json (npm ci), which is the reproducible default. If your environment can only use a private npm registry and the locked build fails there, .\build.ps1 -NoLockfile (./build.sh --no-lockfile, or XCLD_NO_LOCKFILE=1) resolves package.json ranges instead; its tag ends in -nolock.

Or: use the prebuilt image

Available after the public release. Until then the image is private and docker compose pull fails with denied; build from source as above.

Every push to main that changes the image publishes a multi-arch image (linux/amd64 and linux/arm64) to ghcr.io/hack4impact/xcld-collab, so you can skip the build. You still need the clone (for compose.yaml and boards/). Create .env next to compose.yaml:

XCLD_IMAGE=ghcr.io/hack4impact/xcld-collab
XCLD_TAG=latest
COMPOSE_PROFILES=widget
# Linux only, so boards stay writable: your `id -u` / `id -g`
# XCLD_UID=1000
# XCLD_GID=1000
# Linux only: run `mkdir -p ~/.excalidraw/history ~/.excalidraw/exports` first (Docker would
# create them owned by root), and keep version history there instead of in the xcld-state volume:
# XCLD_HISTORY=cache
docker compose pull

Then continue with step 2. latest follows main; set XCLD_TAG to a full <ours7>-<exc7>-<m2e7>-<mcp7> tag to stay on one build. Running build.ps1 / build.sh later points .env back at your local build.

2. Start

docker compose up -d --wait

--wait returns once the workspace is healthy. This starts the canvas only, with no outside requests. The chat widget is experimental and opt-in.

3. Draw something

Open the board browser at http://127.0.0.1:3100/. examples/demo is a ready-made drawing you can look around in.

To see the full loop, make your own copy of its Mermaid source and open that. Examples are checked into git, so work on copies:

New-Item -ItemType Directory -Force boards\sandbox | Out-Null
Copy-Item boards\examples\demo.mmd boards\sandbox\demo.mmd
# macOS/Linux: mkdir -p boards/sandbox && cp boards/examples/demo.mmd boards/sandbox/

Open http://127.0.0.1:3100/?board=sandbox/demo. Your browser turns the Mermaid into an editable drawing and saves it as boards/sandbox/demo.excalidraw. Move things, relabel, recolor, add notes. Every change saves automatically.

4. Ask what changed

docker exec xcld-collab xcld snapshot sandbox/demo     # remember this version (pinned in history)
# ...edit the drawing in the browser...
docker exec xcld-collab xcld diff sandbox/demo         # what changed since the snapshot
docker exec xcld-collab xcld diff sandbox/demo --since 10m   # ...or since any point, with what was overwritten
docker exec xcld-collab xcld to-mermaid sandbox/demo   # the drawing as Mermaid again
docker exec xcld-collab xcld open-in-canvas <checkpointId> sandbox/from-chat
docker exec -i xcld-collab xcld mcp                    # optional: MCP tools over stdio

Opening this repo in VS Code offers the xcld tools server from .vscode/mcp.json. For Copilot CLI, Claude Code, Codex and OpenCode, see the client configs in the reference.

That's the whole default loop. Your agent runs those same commands; xcld mcp is the stdio tools server inside the canvas container and does not need the chat widget.

Chat widget (experimental)

The image also contains the upstream Excalidraw MCP Apps chat widget, which draws diagrams inside chat in hosts that render MCP Apps, such as VS Code. It is experimental and off by default:

  • It sends requests outside your machine. When it renders, the widget loads React, React DOM, Excalidraw 0.18.0 and morphdom, plus Excalidraw's CSS and some fonts, from https://esm.sh. Attempts to serve that JavaScript locally rendered a blank diagram in VS Code (#3).
  • Known font errors. The VS Code webview console can show font Content-Security-Policy errors; affected UI text falls back to a system font (#2).

To opt in:

  1. Add COMPOSE_PROFILES=widget to .env and run docker compose up -d --wait. This starts a second service, mcp, at http://127.0.0.1:3001/mcp.
  2. Add the widget server to .vscode/mcp.json (or your user mcp.json) next to xcld: "excalidraw": { "type": "http", "url": "http://127.0.0.1:3001/mcp" }.

If a host cannot open the widget editor, the widget shows its checkpoint id; use open-in-canvas (or MCP open_in_canvas) with an explicit board path to move the drawing into the persistent canvas.

To opt out again, remove widget from COMPOSE_PROFILES in .env, then run docker compose --profile widget down followed by docker compose up -d --wait.

Stop

docker compose down

Boards stay in ./boards, and version history stays where it lives (see below); docker compose down -v also deletes the xcld-state volume, which holds the history on Windows and macOS. Only boards/examples/ is tracked in git; everything else you create there is gitignored. Nothing leaves your machine: the canvas is served from 127.0.0.1 only, and fonts and assets come from the container. The one exception is the experimental chat widget, which is off unless you opt in.

Environment Settings

Setting Default What it does
XCLD_PORT 3100 Port on 127.0.0.1
XCLD_MCP_PORT 3001 Host port for the experimental chat widget service at /mcp (only with COMPOSE_PROFILES=widget)
XCLD_CONTAINER xcld-collab Canvas container name. Override when running multiple Compose projects side by side
XCLD_MCP_CONTAINER xcld-mcp Chat widget service container name
XCLD_BOARDS ./boards Boards folder. Relative paths are relative to compose.yaml, so use an absolute path for a folder in another repo
XCLD_WATCH_POLL_MS 1000 How often the server checks for file changes
XCLD_MAX_DEPTH 0 Nested board folder limit; 0 means unlimited
XCLD_AUTO_EXPORT snapshot When Mermaid is written for you: off, snapshot (each xcld snapshot also writes a .mmd) or save (also keeps boards/.exports/<path>.mmd current). See the decision tree
XCLD_DESIGN_RULES <boards>/design-rules.csv Optional path to the default design rules CSV (inside Docker, use /boards/...). A folder's own design-rules.csv still replaces the inherited defaults for boards below it
XCLD_PUBLIC_URL http://127.0.0.1:${XCLD_PORT} URL returned by MCP board_url; Compose sets this for the canvas service
XCLD_AUTHOR_NAME your git config user.name, else your OS user The canvas's default author name in version history, shown as "Author: …" at the bottom of the canvas (click it to rename in that browser). The build writes it into .env once and never overwrites it; edit it there
XCLD_TIMING unset 1 records per-stage commit timings at GET /api/timings (for profiling, see tests/versions-load.mjs)
XCLD_SLOW_IO_MS 1000 A file operation of the commit pipeline that takes this many ms or more is logged once and listed under slowIo in GET /api/status (always on). See Troubleshooting
XCLD_CACHE_DIR ~/.excalidraw A folder on your machine. xcld history export writes to exports/ in it; on Linux it also holds version history (history/). Must be a path; on Linux, writable by you. See where history lives
XCLD_HISTORY Linux: cache; Windows/macOS: volume Advanced. Where version history lives: volume (the Docker volume xcld-state) or cache (XCLD_CACHE_DIR/history/). The build writes the default for your OS once and never overwrites it. cache on Windows/macOS is slow
COMPOSE_PROFILES unset (canvas only, no outside requests) widget also starts the experimental chat widget, which loads JavaScript from esm.sh. The build never writes or changes this line; builds before the widget became opt-in added COMPOSE_PROFILES=widget, and the build prints a notice while it is there

Put these in .env next to compose.yaml, then run docker compose up -d --wait again.

Where next

  • User guide: the recommended way to draw, give feedback and work with agents.
  • Reference: every xcld command, the Mermaid mapping, and troubleshooting.
  • Agent skill: the safe MCP workflow for creating, reviewing and revising boards.
  • Design: how it works and why, for contributors.

Working in parallel

Parallel editing is supported. You can draw in the canvas while several agents write to the same board; nothing is silently lost. The server merges writes instead of letting the last one win. The tab's saves, agents' write_board and write_mermaid (MCP), xcld write and xcld write-mermaid (CLI), and direct edits of a board or .mmd file all go through one commit pipeline:

  • Every write names the version it started from (base). The server merges it with anything committed since. Edits to different shapes all apply.
  • Overwritten means: the same shape (or its label) was changed on both sides since the writer's base. The later edit (by when it was made, not when it arrived) takes the whole shape and its label; the other edit loses. A queued write made earlier loses to a newer edit even when it lands later.
  • Losers live in version history only: the losing shape is kept in the history entry of the write that overwrote it (or lost), nothing re-applies it, and it is reported three ways: the canvas banner ("overwritten by …"), the write's answer, and xcld diff <board> --since … (below). xcld history export copies it out with everything else.
  • Every change is kept: version history keeps one entry per author turn (a person's autosaves fold into one restore point until someone else writes, 3 minutes pass or a checkpoint). Each entry stores only what changed, with a full copy every 20 entries: 100 small turns on a 1,500-element board take about 1 MB. See where history lives.
  • queued answers: a write is safe once the server has it in its journal. If the merge takes longer than 5 s, typically because the disk is slow (Docker Desktop shares one disk among all containers; cloud VMs throttle), an agent gets queued with a branch id instead of merged, and the write lands as soon as the disk catches up. Nothing is dropped; read the board again before building on it. GET /api/status lists slow file operations (see Troubleshooting).
  • Mermaid merges too, with no tab open. The server applies an agent's Mermaid to the board: your layout, notes and colors stay, existing shapes keep their place, new nodes go next to a connected one, and only shapes that came from that Mermaid are ever removed (a copy you made of one never is). Several agents can write Mermaid to the same board at once, each diagram under its own source name if they like. A new diagram is laid out by an open tab and added next to your drawing (below it, or to the right for left-right diagrams), never over it; with no tab, the server lays it out in a simple grid after about 2 minutes. A shape you edited keeps your version until the agent's Mermaid changes that shape.

The canvas takes part like any other writer:

  • Your name is at the bottom of the canvas ("Author: …"), from XCLD_AUTHOR_NAME. Click it to rename; the browser remembers it for all its tabs. Each tab also saves under a hidden tab id, so two tabs of yours never overwrite each other's turn.
  • Save before reload: when someone else's write lands, the tab first saves your pending edits (with the version they started from), then shows the merged board. A drag or a label you are typing carries on.
  • A banner lists what merged from whom and every overwritten edit, with who won. A losing edit is kept in version history only; nothing re-applies it.
  • Ctrl+S (Cmd+S) closes your current turn as a restore point ("Saved checkpoint"). It no longer downloads a file.

See what happened:

docker exec xcld-collab xcld diff myproject/demo --since 30m           # changes and losers in the last 30 minutes
docker exec xcld-collab xcld diff myproject/demo --since author:Ada    # since Ada's last turn, incl. what she lost
docker exec xcld-collab xcld snapshot myproject/demo --name review-1   # pin this version under a name...
docker exec xcld-collab xcld diff myproject/demo --since review-1      # ...and diff against it later
docker exec xcld-collab xcld watch myproject/demo                      # every merge and history entry, live

--since takes a version id prefix, a time (10m, 2h, an ISO time), author:<name> or a snapshot name; add --json for agents. Agents use MCP diff with since the same way. See the reference.

Caveats:

  • The same shape (or its label) edited on both sides goes whole to the later edit; there is no field-by-field merge. Look at the banner's details for what lost.
  • An edit you make in the fraction of a second while a save is on its way, to a shape that the merge changed, is replaced by the merged shape. The banner says so; that edit is not in history.
  • Arrow points can go stale when the bound shape moves on the other side (issue #6).
  • Names are not checked for uniqueness. A shared server with several people (random names, a server-checked unique name) is future work.

Where history lives

One folder on your machine, XCLD_CACHE_DIR (default ~/.excalidraw), holds what you can open: exports, and on Linux the history itself.

Host Version history xcld history export writes to
Linux ~/.excalidraw/history/ ~/.excalidraw/exports/<board>/
Windows, macOS (Docker Desktop) the Docker volume xcld-state ~/.excalidraw/exports/<board>/
  • Windows and macOS: history stays in a Docker volume because Docker Desktop folder mounts are too slow for the write path. docker compose down -v deletes the volume and all version history. docker compose down keeps it.

  • Linux: the build creates the folders as you and sets XCLD_UID/XCLD_GID, so the container can write them.

  • Move the folder with XCLD_CACHE_DIR=<path> in .env. Advanced: XCLD_HISTORY picks where history lives, volume or cache (the folder). The build writes the default for your OS once and never overwrites a value you set.

  • Copy a board's history out of either place to ~/.excalidraw/exports/<board>/:

    docker exec xcld-collab xcld history export myproject/demo          # as stored: checkpoints + deltas
    docker exec xcld-collab xcld history export myproject/demo --full   # every version as an .excalidraw file

    See the reference.

  • An older .env with XCLD_STATE_DIR or XCLD_EXPORT_DIR (from development builds) still works: the build prints a notice and adds the matching XCLD_HISTORY / XCLD_CACHE_DIR. It doesn't delete your lines.

Upgrading from an earlier build

Rebuild (or pull) the image and start it as usual; your boards and their .mmd files stay where they are.

  • Reload every open canvas tab after the upgrade. A tab keeps running the page it loaded, and the server refuses saves from a page of an earlier build: such a tab shows "Save failed: HTTP 409" and saves nothing until you reload it (your board is unchanged). Before this guard, an old tab could replace the board with a stale copy.
  • Boards made from Mermaid before versions (when a tab converted boards/<path>.mmd and replaced the board) are taken over as they are. The first time the new build looks at the .mmd (a tab opens the board, or the file watcher), it treats that file as already converted: the shapes it made become the board's main Mermaid source, so a later write_mermaid updates them in place instead of adding the diagram a second time. Nothing on the board moves or changes; shapes you edited since keep your edits, and shapes you drew yourself are never part of it. A .mmd that no tab ever converted is not applied either: write it again (write_mermaid, or save the file) to apply it.
  • Scripts that PUT a board without the tab's headers keep working as before.

The details are in DESIGN.

Coming soon

These are designed but not built yet. Don't rely on them.

  • Version browsing in the canvas. History is recorded; xcld diff --since, pinned snapshots and xcld history export read it (see above), but the canvas can't open an old version yet. Ctrl+S closes a restore point; Excalidraw's menu "Save to…" still downloads a separate copy.
  • History pruning (keep every version 48 hours, then pinned snapshots only; #4). Today nothing is pruned.
  • More diagram types: sequence, class, ER, state.

Development

.\build.ps1 -Target vendor    # upstream packages built from source, into app\vendor
cd app; npm ci; npm run dev   # npm ci installs exactly app/package-lock.json
node --test tests             # from the repo root

app/package-lock.json is committed with public npm URLs only. If you change dependencies (or pins.json, which changes the vendor tarballs), rebuild app\vendor, then run npm install and npm run lockfile:public in app/ and commit the lockfile. npm writes your registry's URLs into the lockfile, so after any npm install <pkg> through a private registry, run npm run lockfile:public before committing. npm run lockfile:public -- --check exits non-zero if any URL isn't public npm, and tests/lockfile.test.mjs runs the same check. npm ci rejects stale vendor tarballs. Installing through a mirror is fine: npm fetches the same tarballs from your configured registry.

Optional: scripts/install-hooks.sh (Windows: scripts\install-hooks.ps1) adds a gitleaks pre-commit hook with the same .gitleaks.toml rules CI enforces on main; it also rejects a staged app/package-lock.json that names a non-public registry. CI runs only after merge to main (see .github/workflows/).

License

MIT, © 2026 Hack4Impact. Bundles Excalidraw, mermaid-to-excalidraw and (later) excalidraw-mcp, all MIT. See LICENSE and NOTICE.

About

Shared Excalidraw workspace for humans and coding agents

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages