Rich elements
A node in the outliner is normally just text. Some nodes carry an element: a table, a code cell, a custom function, a file, a terminal, or a view spec. Elements turn bullets into small, live applications without leaving the page: the surrounding outline, its links, and its multiplayer guarantees all keep working exactly the same way.
The element registry
Section titled “The element registry”Every element type is registered once, on both client and server, as a pair:
type ElementRegistration = { component: React.ComponentType<ElementProps>; server?: ElementService;};The component renders the element in the web, desktop, and extension clients. The
optional server side implements whatever the element needs beyond plain command
storage, spawning a Jupyter kernel for a code cell, running a QuickJS sandbox for a
custom function, or attaching a PTY for a terminal. Looking up an element’s type in the
registry is the only branch point the outliner renderer needs; everything else about
laying out a page, editing its text, and syncing it across windows is generic.
The node-frame pattern
Section titled “The node-frame pattern”Every element renders inside the same node frame:
- it sits directly after the node’s bullet dot, in the position where the node’s text would otherwise be;
- a mono-font header names the element (a table’s name, a code cell’s kernel, a terminal’s path);
- one action area is right-aligned in the header (
+ row, a kernel picker,open as page ↗, a respawn button).
This is a deliberate rule of restraint: an element gets a header and one action area, never a new sidebar, a modal, or a second split pane. Constraining every element to the same frame keeps the outliner feeling like one surface instead of a shell around a grab bag of embedded apps, and it means a page mixing a table, two code cells, and a terminal reads at a glance.
One command protocol for every element
Section titled “One command protocol for every element”An element never mutates its own state directly. Editing a table cell, executing a code
cell, or appending a custom function’s write all go through the same
command model as typing text: a command runs through the
pure reducer, commits in one Postgres transaction, and echoes back over the
outbox to every open window. This is why elements get
multiplayer, undo, and page history for free: a table’s setCell command and a text
node’s setText command are peers as far as persistence, echoing, and conflict
resolution are concerned. Element authors never invent their own storage path or their
own sync channel.
Universal activation: ⌘↵
Section titled “Universal activation: ⌘↵”⌘↵ (Cmd-Enter, Ctrl-Enter on Windows/Linux) is the one shortcut every element
recognizes as “activate this node”:
| Element | ⌘↵ does |
|---|---|
| Code cell | Runs the cell against its kernel |
| Custom function | Converts a call bullet into a live rendered function |
| Plain bullet | Invokes the default agent on that bullet |
Because activation is one shortcut with one meaning across element types, you don’t have to remember a different keybinding per element, and the muscle memory transfers the moment a new element type ships.
The elements
Section titled “The elements”The slash menu
Section titled “The slash menu”Typing / at the start of a bullet opens a popover listing registered commands, today
that means custom functions, with agents joining the same
menu as another entry kind. The list filters as you type; Enter or a click inserts
name( and hands off to the argument UI, the same flow as typing a function call by
hand. The slash menu reads from the same registry that resolves ⌘↵, so anything
invokable from a bullet is discoverable there.