zooqueen 076d0cce57 three measured fixes on the front page
Walked the page at 390, 768 and 1440 and measured rather than looked. Three
things were wrong, all of them one property:

The hero paragraph was max-w-xl — 576px inside a 768px measure — so the sentence
broke with "a browser." alone on a third line at every width above the phone.
max-w-2xl gives it the room the container already had.

The card's closing two sentences were text-xs, which is 11px here. That is a size
for the label above them, not for prose. text-sm.

The two footer links were 51x15 and 127x15. A thumb needs 44, which the buttons
in the hero already ask for with min-h-11, so the links ask for it too.

What was NOT wrong, having checked: no horizontal overflow at any width, the
feature grid collapses correctly, and every colour pair passes AA against the
background it actually sits on — the footer reads dim and is fine.

No visual guard exists in this repo; the suite is unit tests over pane and tile
logic. These were verified by measuring the live page.
2026-08-17 00:09:41 -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%