zooqueen 4b4bf9dfce tabs: a pane names the machine and which of its faces, both numbered
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.
2026-08-15 10:34:01 -07:00

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.

S
Description
No description provided
Readme
503 KiB
Languages
TypeScript 96.8%
JavaScript 2.6%
CSS 0.6%