Projections and whiteboards

Domain lenses on the session icon rail, plus Excalidraw-backed .whiteboard files in the editor.

Projections and whiteboards

Studio separates activity modes (what you are doing) from projections (which slice of the project you want to see). Core rail icons answer activity; projections answer focus.

Icon rail layout

CORE (top → bottom)                  PROJECTIONS (pinned, per workspace)
Graph · Plan · Code · Database ·     Notes · Whiteboards · … · [ + ]
Assets · Design · Review · Preview ·
Logs
         ↑ divider                              ↑ pin picker

Core activity views (top to bottom): Graph, Plan, Code, Database (?view=cms), Assets, Design, Review, Preview, Logs. Preview uses ?view=browser in the URL; ?view=preview is an alias.

  • Core routes use ?view=graph, ?view=code, and similar.
  • Projections use ?view=projection&lens=<id> (for example lens=notes or lens=whiteboards).
  • + opens a pin picker grouped as On rail and Add to rail (up to five pins; at least one required). Hover a pinned icon to reveal × in the corner and unpin without opening the menu (keyboard focus on the icon shows × as well).

Projections filter and present data. They do not replace full CMS (schema and collections), Assets (media library), or Plan (issues). Use Plan for the full board, milestones, and suggestions; the optional Calendar projection reuses the same monthly grid in the side panel.

Built-in projections

LensPurpose
NotesQuick-capture cards backed by the Trellis store (type:note). Always pinned by default.
WhiteboardsExcalidraw diagrams stored as .whiteboard files in the repo.
CalendarMonthly grid: graph issues/work units/milestones plus manual events you create in Studio. Optional; pin from + or projections.pinned.
Content, Records, Posts, …CMS-filtered tables and cards by collection (workspace-type defaults).

Default pins depend on session template / workspace type (for example app and productivity include Notes and Whiteboards). Calendar is not pinned by default. Up to five projection icons are shown; order is left to right.

Override pins in opencode.jsonc:

{
  "projections": {
    "workspaceType": "app",
    "pinned": ["notes", "whiteboards", "records", "content"],
  },
}

To pin Calendar without changing other defaults:

{
  "projections": {
    "pinned": ["notes", "calendar", "whiteboards"],
  },
}

User pin order is also stored per project in localStorage (session.projections.pinned.<projectId>).

Calendar projection

Select Calendar in the projection zone (or open ?view=projection&lens=calendar). The same view is available under Plan → Calendar.

Graph-backed markers (read-only on the grid):

  • Priority-colored dots for issues (round) and work units (square).
  • Milestones as interactive-colored squares on their created date.

Manual events (calendar_event entities in the Trellis store):

  • Click a day to open the day panel; double-click a day or use Add to create an event.
  • Events show as title chips on the grid (issues/milestones stay as dots below).
  • Edit title, description, all-day vs timed, and color (default, critical, high, medium, low).
  • Persisted via POST /trellis/calendar/save and POST /trellis/calendar/delete; listed with GET /trellis/calendar/events (optional year and month query params, month 1–12).

Agents — calendar tool

OpenCode registers a calendar tool for CRUD on the same calendar_event entities:

ActionPurpose
listOptional year + month (1–12) to filter one calendar month
createtitle, startAt (ISO datetime or YYYY-MM-DD with allDay: true), optional endAt, description, color
updatePatch fields by event id
deleteRemove by id

Example: create an all-day event on 2026-06-03 with startAt: "2026-06-03", allDay: true, color: "high".

Manual events are linked to the project entity with knows (same pattern as notes). They are not stored as files on disk.

Whiteboards (.whiteboard files)

Whiteboards are first-class project files, not store entities. Each file holds Excalidraw JSON (same shape as a native .excalidraw export):

{
  "type": "excalidraw",
  "version": 2,
  "elements": [],
  "appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
  "files": {}
}

Open in Code view

Open any *.whiteboard path in Code view. Studio renders the Excalidraw editor instead of Monaco. Changes auto-save to disk (debounced) and integrate with the tab Save action.

What gets saved

Persisted JSON includes elements, durable appState fields (for example background color and grid), and embedded files. Studio strips runtime-only appState keys before save — notably collaborators, which Excalidraw keeps as an in-memory Map, not JSON.

When opening a board, Studio passes collaborators: new Map() to Excalidraw, scopes browser storage with a per-file name (the workspace path), and repairs corrupted localStorage entries (including the default excalidraw key) by removing plain-object collaborators values left over from earlier sessions.

SolidJS and React

Studio is SolidJS; Excalidraw is a React component. They do not share one component tree. ExcalidrawHost mounts a dedicated DOM node and uses React 18 createRoot to render Excalidraw inside it. The implementation lives in packages/app/src/pages/session/excalidraw-react-bridge.tsx (lazy-loaded chunk) and excalidraw-host.tsx.

If the canvas looks broken (toolbar only, black canvas)

Symptom: shape icons stack vertically, the drawing surface is empty or black, or layout looks unstyled. Excalidraw’s JavaScript loaded but @excalidraw/excalidraw/index.css did not (common after a partial install or before restarting dev).

  1. From the turtlecode repo root: bun install (installs @excalidraw/excalidraw, react, and react-dom).
  2. Restart the Studio dev server and hard-refresh the browser.
  3. If it persists, clear Excalidraw localStorage keys (see below) and reopen the file.

