Notepad pane: folders, markdown, sketches, and canvas notes
A location-aware notebook with folders, markdown notes, Excalidraw sketches, JSON-rendered canvas UIs, and sandboxed HTML — readable and writable by agents over the CLI.
The notepad pane is a project-scoped notebook. Notes live next to the project on its owning location, 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
Cmd+Shift+N, the Home sidebar's Notepad row, orCmd+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 project in collapsible sections, each with the project's custom icon and a count chip:
- Folders — create, rename, delete, expand, and collapse folders; drag notes between them or back to the project root. Folder expansion persists across restarts.
- 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 project'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. - Location marks distinguish notes stored on remote boxes. Remote notes stay in the same unified sidebar, while reads, edits, folders, search, and history route to the owning daemon.
Keyboard navigation
The sidebar is fully keyboard-driveable.
| Chord | Action |
↑ / ↓ | Step between projects, folders, and notes |
← | Collapse the current project / folder |
→ | Expand the current project / 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 project 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 |
Opening the same note in a second notepad pane uses the same stored position — switch between panes and you land where you left off in each one.
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. A new note titles itself from its first line, Bear/Obsidian-style, until you rename it explicitly.
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.
- Project-stamped — sketch state is stamped with its
workspaceIdso notes survive switching between projects 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 project's notepad selection (current note ID).
- Sidebar collapse / width (app-level, shared across panes).
- Expanded project IDs in the sidebar.
- Expanded folder paths 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/; remote note bodies live under the equivalent path on their owning location. They are part of that project's Vault snapshots.
Constraints
- Notes are project-scoped — they do not roam across projects. Related project copies on different locations keep separate notes; worktrees share their parent project's notes.
- 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.