tutorial
A fleet of surfaces.
One mirror gives you one remote box. A surface map gives you a whole fleet:
the same top surface, keyed by host, reached through one object. Membership is
one authoritative list and each host is a chip; when one box drops, its chip —
and only its chip — reflects the trouble (warming while it retries, failed
if it needs you), while the rest keep serving. By the end of this page you will
be looking at a miniature drishti.
Type the surface once, key it by host
defineSurfaceMap folds the host key into every entry-member call, so the client
reaches any host’s cells and collections through one object and membership is a
single authoritative collection. The key here is just a hostname, so its codec is
the identity pair.
/**
* The surface MAP — the `top` surface typed once, keyed by host at runtime.
*
* `defineSurfaceMap(keySchema, entry, codec)` folds the host key into every
* entry-member call, so the client reaches any host's cells/collections through
* one object (`app.entry("boxA").cells.load.use(…)`) and membership is one
* authoritative collection. The key is a plain string (a hostname), so the
* codec is the identity pair.
*/
import { defineSurfaceMap, type KeyCodec } from "@kolu/surface-map";
import { z } from "zod";
import { surface } from "./surface";
const HostKeySchema = z.string();
const identityCodec: KeyCodec<string> = {
encode: (k) => k,
decode: (s) => s,
};
export const hostMap = defineSurfaceMap(HostKeySchema, surface, identityCodec);
One registry, one writer of membership
On the parent, a MapRegistry is the single writer of membership. It lists the
hosts, and its resolve(host) hands the map each host’s link plus its projected
connection state — or a fault for an unknown key. serveSurfaceMap(hostMap, registry)
publishes the entries membership collection and forwards every key-folded call
to the right host’s link. That forwarding is why a fault on one box is confined
to that box’s chip rather than taking down the page. (The registry lives in
src/server/main.ts; it is a plain object with members, subscribe, has,
and resolve.)
Render the fleet
The client consumes the map through three hooks: app.entries.use() for the chip
strip, app.entry(host).state() for each chip’s status, and app.useEntry(host)
for the selected host’s canvas — which re-keys on switch, disposing the old
host’s subscriptions and populating the new one’s.
const entries = app.entries.use();
const [activeHost, setActiveHost] = createSignal<string>(
entries.keys()[0] ?? "localhost",
);
const active = app.useEntry(activeHost);
const load = active.cells.load.use();
const memory = active.cells.memory.use();
const processes = active.collections.processes.use();
const rows = createMemo<Pid[]>(() =>
[...processes.keys()].sort(
(a, b) =>
(processes.byKey(b)?.()?.cpuPct ?? 0) -
(processes.byKey(a)?.()?.cpuPct ?? 0),
),
);
// `entry.rpc` is `unknown` on a generic map (a `ContractRouterClient` over an
// abstract `ES` would overflow TS's union budget) — the consumer casts it once
// to its own surface's procedure shape.
type KillRpc = {
surface: {
process: {
kill: (input: { pid: Pid; signal: "TERM" }) => Promise<{ ok: boolean }>;
};
};
};
const kill = async (pid: Pid): Promise<void> => {
await (active.rpc as KillRpc).surface.process.kill({ pid, signal: "TERM" });
};
Watch one box drop on its own
-
Point the parent at several hosts. The map is keyed by hostname, so the targets must be distinct — three real ssh hosts, or three
~/.ssh/configaliases pointing at the same box (a repeatedlocalhost,localhostcollapses to one entry, not three chips). Each must be able to realise the agent derivation.export FLEET_TOP_AGENT_DRV="$(nix eval --raw "$(git rev-parse --show-toplevel)#fleet-top-agent.drvPath")" HOST=boxA,boxB,boxC pnpm run devfleet-top part 3 serving 3 host(s) on http://localhost:7740 -
Open http://localhost:5176. Each host is a chip; click one to bring its
toponto the canvas. Switch between them — the canvas re-keys instantly. -
Now break one box — pull its network or kill the agent. Its chip turns
warmingand stays there, retrying at backoff: an unreachable box is in motion, not failed, so bring it back and it reconnects on its own. Only a box that answers-but-refuses, or gives up terminally, turnsfailed(needs intervention). Either way, every other host keeps streaming and a live host’s canvas is untouched.
That last step is the whole point. There is no global error state, no fleet-wide
spinner: presence is per-entry, and a fault is isolated to the entry that owns
it — which is exactly why the projection distinguishes warming (self-heals)
from failed (needs you). See Entry contracts.
You built a mini-drishti
Look back at what fleet-top became. A surface you declared once (part 1) now runs as a durable daemon (part 2), is mirrored from remote boxes over ssh (part 3), and is presented as a live, keyed fleet where each host stands or falls on its own. Swap “top” for “an agent session” and you have drishti; swap it for “terminals” and you have kolu’s own multi-host canvas. The surface is the part that did not change.
Where to go next: The client half for why the fleet is shaped this way, Serve a map and Operate a fleet safely for the how-to, and the map reference for the wire contracts underneath.