If the canvas crashes on load

Symptom: console error collaborators.forEach is not a function.

  1. Restart the Studio dev server after pulling the latest build.
  2. Clear bad browser storage for your origin (DevTools → Application → Local Storage), or run in the console:
Object.keys(localStorage)
  .filter((k) => k.startsWith("excalidraw"))
  .forEach((k) => localStorage.removeItem(k));
  1. Reopen the .whiteboard file. The next save writes a clean JSON copy without collaborators.

Dependencies

The editor uses @excalidraw/excalidraw@0.18.0 with React 18.3 (react / react-dom in packages/app). Vite prebundles those packages and injects Excalidraw CSS through the React bridge module.

Whiteboard file helpers live in @opencode-ai/whiteboard (packages/whiteboard/). The Studio UI imports the browser entry (@opencode-ai/whiteboard/browser) so Vite does not bundle the Node-only corpus loader (node:fs). OpenCode agents use the full package entry, which includes corpus JSON and the whiteboard tool.

After pulling Studio:

cd studio   # or your turtlecode/ide clone
bun install

Restart the dev server so Vite picks up the whiteboard chunk and workspace links. Whiteboards require installed node_modules; there is no CDN fallback.

If the Whiteboards lens fails to load (500 on schema.ts)

Symptom: browser console shows GET …/lib/whiteboard/schema.ts 500 or Failed to fetch dynamically imported module for session.tsx.

Cause: Studio accidentally imported the main @opencode-ai/whiteboard entry in the browser (pulls node:fs corpus code). Current builds use @opencode-ai/whiteboard/browser with a Vite alias in packages/app/vite.js.

Fix: pull latest Studio, run bun install from the repo root, restart the dev server, and hard-refresh.

Whiteboards projection

Select Whiteboards in the projection zone (or open ?view=projection&lens=whiteboards):

  • Card grid of .whiteboard files in the workspace (discovered via /find/file).
  • New whiteboard creates whiteboards/<slug>.whiteboard (adds a numeric suffix if the name exists) and opens it immediately.
  • Click a card to open the board fullscreen in the projection panel (Excalidraw fills the side panel). Use the Back control in the toolbar to return to the card grid. This stays on the Whiteboards lens; it does not switch to Code view.

To edit the same file from the file tree or a Code tab, open *.whiteboard in Code view as described above. Both paths share the same on-disk JSON and debounced save behavior.

Agents and the whiteboard tool

OpenCode registers a whiteboard tool for every default agent (same pattern as cms, calendar, and asset). The Trellis system prompt lists corpus actions so models do not claim they lack drawing tools.

Composer context (automatic):

  • Code view: open .whiteboard editor tabs (up to three recent paths) attach on send.
  • Whiteboards projection: the board open in fullscreen in the projection panel attaches the same way (no @ required).
  • You can still @mention a path or name the board in the prompt.

Server hints: when .whiteboard files are in context or your message mentions whiteboards, diagrams, Excalidraw, flowcharts, or similar, OpenCode adds a synthetic <whiteboard-tool> reminder to use list_catalog, apply_template, and insert_figure.

Studio agents should use the built-in whiteboard tool and the versioned corpus in @opencode-ai/whiteboard (server-side) instead of hand-editing raw Excalidraw element JSON. The browser editor does not load the corpus at runtime; agents apply templates and figures through the tool, which writes .whiteboard files to disk. Prefer stable corpus slugs (template.*, figure.*, layout.*, primitive.*) over opaque element id values.

Corpus kindID prefixExamples
templatetemplate.template.sprint-retro, template.flow-diagram, template.system-context
figurefigure.figure.mindmap-node, figure.api-endpoint
layoutlayout.layout.architecture-layers
primitiveprimitive.primitive.rectangle, primitive.arrow, primitive.text

Tool actions:

ActionPurpose
list_catalogList corpus entries (optional kind: template, figure, layout, primitive)
describeSemantic summary of a .whiteboard file (figures, bindings, untagged shapes)
apply_templateStart from or merge a template.* scene (replace: true overwrites the board)
insert_figurePlace a figure.*, layout.*, or primitive.* at canvas x / y with optional label and graph bind

Graph bindings are stored on elements as customData.trellis (for example bind: "issue:42", corpusId: "figure.mindmap-node"). describe reports them as binds:issue:42 for readability. Link boards to issues, entities, or work units so agents and humans can cross-navigate between the graph and the canvas.

Example flow:

  1. whiteboardapply_template on whiteboards/retro.whiteboard with templateId: template.sprint-retro
  2. insert_figure with figureId: figure.mindmap-node, label: "Auth", bind: issue:42
  3. describe to verify structure before finishing the turn

Corpus source lives under packages/whiteboard/corpus/ in the turtlecode repo. Internal spec: studio/specs/whiteboard-ontology.md.

If the agent says it has no whiteboard tools

  1. Restart the OpenCode backend (port 4096) and the Studio dev server after pulling whiteboard changes; tool registration and system-prompt updates load at process start.
  2. Run bun install from the turtlecode repo root so @opencode-ai/whiteboard resolves for OpenCode.
  3. Send a new composer message while a .whiteboard file is open (projection or Code tab), or @ the file path.
  4. Ask explicitly to run whiteboardlist_catalog before editing the board.

Some free models still refuse tool calls occasionally; switching models or starting a fresh session usually clears it.