atriumatrium

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 N from 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].
A fresh install opens the notepad sidebar collapsed; expand it via the chevron on the pane header. Sidebar width and collapsed state are persisted at the app level so every notepad pane shares them.

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.
The sidebar appears in every notepad pane in the workspace. Edits to one note are reflected in every notepad pane viewing that note (the rendered body updates origin-lessly so the iframe / canvas refreshes without a hard reload).

Keyboard navigation

The sidebar is fully keyboard-driveable.

ChordAction
/ Step between workspaces, folders, and notes
Collapse the current workspace / folder
Expand the current workspace / folder
EnterOpen the focused note in the active pane
Cmd+EnterOpen 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 typePersisted per-note
MarkdownScroll position and Lexical cursor/selection
SketchPan offset and zoom level
CanvasScroll position
HTMLScroll 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.

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 workspaceId so notes survive switching between workspaces or worktrees.
  • CLI exportatrium note read <id> --svg or --png renders 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>, ... }. The root references a component by name (Card, Stack, Form, etc.) with props, optional children, and optional on.<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.state and seeded into a per-note store on mount. The in-memory store is round-tripped to state.json so user input survives pane close / open and stays in sync across notepad panes viewing the same note.
  • Actionson.press, on.change, on.select, etc. bind events to one of atrium's canvas actions: send_to_agent for round-tripping state back to the originating agent, and atrium_command for invoking any atrium:// 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_agent posts 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+F find — 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. direction is "row" | "column". Row-stacks default to align: center (so labels and controls line up vertically); column-stacks default to align: stretch. align matches CSS align-items ("start" | "center" | "end" | "stretch" | "baseline"); justify matches CSS justify-content ("start" | "center" | "end" | "between" | "around" | "evenly"). padding respects the app's uiScale.
  • Grid{ columns, gap }. CSS grid container with 1–6 columns. Accepts children.
  • Separator{ orientation }. Visual divider; orientation is "horizontal" | "vertical".
  • Collapsible{ title, defaultOpen? }. Disclosure section. Accepts children.

Text & typography

  • Heading{ text, level }. Renders h1..h6 from level.
  • Text{ content, tone }. Inline or block text. tone is "default" | "muted" | "destructive" | "success".
  • Label{ text, htmlFor? }. Form label, optionally associated with a control by id.
  • Link{ label, href? }. HTTP(S) href opens as a browser subtab on the notepad pane so navigation stays in-app; non-http schemes (mailto:, file://, …) route through the OS opener. Fires press for action bindings.

Visual

  • Icon{ name, size?, color? }. Any Lucide icon by name.
  • Image{ src?, alt?, width?, height? }. Static image; alt required for accessibility.
  • Avatar{ src?, name?, size? }. Circular avatar; falls back to initials derived from name when src is missing.
  • Badge{ text, variant }. Inline pill for status / count / tag. variant is "default" | "success" | "warning" | "destructive" | "muted".
  • Tooltip{ text, content }. Hover tooltip: text is the trigger label, content is the hover body.

Feedback & status

  • Alert{ title?, message, type }. Inline notification box. type is "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 to max.
  • Metric{ label, value, change?, changeType?, prefix?, suffix? }. KPI tile — label + big value + optional delta. changeType is "positive" | "negative" | "neutral" and drives the colour treatment.
  • Rating{ value, max?, label?, interactive? }. Star rating display. value is bindable; with interactive: 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 the press event. variant aligns with atrium's own ui/button: "default" | "primary" | "secondary" | "destructive" | "ghost" | "outline" | "link" ("primary" is an alias for "default").
  • Input{ value, label?, placeholder?, type? }. Single-line text input. value is bindable. type is "text" | "email" | "url" | "number" | "password".
  • Textarea{ value, label?, placeholder?, rows? }. Multi-line input. value is bindable.
  • Select{ value, label?, options, placeholder? }. Dropdown select. value is bindable; options is { value, label }[].
  • Checkbox{ label?, checked, disabled? }. Binds a boolean via checked.
  • Switch{ label?, checked, disabled? }. Toggle switch; binds a boolean via checked.
  • Radio{ label?, value, options }. Radio group. value is bindable to a string (the selected option's value).
  • Slider{ label?, value, min?, max?, step? }. Range slider. value is bindable to a number.
  • Toggle{ label, pressed, variant? }. Single aria-pressed toggle button. pressed is bindable to a boolean. variant is "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 for tabs[i]. value is bindable to the active tab's value.
  • Accordion{ items, type? }. Collapsible accordion. Children render in item order — child[i] is the panel for items[i]. type is "single" | "multiple".
  • ButtonGroup{ buttons, selected? }. Inline row of buttons. When selected is bound, the matching button stays highlighted (radio-like). Each button entry has its own optional variant.
  • Pagination{ totalPages, page }. Page navigation widget; page is 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 at openPath is truthy; closes via the close button or Escape. Accepts children.
  • Drawer{ title?, description?, openPath }. Bottom-sheet drawer with the same openPath semantics as Dialog. Accepts children.
  • DropdownMenu{ label, items, value? }. Dropdown menu — label is the trigger. Picking an item writes its value back through value (if bound) and fires the select event.

Data display

  • Table{ columns, rows, caption? }. Static data table. columns is { key, label, align? }[]; rows are open-shape objects looked up by each column's key. 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's meta.sendFraming, then to the default "{payload}".
  • target (string, optional). A pane id to receive the message. Falls back to the note's meta.originAgentPaneId. If neither resolves, the bridge throws — the notepad chrome's "user terminal" fallback covers this case for the user.
All paths funnel through atrium's canvas bridge so framing, target resolution, and protocol dispatch have one source of truth — no parallel logic across the spec, the HTML 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 with atrium://. 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 with atrium note new --send-framing ... from an agent pane. If neither the action's target override 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 framing param if set, otherwise the note's meta.sendFraming, otherwise the default "{payload}".
This is how an agent can hand the user a small UI to fill in — a configuration form, a "pick one" dialog, a multi-step wizard — and get the answers back as a structured message rather than freeform prose.

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.json for each note.
Note bodies themselves live under ~/.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 `mermaid fence instead.
  • HTML notes intentionally have no access to the host — keep that in mind when an agent generates a tool sheet.
  • atrium note open requires the running app UI; the rest of the surface works headlessly against the storage layer.