UNIFIED IDENTITY. A pasted token was the honest shape for a backendless app on day one and the wrong one to keep: it is a credential handled by hand, and it is not the identity the rest of Hanzo uses. Sign-in is OIDC authorization code + PKCE against hanzo.id now — the same login as everything else — in about sixty lines against their own discovery document, with no dependency. A static site cannot keep a secret, so PKCE is not a convenience here; it IS what replaces the secret, and `hanzo-tabs` is registered as a genuine public client. The token still lives in this browser, because there is nowhere else it could: inventing a session store would mean inventing the server this product exists without. The difference is that it is now short-lived, refreshable, and issued to this origin rather than copied from a terminal. A PURE STATIC EXPORT. `output: 'export'` — no server at request time, not even a Node one. Tabs reads the control plane from the browser with the caller's own token and frames terminals the machines themselves serve over the zero-trust fabric; both ends of that conversation are already authenticated, so a server here would be a third party to it. The Dockerfile goes with the same reasoning: what ships is a directory of files. Deployed at tabs.hanzo.ai from S3 via hanzoai/static. Two things had to be fixed to get there and both were ours: the S3 adapter refused directory requests outright (fixed in hanzoai/static v0.5.4 — every static site was 404ing at its own root), and the default CSP is `default-src 'none'`, which blocks every script, so the page rendered and nothing ran. Verified in a real browser against the live IdP: the sign-in button lands on hanzo.id/login/oauth/authorize carrying the PKCE challenge, with no console errors. Co-authored-by: Hanzo Dev <[email protected]>
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.