Notepad pane
A workspace-scoped notebook for markdown, sketches, interactive canvases, and sandboxed HTML.
The notepad pane is a workspace-scoped notebook. Notes live next to the project on disk, are searchable from the CLI and the Omni menu, snapshot-versioned by the Vault, and accessible to agents through the atrium note CLI. Four note types share the same sidebar: markdown, sketch, canvas, and HTML.
Opening
- Toolbar tile, or
Cmd+T Nfrom the launcher chain. - Drag the Note tile onto any split zone.
- New-note button in the notepad sidebar — adds a note and swaps it into the current pane rather than stealing a new pane.
- The
+button on a collapsed notepad pane spawns a fresh notepad in the mosaic layout. - CLI:
atrium note new --type markdown|sketch|canvas|html [--open].
The sidebar
Notes are grouped by workspace in collapsible sections, each with the workspace's custom icon and a count chip:
- Inline previews — note title and a short snippet sit on one row.
- Last-updated timestamps appear next to each row.
- Color-coded type icons — markdown blue, sketch teal, canvas violet, HTML amber — match the pane accent.
- Collapsed rail — when the sidebar is collapsed, a slim icon-only rail shows the current workspace's notes so you can scan quickly.
- Empty state when a note is deleted out from under the pane — pick another note or hit
+to start fresh.
Keyboard navigation
The sidebar is fully keyboard-driveable.
| Chord | Action |
↑ / ↓ | Step between workspaces, folders, and notes |
← | Collapse the current workspace / folder |
→ | Expand the current workspace / folder |
Enter | Open the focused note in the active pane |
Cmd+Enter | Open the focused note in a split-right pane |
Per-note viewport persistence
atrium remembers your position inside every note across pane close, room switches, and workspace reloads. The exact thing it remembers depends on the note type:
| Note type | Persisted per-note |
| Markdown | Scroll position and Lexical cursor/selection |
| Sketch | Pan offset and zoom level |
| Canvas | Scroll position |
| HTML | Scroll position inside the sandboxed iframe |
Note types
Markdown
Plain Markdown notes use the same Lexical-based rich editor as the Markdown pane — RTE / source toggle, floating toolbar, inline-comment composer with persisted MDAST directives.
Sketch
Hand-drawn diagrams via an embedded Excalidraw canvas.
- Theme-aware — Excalidraw inherits the active atrium theme so the canvas background, tool palette, and selected-tool tinting match light, dark, and colored themes instead of clashing.
- Workspace-stamped — sketch state is stamped with its
workspaceIdso notes survive switching between workspaces or worktrees. - CLI export —
atrium note read <id> --svgor--pngrenders the sketch to bytes on stdout, useful when an agent wants to dump a diagram into a PR description.
Canvas
A canvas note is a JSON-render surface: a declarative JSON spec describing a layout of components, rendered by the @json-render/* runtime against atrium's canvas catalog. The spec is the source of truth — agents (or you) write JSON, the renderer paints a real React UI, and component values bind two-way into a shared per-note store. No code execution is involved; canvas notes ship structure, not behavior.
- Spec shape —
{ root: <component>, state: <object>, ... }. Therootreferences a component by name (Card,Stack,Form, etc.) withprops, optionalchildren, and optionalon.<event>action bindings. Authoring the spec is the same skill regardless of which 42 components you stitch together. - State binding — values that should be reactive use the
{ "$bindState": "/path/to/value" }directive. The renderer resolves the directive at runtime, wires the component up two-way (useBoundProp), and any write echoes back through the store. - State persistence — declared via
spec.stateand seeded into a per-note store on mount. The in-memory store is round-tripped tostate.jsonso user input survives pane close / open and stays in sync across notepad panes viewing the same note. - Actions —
on.press,on.change,on.select, etc. bind events to one of atrium's canvas actions:send_to_agentfor round-tripping state back to the originating agent, andatrium_commandfor invoking anyatrium://URI. - Component catalog — 42 components grouped by purpose. The full reference is below. Buttons wrap atrium's own
ui/button, so canvas chrome matches the rest of the app.
HTML
A sandboxed HTML page rendered inside an iframe with the sandbox="allow-scripts" attribute. Communication with atrium is via postMessage:
- Outbound
send_to_agentposts pipe through the same chrome as canvas notes. - The iframe re-renders origin-lessly on edit, so changes in one pane refresh other panes viewing the same note.
- HTML notes do not get access to the parent window, the host filesystem, cookies, or
localStorage. Treat them like a self-contained tool sheet, not an extension.
Spec / body editor
Canvas and HTML notes have a view-mode toggle in the pane header that swaps the rendered surface for a full Monaco editor on the underlying spec (JSON for canvas, HTML for HTML notes):
- Syntax highlighting, formatting, completions, and
Cmd+Ffind — same engine as the editor pane. - Validates on save and surfaces parse errors inline.
- Sketch and markdown notes do not have a spec editor — they're already in their native format.
Canvas component reference
The 42 components in the canvas catalog. Every component shares the same shape — declared by name, configured via props, optionally accepting children, and emitting events via on.<event> bindings. Props that accept a { "$bindState": "/path" } directive for two-way state are marked bindable. The catalog is a stable subset across @json-render/* minor-version bumps; adding new components is backward compatible.
Each entry below leads with the component name and its prop signature, followed by behavior notes.
Layout & structure
- Card —
{ title?, description? }. Container with optional title and description. Accepts children. - Stack —
{ direction, gap, padding, align, justify }. Flex container.directionis"row" | "column". Row-stacks default toalign: center(so labels and controls line up vertically); column-stacks default toalign: stretch.alignmatches CSSalign-items("start" | "center" | "end" | "stretch" | "baseline");justifymatches CSSjustify-content("start" | "center" | "end" | "between" | "around" | "evenly").paddingrespects the app'suiScale. - Grid —
{ columns, gap }. CSS grid container with 1–6 columns. Accepts children. - Separator —
{ orientation }. Visual divider;orientationis"horizontal" | "vertical". - Collapsible —
{ title, defaultOpen? }. Disclosure section. Accepts children.
Text & typography
- Heading —
{ text, level }. Renders h1..h6 fromlevel. - Text —
{ content, tone }. Inline or block text.toneis"default" | "muted" | "destructive" | "success". - Label —
{ text, htmlFor? }. Form label, optionally associated with a control by id. - Link —
{ label, href? }. HTTP(S)hrefopens as a browser subtab on the notepad pane so navigation stays in-app; non-http schemes (mailto:,file://, …) route through the OS opener. Firespressfor action bindings.
Visual
- Icon —
{ name, size?, color? }. Any Lucide icon by name. - Image —
{ src?, alt?, width?, height? }. Static image;altrequired for accessibility. - Avatar —
{ src?, name?, size? }. Circular avatar; falls back to initials derived fromnamewhensrcis missing. - Badge —
{ text, variant }. Inline pill for status / count / tag.variantis"default" | "success" | "warning" | "destructive" | "muted". - Tooltip —
{ text, content }. Hover tooltip:textis the trigger label,contentis the hover body.
Feedback & status
- Alert —
{ title?, message, type }. Inline notification box.typeis"info" | "success" | "warning" | "destructive". - Spinner —
{ size?, label? }. Indeterminate loader with optional label. - Skeleton —
{ width?, height?, rounded? }. Placeholder block while real content is loading. - Progress —
{ value, max?, label? }. Determinate progress bar from 0 tomax. - Metric —
{ label, value, change?, changeType?, prefix?, suffix? }. KPI tile — label + big value + optional delta.changeTypeis"positive" | "negative" | "neutral"and drives the colour treatment. - Rating —
{ value, max?, label?, interactive? }. Star rating display.valueis bindable; withinteractive: true, clicking a star writes back through the binding.
Form controls
All bindable controls write through the { "$bindState": "/path" } directive on the marked prop.
- Button —
{ label, variant?, disabled? }. Fires thepressevent.variantaligns with atrium's ownui/button:"default" | "primary" | "secondary" | "destructive" | "ghost" | "outline" | "link"("primary"is an alias for"default"). - Input —
{ value, label?, placeholder?, type? }. Single-line text input.valueis bindable.typeis"text" | "email" | "url" | "number" | "password". - Textarea —
{ value, label?, placeholder?, rows? }. Multi-line input.valueis bindable. - Select —
{ value, label?, options, placeholder? }. Dropdown select.valueis bindable;optionsis{ value, label }[]. - Checkbox —
{ label?, checked, disabled? }. Binds a boolean viachecked. - Switch —
{ label?, checked, disabled? }. Toggle switch; binds a boolean viachecked. - Radio —
{ label?, value, options }. Radio group.valueis bindable to a string (the selected option's value). - Slider —
{ label?, value, min?, max?, step? }. Range slider.valueis bindable to a number. - Toggle —
{ label, pressed, variant? }. Single aria-pressed toggle button.pressedis bindable to a boolean.variantis"default" | "outline". - ToggleGroup —
{ value, type?, items }. Row of toggle buttons.type: "single"is radio-like and binds a string;type: "multiple"binds a string array.
Disclosure & interactive
- Tabs —
{ tabs, value, defaultValue? }. Tabbed interface. Children render in tab order —child[i]is the panel fortabs[i].valueis bindable to the active tab's value. - Accordion —
{ items, type? }. Collapsible accordion. Children render in item order —child[i]is the panel foritems[i].typeis"single" | "multiple". - ButtonGroup —
{ buttons, selected? }. Inline row of buttons. Whenselectedis bound, the matching button stays highlighted (radio-like). Each button entry has its own optionalvariant. - Pagination —
{ totalPages, page }. Page navigation widget;pageis bindable to the current page (1-indexed). - Carousel —
{ items }. One-slide-at-a-time horizontal carousel with prev / next arrows and position dots. Each item is{ title?, content? }. - Popover —
{ trigger, content }. Floating panel anchored to a trigger button. Opens on click; closes on outside click or Escape. - Dialog —
{ title?, description?, openPath }. Modal dialog. Opens when the state atopenPathis truthy; closes via the close button or Escape. Accepts children. - Drawer —
{ title?, description?, openPath }. Bottom-sheet drawer with the sameopenPathsemantics as Dialog. Accepts children. - DropdownMenu —
{ label, items, value? }. Dropdown menu —labelis the trigger. Picking an item writes its value back throughvalue(if bound) and fires theselectevent.
Data display
- Table —
{ columns, rows, caption? }. Static data table.columnsis{ key, label, align? }[];rowsare open-shape objects looked up by each column'skey. Values coerce to strings for display. - BarGraph —
{ title?, data }. Recharts-shaped data: an array of{ name, ...series }records. Each numeric key becomes a bar series. - LineGraph —
{ title?, data }. Same data shape as BarGraph; each numeric key becomes a line series.
Canvas actions
Canvas actions are how a spec talks back to atrium. They're invoked from any on.<event> binding — for example "on": { "press": { "action": "send_to_agent", "params": { ... } } }. Two actions ship today.
send_to_agent
Submits the current canvas state to an agent pane. Params:
payload(any). Required. Any shape — typically the bound store via{ "$bindState": "$state" }. The bridge JSON-stringifies it before framing.framing(string, optional). Template applied to the payload before dispatch. Variables:{payload},{noteId},{noteTitle},{actionId}. Falls back to the note'smeta.sendFraming, then to the default"{payload}".target(string, optional). A pane id to receive the message. Falls back to the note'smeta.originAgentPaneId. If neither resolves, the bridge throws — the notepad chrome's "user terminal" fallback covers this case for the user.
postMessage listener, and the chrome's Send button.
atrium_command
Invokes any atrium:// URI through the protocol router — open a file, switch a room, create a note, dispatch a command. Params:
uri(string). Required. Must start withatrium://. Malformed URIs are rejected at action-fire time and surface in DevTools — the protocol router never sees them. See the protocol reference for the available routes.
Send to agent
Canvas and HTML notes both round-trip back to an agent through the same path. Canvas notes invoke the send_to_agent action from any spec event binding; HTML notes post the same payload over postMessage. Either way the dispatch funnels through atrium's canvas bridge, applies framing, resolves the target, and routes via atrium://agents/{id}/message with a toast on success or failure.
The notepad chrome sits around both surfaces and exposes the user-visible controls:
- A target dropdown picks which agent pane receives the message. The default is the note's
meta.originAgentPaneId— set automatically when a note is created withatrium note new --send-framing ...from an agent pane. If neither the action'stargetoverride nor the meta default is set, the chrome offers a "user terminal" fallback entry so the user always has somewhere to send. - Sidebar buttons mirror the dispatch verbs the canvas itself can fire, so simple notes can be sent without the user needing to click inside the canvas.
- Framing comes from the action's
framingparam if set, otherwise the note'smeta.sendFraming, otherwise the default"{payload}".
CLI
The full note surface is callable from any shell with $ATRIUM_CLI_PATH set. See CLI → note for the full reference.
# List, search, read, write, delete
atrium note list [--workspace <id>] [--tag <t>] [--type <t>] [--source user|agent] [--folder <p>]
atrium note search <query> [--workspace <id>] [--limit N]
atrium note read <id> [--svg | --png]
atrium note write <id> [--content <md> | --from-file <path>]
atrium note delete <id> --confirm
# Create — for canvas/html, body comes via --spec / --body
atrium note new --title "Sprint review" --type markdown
atrium note new --title "Architecture" --type sketch
atrium note new --title "Config picker" --type canvas --spec ./spec.json --open
atrium note new --title "Tool sheet" --type html --body ./body.html --send-framing '{payload}' --open
# Open + history
atrium note open <id>
atrium note history <id> [--limit N]
--open dispatches atrium://commands/notepad.open to land the new note in a notepad pane in the current room. Without --open the note is durable on disk but does not steal focus — useful when an agent is generating notes in the background.
Persistent state
The notepad pane snapshot stores:
- The active workspace's notepad selection (current note ID).
- Sidebar collapse / width (app-level, shared across panes).
- Expanded workspace IDs in the sidebar.
- Per-note viewport state (scroll, zoom, cursor) keyed by
(noteId, paneId). - For canvas notes — the round-tripped
state.jsonfor each note.
~/.atrium/workspaces/<id>/notes/ and are part of every workspace and Vault snapshot.
Constraints
- Notes are workspace-scoped — they do not roam across workspaces (worktrees do share their parent workspace's notes today, but that's the only cross-link).
- Mermaid notes from older builds still load (the storage enum keeps the variant), but new mermaid notes are not creatable — use a markdown note with a
`mermaidfence instead. - HTML notes intentionally have no access to the host — keep that in mind when an agent generates a tool sheet.
atrium note openrequires the running app UI; the rest of the surface works headlessly against the storage layer.
