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 examplelens=notesorlens=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
| Lens | Purpose |
|---|---|
| Notes | Quick-capture cards backed by the Trellis store (type:note). Always pinned by default. |
| Whiteboards | Excalidraw diagrams stored as .whiteboard files in the repo. |
| Calendar | Monthly 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/saveandPOST /trellis/calendar/delete; listed withGET /trellis/calendar/events(optionalyearandmonthquery params, month 1–12).
Agents — calendar tool
OpenCode registers a calendar tool for CRUD on the same calendar_event entities:
| Action | Purpose |
|---|---|
list | Optional year + month (1–12) to filter one calendar month |
create | title, startAt (ISO datetime or YYYY-MM-DD with allDay: true), optional endAt, description, color |
update | Patch fields by event id |
delete | Remove 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).
- From the turtlecode repo root:
bun install(installs@excalidraw/excalidraw,react, andreact-dom). - Restart the Studio dev server and hard-refresh the browser.
- If it persists, clear Excalidraw
localStoragekeys (see below) and reopen the file.
If the canvas crashes on load
Symptom: console error collaborators.forEach is not a function.
- Restart the Studio dev server after pulling the latest build.
- 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));
- Reopen the
.whiteboardfile. The next save writes a clean JSON copy withoutcollaborators.
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
.whiteboardfiles 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
.whiteboardeditor 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
@mentiona 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 kind | ID prefix | Examples |
|---|---|---|
| template | template. | template.sprint-retro, template.flow-diagram, template.system-context |
| figure | figure. | figure.mindmap-node, figure.api-endpoint |
| layout | layout. | layout.architecture-layers |
| primitive | primitive. | primitive.rectangle, primitive.arrow, primitive.text |
Tool actions:
| Action | Purpose |
|---|---|
list_catalog | List corpus entries (optional kind: template, figure, layout, primitive) |
describe | Semantic summary of a .whiteboard file (figures, bindings, untagged shapes) |
apply_template | Start from or merge a template.* scene (replace: true overwrites the board) |
insert_figure | Place 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:
whiteboard→apply_templateonwhiteboards/retro.whiteboardwithtemplateId: template.sprint-retroinsert_figurewithfigureId: figure.mindmap-node,label: "Auth",bind: issue:42describeto 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
- 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.
- Run
bun installfrom the turtlecode repo root so@opencode-ai/whiteboardresolves for OpenCode. - Send a new composer message while a
.whiteboardfile is open (projection or Code tab), or@the file path. - Ask explicitly to run
whiteboard→list_catalogbefore editing the board.
Some free models still refuse tool calls occasionally; switching models or starting a fresh session usually clears it.
Related
- Trellis Studio introduction — session layout, file search, and core rail
- Ecosystem desk — multi-repo desk layout and
just trellis -r studio