Suplex
the filmsuplex@a.skh.am#film

72 seconds, with music and no speech. One workspace in every client: Suplex for Linux, the sidebar in a terminal, the Mac, and Android on a Harbour home screen and in a Flotilla float. Terminals and agents of three nodes side by side, and the same workspace on a desk and a phone at once.

Filmed by script on 14 Nov 2026, on a fleet of three made-up nodes on one machine: Suplex 0.1.0, Suplex for Android 0.6.14, Harbour 0.12.102, Flotilla 0.1.53, Rex 0.0.1793741575, pi-daemon 1.14.0. The agents are real Pi sessions on real daemons, and what they say is scripted so that every take is the same. The phone is an emulator; what Nix-on-Droid runs on a real one (its own Rex, pi-daemon, pi-client and Suplex daemon) runs beside it as the node phone. The Mac's window is the real one, recorded through the Mac's own screen sharing at about nine pictures a second. Some shots run faster than they were filmed, the fastest 4.6 times.

ideasuplex@a.skh.am#lede

Suplex is a multiplexer of multiplexers. A terminal multiplexer such as Rex keeps terminals in sessions, tabs and splits; a browser keeps pages; an agent host keeps conversations. Suplex lists what each of them holds, on every machine, and lets one workspace of tabs and splits point at any of it, side by side.

It stores the layout and nothing else. Every pane still belongs to the backend that made it, and its stream comes straight from there.

todayRex's terminals in every client, and pages on the Mac, on Linux and on Android. More backends are in progress.

one workspace, three ownerssuplex@a.skh.am#stitch

What already runs

desk · a Mac
rex@deskterminals
  • worksession
  • editorwindow
  • vimblock
  • testswindow
  • cargo testblock
tyler@deskpages
  • Workwindow
  • Grafanapage
atlas · a server
rex@atlasterminals
  • agentssession
  • wrenwindow
  • logswindow

What Suplex stores

workspace work7 frames
  • tabs
  • splitbuildrow
  • sloteditorrex@desk#block:01a1…
  • splitcolumn
  • slottestsrex@desk#block:01a4…
  • slotgrafanatyler@desk#p_7f3a
  • slotagentsrex@atlas#session:01b2…

A tree, and four addresses. No terminal's bytes and no page are in it.

What a client draws

work
editorrex@desk
  1 ## Remaining work
  2 
  3 | ID | Work | Needs |
  4 | --- | --- | --- |
  5 | V2 | frames views | V1 |
  6 | X6 | resume a slot |  |
~
~
PLAN.md                 3,1
testsrex@desk
running 16 tests
................
ok. 16 passed
grafanatyler@desk
grafana.example
wrenlogsrex@atlas · a whole session
wren  reading tests/merge.rs
wren  3 of 16 cases need the new op
wren  writing the fourth
▍
$ git status --short
 M crates/core/src/op.rs
 M tests/merge.rs
$ 

The tab on show is this device's own. Each pane's stream comes straight from its backend: a terminal's bytes, a page's pictures or its address.

modelsuplex@a.skh.am#model

Workspaces, frames and slots

A workspace is a named tree. Its nodes are frames, of three kinds, and its leaves point at things that backends own.

tabsshows one child at a time
splitlays its children out in a row or a column; each child has a weight
slota leaf: empty, or one reference to an entity of a backend

Suplex makes nothing of its own. Filling a slot asks a backend to make something new, or pins something it holds already; the slot keeps the address, and the backend's recipe for bringing the thing back should it go. An empty slot offers both.

