Skip to content

Add per-agent whiteboard with Excalidraw thin relay architecture#752

Open
selfcontained wants to merge 9 commits into
mainfrom
agt_9e9d7d8b039e/whiteboard-thin-relay-excalidraw
Open

Add per-agent whiteboard with Excalidraw thin relay architecture#752
selfcontained wants to merge 9 commits into
mainfrom
agt_9e9d7d8b039e/whiteboard-thin-relay-excalidraw

Conversation

@selfcontained

Copy link
Copy Markdown
Owner

Summary

  • Adds a per-agent whiteboard powered by Excalidraw with a thin relay architecture — agents construct raw Excalidraw element JSON directly via MCP tools, the server merges by id and persists to Postgres, and the frontend renders an interactive canvas with live bidirectional sync via SSE.
  • Three MCP tools: whiteboard_update (with embedded ~200-line Excalidraw element format cheat sheet), whiteboard_get, and whiteboard_clear. REST endpoints for user-side scene saves with optimistic locking (version check + 409 on conflict).
  • Frontend: lazy-loaded Excalidraw component in a new "Whiteboard" center pane tab, self-hosted fonts via Vite plugin (excluding CJK for 12MB savings), debounced save (1s) and snapshot export (4s), pointer-down deferral for remote updates, "agent drew" violet dot indicator.

New files

  • apps/server/src/db/migrations/0030_whiteboards.sql — whiteboards table
  • apps/server/src/shared/whiteboard.ts — shared types + simplifyElements()
  • apps/server/src/shared/whiteboard-store.ts — persistence layer
  • apps/server/src/shared/mcp/whiteboard-tools.ts — MCP tool definitions
  • apps/server/src/server/mcp-whiteboard-handlers.ts — thin relay handlers
  • apps/server/src/routes/whiteboard.ts — REST endpoints
  • apps/web/src/hooks/use-whiteboard.ts — React Query hook
  • apps/web/src/components/app/whiteboard-tab.tsx — Excalidraw canvas
  • apps/web/src/components/app/whiteboard-pane.tsx — lazy mount wrapper

Test plan

  • 31 unit tests: simplifyElements, isValidScene, handler merge/upsert/delete/retry/overflow logic
  • 4 E2E tests: tab navigation, REST PUT/GET persistence, optimistic locking 409, MCP tool call round-trip (update → get → clear → verify empty)
  • pnpm run check — type checking passes
  • pnpm run finalize:web — production build succeeds
  • pnpm run test — all 2295 unit tests pass
  • pnpm run test:e2e — all 182 E2E tests pass (12 pre-existing skips)

🤖 Generated with Claude Code

selfcontained and others added 9 commits July 10, 2026 12:20
Agents construct raw Excalidraw element JSON directly via MCP tools
(whiteboard_update, whiteboard_get, whiteboard_clear). The server is a
thin relay — it merges elements by id, persists to Postgres with
optimistic locking, and pushes updates via SSE. The frontend renders
an interactive Excalidraw canvas with live sync in both directions.

Key pieces:
- Migration 0030: whiteboards table (agent_id PK, JSONB scene, version)
- MCP tools with embedded Excalidraw element format cheat sheet (~200 lines)
- REST endpoints for user-side scene save with optimistic locking
- Whiteboard tab in center pane with lazy-loaded Excalidraw component
- Self-hosted Excalidraw fonts via Vite plugin (excluding CJK for 12MB savings)
- SSE-driven React Query invalidation with "agent drew" dot indicator
- 31 unit tests + 4 E2E tests covering REST API, MCP tools, and UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the whiteboard tab is hidden (display:none) and an agent updates
the scene via MCP, Excalidraw's updateScene() updates internal state
but the canvas can't repaint at zero dimensions. Call refresh() when
the tab becomes visible so the canvas repaints with the latest content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…dration

Raw server elements lack Excalidraw internal properties (seed, version,
roughness, opacity, strokeWidth, etc.) needed for rendering. Use
restoreElements() to hydrate sparse server elements before updateScene()
so the canvas renders correctly without requiring a page reload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a useEffect in WhiteboardCanvas that fetches all four .excalidrawlib files
in parallel and loads them into Excalidraw via updateLibrary(). Includes proper
cleanup via cancelled flag to prevent state updates after unmount. Library
loading is best-effort and fails silently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… whiteboard library

- Key the library catalog Map by category:name instead of name alone so
  duplicate item names across different .excalidrawlib files (e.g. "Button"
  in both html-input-elements and universal-ui-kit) no longer silently
  collide. get()/stamp() accept a bare name (first match) or category:name
  for an exact lookup.
- Remap groupIds through a dedicated groupIdMap in stamp() instead of
  reusing the element idMap, so stamping the same item twice no longer
  shares groupIds between the two copies.
- Wrap the LibraryCatalog.fromDirectory() startup call in try/catch so a
  missing/malformed library directory logs a warning and disables library
  support instead of crashing the server; all downstream consumers already
  treat libraryCatalog as optional.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant