Skip to content

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.

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.

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.

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.

⌘↵ (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.

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.