work, drawn from its documentsuplex@a.skh.am#frames
tabsbuildagents
splitbuildrow
sloteditor×3
rex@atlas#block:…510bterminal
splitcolumn×2
slottests
rex@atlas#block:…67ccterminal
slotdocs
fake@atlas#p_7page
slot agentsrex@atlas#session:…e8c9session: a pin
what the master stores for worksuplex@a.skh.am#document
{
  "name": "work", "root": "f_…ARSZ",
  "frames": {
    "f_…ARSZ": { "kind": "tabs" },
    "f_…00YP": { "kind": "split", "parent": "f_…ARSZ", "pos": "V", "dir": "row", "title": "build" },
    "f_…0E1D": { "kind": "slot", "parent": "f_…00YP", "pos": "V", "title": "editor", "weight": 3,
      "ref": { "at": "rex@atlas#block:…510b", "kind": "terminal", "resume": { the backend's recipe } } },
    "f_…RWB3": { "kind": "split", "parent": "f_…00YP", "pos": "W", "dir": "column", "weight": 2 },
    "f_…3BJJ": { "kind": "slot", "parent": "f_…RWB3", "pos": "V", "title": "tests",
      "ref": { "at": "rex@atlas#block:…67cc", "kind": "terminal", "resume": { the backend's recipe } } },
    "f_…8RE0": { "kind": "slot", "parent": "f_…RWB3", "pos": "W", "title": "docs",
      "ref": { "at": "fake@atlas#p_7", "kind": "page", "resume": { the backend's recipe } } },
    "f_…XNF4": { "kind": "slot", "parent": "f_…ARSZ", "pos": "W", "title": "agents",
      "ref": { "at": "rex@atlas#session:…e8c9", "kind": "session", "resume": { the backend's recipe } } }
  }
}
an addresssuplex@a.skh.am#address

An ID is the backend's own and says nothing of where the thing sits, so an address survives a move. It may name a leaf or a container.

  • rex:work@deska second Rex server on one machine
  • rex@desk#*everything that backend holds
pinssuplex@a.skh.am#pins

A slot that points at a container is a pin

A whole Rex session, a window, a whole backend. The client draws whatever the backend has there now, from the backend's live tree, as the backend arranges it. Nothing under a pin is stored in Suplex, so what other programs change there never touches the master.

What is done inside a pin is the backend's own operation. Step the keys one layer in, and a split made there is Rex's split and closing a pane closes it in Rex. One layer out, the same keys act on the slot as a whole.

whose it issuplex@a.skh.am#shared
The workspace's: written, syncedThis device's: never merged
frames, their order and namesthe tab on show in each set of tabs
weights: a line moved here moves everywherethe pane in use, zoom, scroll
what each slot points atwhether a split is laid out or paged through

A terminal has one size. The view a person last touched sets it; other views of it crop or scale.

backendssuplex@a.skh.am#backends

Backends

A backend is a multiplexer that speaks the contract to its machine's daemon over a local socket: JSON-RPC, one object a line. The contract is additive. A backend keeps working with no Suplex anywhere.

Rex needed no change: suplex backend rex runs beside a Rex server as one more client of Rex's own control API.

rexterminalsworks
referenceterminals, pages as addresses, a screen as pictures; every method of the contractworks
tylera browser's pages; its own daemon speaks the contract. Linux draws themin part
desktopsa machine's screens and windows, as pictures: an X server, a Mac, an Android device, programs kept in cages. Built; not yet in daily usein progress
pia machine's agent sessions, listed and made new. Built; not yet turned onin progress
each backend declares its own shapesuplex@a.skh.am#shapes
rexterminals
  • sessiontabs
  • windowfloat
  • layersingle
  • splitsplit
  • blockleaf
tylerpages
  • windowtabs
  • tabsingle
  • grouptabs
  • splitsplit
  • pageleaf
referencebackend fake
  • windowtabs
  • tabsingle
  • splitsplit
  • paneleaf
suplexa workspace
  • tabstabs
  • splitsplit
  • slotleaf
nest freely
  • tabs ordered, one on show
  • split a row or a column, by weight
  • single one child, which fills it
  • float rectangles, front to back
  • list an inventory
  • leaf a terminal, a page

No backend is pressed into session, tab, block. Each says which levels it has and how a level arranges its children. Suplex's own tree is the one that can hold any of them.

the contractsuplex@a.skh.am#contract
helloIts schema: the levels, how each arranges its children, the operations each level takes, the kinds of leaf and how each can be shown.
treetree.get and tree.watch: entities with IDs that are the backend's own and stay for the entity's life; changes told as they happen.
operationsentity.create, close, move, rename, select, focus, resize, zoom. Each carries a request key: asked twice, it is done once.
viewsview.open answers offers, best first: vt terminal bytes, frames pictures, surface a shared native surface, url an address the client loads, external the backend's own app.
resumeA recipe for bringing an entity back. The backend writes it; Suplex keeps it unread and hands it back when what a slot pointed at has gone. And what letting go of an entity does, keep or close.
# the first line a backend sends, and the daemon's answer
{"jsonrpc": "2.0", "id": 1, "method": "backend.hello", "params": {
  "protocol": "suplex.backend.v0", "backend": "yours", "version": "1.0",
  "schema": SCHEMA, "capabilities": ["watch", "resume", "views", "state"]}}
{"jsonrpc": "2.0", "id": 1, "result": {"node": "atlas", "suplexd": "0.1.0",
  "capabilities": [those both have]}}
asked of a fleetsuplex@a.skh.am#asked
suplex node listatlas (here)  master  0.1.0 55be3ba2  -  fake, rexsuplex backend listfake@atlas  0.1.0  [watch resume views state unasked shown place]  window (tabs) > tab (single) > split (split) > pane (leaf)rex@atlas  0.1.0  [watch state resume views unasked]  session (tabs) > window (float) > layer (single) > split (split) > block (leaf)suplex tree rex@atlas --depth 5rex@atlas├─ work  session  [session:…0d7a]│  └─ editor  window  [window:…9476]│     └─ layer  [layer:…0834]│        └─ split  [split:…8009]│           ├─ shell  block terminal  [block:…510b]│           └─ shell  block terminal  [block:…67cc]└─ agents  session  [session:…e8c9]   └─ wren  window  [window:…f810]      └─ layer  [layer:…4047]         └─ shell  block terminal  [block:…ef39]
the smallest backendsuplex@a.skh.am#write-one
SCHEMA = {
    "levels": [
        {"name": "window", "arrangement": "tabs", "children": ["page"], "ops": ["rename"]},
        {"name": "page", "arrangement": "leaf", "kinds": ["page"], "ops": ["rename"]},
    ],
    "root": ["window"],
    "kinds": {"page": {"presentations": ["frames"], "size": "shared"}},
}
suplex tree demo@atlasdemo@atlas└─ Work  window  [w1]   ├─ Grafana  page  [p1]   └─ Docs  page  [p2]

The example backend is 152 lines of Python with no packages. It says hello with this schema, two levels, and answers tree.get and entity.rename from a small tree in memory. That is enough to be listed in the fleet, as above, and pinned into a slot.

held to the contractsuplex@a.skh.am#check
suplex backend check fake@atlas --mutatefake@atlas 0.1.0 [watch resume views state unasked shown place]pass  H0   the backend is registered with the daemon, which took its hello and schemapass  T1   tree.get without an ID gives the top-level entitiespass  T2   the whole tree (7 entities) follows the schema and hangs togetherpass  T3   an answer is one message: at most 5,000 entities and under a megabytepass  T4   tree.get with depth 0 gives the entity alonepass  T5   tree.get with depth 1 gives the entity and its childrenpass  T6   an unknown ID is refused as not_foundpass  T7   an unknown method is refused as unsupportedpass  T8   an unknown field is ignoredpass  T9   what stands at the top is given in parts: after one of it, what follows and none of it againpass  W1   tree.watch gives a watchpass  O1   entity.create makes an entity, in the tree when it answers… 48 more lines: 48 pass60 passed, 0 failed, 0 skipped

One command holds a backend to the contract and to its own schema, rule by rule. The reference backend passes all 60 rules, asked through a daemon, and the check is itself tested by breaking that backend one rule at a time. For a backend in Rust, the crate suplex-backend has the protocol, request keys, watches, views and the stream of pictures written once.

three nodes, one mastersuplex@a.skh.am#roles
Three nodes, one masterThree machines, phone, atlas and desk, each run one Suplex daemon. Each has a client: Suplex for Android on the phone, the terminal sidebar on atlas, Suplex for the Mac on desk. A client talks only to the daemon on its own machine. The daemons on phone and desk exchange edits with the daemon on atlas, which is the master. Each daemon speaks to the backends on its own machine over a local socket: Rex on all three, and Tyler on desk. The stream of a pane passes beside the daemons and not through them: each client connects straight to the backend, and the Mac app on desk also shows a terminal of the Rex server on atlas that way.NODEphoneSuplex for Androida clientsuplexda full copyrexterminalsNODEatlassuplex tuia clientsuplexdthe masterrexterminalsNODEdeskSuplex for the Maca clientsuplexda full copyrexterminalstylerpageseditseditsa terminalon atlas
  • the layout: edits, small JSON
  • control, on a local socket
  • a pane's stream, straight from its backend
fleetsuplex@a.skh.am#fleet

Across machines

Every machine runs the same daemon. One is the master: it keeps the workspaces, orders edits and relays them. It never carries a pane's data.

A client talks only to the daemon on its own machine, which holds a full copy, opens from it at once and keeps working when the master cannot be reached.

  • < 5 msan edit on one node reaches a client of another, at the 99th percentile
  • < 14 MiBthe master's own memory, holding 100 workspaces of 20 frames
  • < 20 mssuplex workspace list, at the median

Budgets a test holds, with three daemons on one machine.

when two edits meetsuplex@a.skh.am#merging
Two edits to one propertyThe later one wins, by a clock that travels with the edit. A stale offline edit does not beat a newer one that got there first.
Edits to different properties or framesBoth apply.
A delete whose author had not seen the newest edit under that frameRefused: the edit wins.
An edit under a frame deleted meanwhileApplies, and brings the frame back.
A move that would put a frame inside itselfRefused.
what is never lostsuplex@a.skh.am#kept

The master tells every node of an edit at once and answers its author only when the edit is on disk. Until that answer the author keeps the edit, in memory and in a journal.

So an edit whose author was answered is on the master's disk, and one that was only told around is still held where it was made. Killing a daemon loses neither.

Access is the tailnet's: like Rex's own servers, a daemon admits callers on the tailnet without a login, and refuses what only a browser sends.

clientssuplex@a.skh.am#clients

Clients

A client draws a workspace. The connection, the edits, how a split shares its room, the keymap and following a backend's stream are one library under all of them.

suplex tuiworksthe sidebar: a tree, and a workspace laid out with a live terminal in every pane
Macbetanative; Ghostty's terminal, its own web view for a page, and pictures of what a backend draws
Linuxworksnative and drawn by itself; Ghostty's terminal core, and a page from its backend's pictures
Androidinternal testingterminals, its own web view for a page, and pictures of what a backend draws
CLI, MCPworksthe same operations, for scripts and for agents
one librarysuplex@a.skh.am#library
suplex tuiRust
LinuxRust
MacSwift
AndroidKotlin
the C surfacesuplex.h: JSON in and out, nothing that blocks
suplex-clientthe connection, the session a client watches, edits that take several calls, views of what backends show
suplex-coredocuments, edits and how they merge, how a split shares its room, filling a slot, the keymap. No I/O.

What is the same in every client is written once, here. Each client brings its own terminal emulator (Ghostty's, on the Mac and on Linux), and the library hands it the backend's bytes as they arrive.

one keymapsuplex@a.skh.am#keys

CtrlA or CtrlB, then

  • hjklthe pane that way
  • |Ssplit right, split down
  • cnpnew tab, next, previous
  • 1–9a tab by its number
  • x&close the pane, the tab
  • z=zoom, even out
  • ,$rename a tab, the workspace
  • g:switch workspace, every action

One keymap for every client, read from one file, keys.yaml. The sidebar, the Mac app and the Linux app have every chord here. One layer in, a chord is the backend's own: in a pinned Rex session, | makes Rex's split.

trysuplex@a.skh.am#try

A run, as it happened

A throwaway daemon called atlas, a private Rex server with two sessions, and the reference backend. Every line below is what the program printed; long IDs are cut to their last four.

stitching worksuplex@a.skh.am#run
suplex workspace create workcreated work  w_…51W1suplex frame add work --kind split --dir row --title buildadded f_…00YPsuplex frame add work --kind slot --parent f_…00YP --title editor --weight 3added f_…0E1Dsuplex frame fill work f_…0E1D --pin rex@atlas#block:…510bf_…0E1D points at rex@atlas#block:…510bsuplex frame fill work f_…8RE0 --pin fake@atlas#p_7f_…8RE0 points at fake@atlas#p_7suplex frame add work --kind slot --title agentsadded f_…XNF4suplex frame fill work f_…XNF4 --pin rex@atlas#session:…e8c9f_…XNF4 points at rex@atlas#session:…e8c9suplex workspace show workwork  w_…51W1  [tabs  [f_…ARSZ]]├─ build  split row  [f_…00YP]│  ├─ editor  rex@atlas#block:…510b  ×3  [f_…0E1D]│  └─ split column  ×2  [f_…RWB3]│     ├─ tests  rex@atlas#block:…67cc  [f_…3BJJ]│     └─ docs  fake@atlas#p_7  [f_…8RE0]└─ agents  rex@atlas#session:…e8c9  [f_…XNF4]
where it standssuplex@a.skh.am#status

Works today

  • The daemon, the master and its followers, merging, the CLI and the MCP server.
  • Rex's terminals in all four clients, and a whole Rex session in one slot, drawn live.
  • Pages: in a web view of the app's own on the Mac and on Android, and from the backend's pictures on Linux.
  • A slot whose terminal has gone is brought back from its recipe when a person asks, and by itself only where its backend allows.
  • The reference backend and the conformance check, for anyone writing a backend.

Not yet

  • Pages in the sidebar, which shows a card with the address.
  • Desktops and agent sessions as backends in daily use. They are built.
  • iOS. And the repository is private, so there is nothing here to install.