kolu
Docs

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

  1. 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/config aliases pointing at the same box (a repeated localhost,localhost collapses 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 dev
    fleet-top part 3 serving 3 host(s) on http://localhost:7740
  2. Open http://localhost:5176. Each host is a chip; click one to bring its top onto the canvas. Switch between them — the canvas re-keys instantly.

  3. Now break one box — pull its network or kill the agent. Its chip turns warming and 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, turns failed (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.