A workspace read `tabs · hanzo`, `tabs-2 · hanzo`, `tabs-2 · shell-2`. Two bare nouns per pane and neither one told you which box or which shell: `tabs` was the product that opened the machine, and `hanzo` was the company standing where the session's name goes. The shape is now `<machine> · <face>`, and both halves count from one. Machines are `cloud-1, cloud-2` — what the machine IS, one you shell into — and `desk-1, desk-2` for the ones with a screen. The first carries its number like the rest, because `cloud` beside `cloud-2` reads as a different kind of thing rather than the one before it. Two stems and not one: machines are counted against those RUNNING, and the project keys the disk, so a stopped `cloud-2` still owns its home — a desktop later handed that name would mount a shell machine's disk and call it a screen. Faces are `shell-1, shell-2` and `desk`. Only NEW shells take the new name; a pane already open was saved with its binding and comes back on the session it was on, so nothing running is left behind.
Hanzo Tabs
Keep tabs on your agents.
Your coding agents work in shells on real machines. Tabs puts every one of those shells in front of you — split, tiled, and reachable from anywhere you can open a browser.
hanzo link # on any machine with a shell
Then open tabs.hanzo.ai.
Why it is shaped this way
There is no backend. Not "a small one" — none. hanzo link publishes a
terminal from the machine over a zero-trust tunnel, already authenticated and
already gated, and the browser frames it directly. A server here would be a third
party to a conversation that has two, plus one more thing to keep running and one
more place a credential could sit.
A shell is a URL. ttyd runs with --url-arg behind a wrapper that names a
tmux session, so ?arg=build is a shell called build and tmux new -A
attaches or creates. One link serves as many independent shells as you ask for.
There is no spawn endpoint because none is needed — and closing a pane loses
nothing, since reopening the name reopens the session with its scrollback.
The layout is a tree, and that is the whole design. A list of rectangles is
the shape that looks easier and then cannot answer the only question that
matters: close a pane, and who gets the space? With a tree it is structural — the
space belongs to the sibling, so closing replaces the parent split with that
sibling and nothing else moves. lib/tiles.ts is that tree and nothing else:
split, close, resize, sweep order, page projection — every one a pure rewrite,
tested without a browser.
Rectangles are derived, and that keeps a shell alive. Nesting panes in the
DOM the way they nest in the tree remounts every <iframe> on any split, and
each terminal reconnects. So geometry() projects the tree onto percentages, the
panes render as one flat list that never reorders, and only their coordinates
change.
A phone pages; it does not tile. 390px cannot hold two terminals and stay legible. Side-by-side splits become swipeable pages, stacked splits stay stacked, and how many share a page is derived from the room — landscape gets one rather than three slivers.
Layout
lib/tiles.ts the layout tree + geometry + the phone projection (pure)
lib/panes.ts what a pane shows: a machine and a shell name (pure)
lib/api.ts the control-plane read, from the browser
components/ the workspace
app/ the marketing page and the workspace page
tests/ 39 tests, no browser required
The terminal client itself lives in hanzoai/cli
(assets/term/) — xterm.js, themed, with a touch key row — because it is served
by the machine, not by this site.
Develop
npm install
npm run dev # http://localhost:3000
npm test # the layout engine and bindings
npm run typecheck
MIT.