ChromeBar identity rail (srv · client · kaval)
The consolidated connection + build/commit readout in the ChromeBar. A2 shipped it as two coinciding columns (srv = server, pty = the in-process pty-host); B2 overtook that shape — renaming pty → kaval as a separate spawned daemon and adding a client-bundle column — so the live rail is three columns (srv · client · kaval). This note keeps A2's original design rationale, annotated where B2 took over.
The UI design for A2’s deliverable #3 — the consolidated connection + build/commit readout in the ChromeBar, shipped with A2 in
#1063 as packages/client/src/ui/IdentityRail.tsx. Built in its final two-column shape from day one — srv (the server you’re connected to) and pty (the pty-host serving your terminals) — even though in A2 they’re the same process. That coincidence is the point: it’s the live proof A2’s identity plumbing works, and it means Phase B only has to let the columns diverge, never re-lay-out. The original prototype was rendered with the live dark-theme tokens from packages/client/src/index.css against the real ChromeBar.tsx layout.
The design call — two explicit columns in A2
The R-4 plan originally deferred the daemon chip to B, reasoning that an always-green “daemon connected” chip would lie about an absent daemon. But in A2 the pty-host isn’t absent — it’s in-process. So a two-column srv · pty readout where the columns coincide is the literal truth, not a lie. Better still, it’s the cleanest acceptance signal A2 could have:
- It’s the acceptance test. A one-sided readout would show the server’s commit and call it done — exercising none of A2’s actual wire work. The two-column rail forces the whole path to light up: server boots → fetches
system.version()→ relays.identityontoserver.info→ client renders theptycolumn. The columns matching is the green test; a mismatch in A2 means the plumbing B depends on is broken. - The dot was already “srv”. The standalone WebSocket status dot represents the client↔server link — i.e.
srvliveness. Consolidating it into the rail removes a redundant indicator and givesptya parallel slot for its own liveness (in-process now, daemon-handle in B). - What’s genuinely inert until B: only the divergence — the
outdated/deadstates — because nothing can diverge from itself. Those branches are wired in A2 (the rail is divergence-capable) but cannot fire untilptyis a separate surviving process. The only A2 cost is a few deadclassListbranches — paid down to zero the moment B lands, with no re-layout and no migration of the client component.
The rail sits in the left identity cluster where the bare WebSocket dot used to be: it’s identity + liveness, not an action, so it groups with the logo, not the right-hand control cluster.
Wiring — three hops
The rail is thin presentation, but the data takes three hops, because the client speaks kolu-server’s surface (over the WebSocket), not the pty-host’s:
srv | pty | |
|---|---|---|
| liveness dot | WebSocket status (the consolidated dot) | in-process (A2) → daemon handle (B) |
| commit | server’s KOLU_COMMIT_HASH | identity.navigableCommit |
| build | — | identity.staleKey (closure hash) |
| source | server.info (existing) | system.version → relayed |
Only pty carries a build: the staleKey is the @kolu/pty-host closure hash, and it’s the only thing whose staleness matters across a restart — the server always restarts on deploy, so it has no “survives” staleness, just a commit.
Three constraints baked in: (1) identity is optional on the wire so a B-phase older daemon isn’t force-restarted just to add a diagnostic (PTY_HOST_CONTRACT_VERSION unbumped); (2) currentBuildId/currentCommitHash re-export as values, not import type — the regression that collapsed the typed client to unknown; (3) the commit href is the full SHA (display slice(0,7)), matching the recovered 3fd7ea6 renderer and avoiding ambiguous-prefix edge cases.
States
In A2 the rail’s live axis is the WebSocket connection (the dot it absorbed). pty follows srv because it’s the same process — when the link is down the client can’t know anything, so pty reads unknown, not a false green.
| State | Phase | Rendering |
|---|---|---|
| Connected | live in A2 | WebSocket open · srv ≡ pty · both commits + build resolved · the ≡ in-process tag |
| Connecting / reconnecting | live in A2 | re-handshaking — both dots pulse amber, identity dimmed until the first yield (srv connecting… · pty —) |
| Disconnected | live in A2 | srv red; pty unknown (grey) — honest: with the link down we can’t claim pty state |
| Update pending | live · B3.4 | kaval build ≠ the build the server would spawn → amber ⬆ update via the read-site kavalStale(expected, reported, state) derivation, comparing the server’s buildInfo.expectedKaval.staleKey against the connected daemon’s reported daemonStatus.identity.staleKey (B3.4,
#1353 ). The nudge #1034 over-fired now fires only here. |
| Daemon dead | live · B2 | kaval handle closed → red (daemon dead — restart). Pairs with the honest degraded canvas; never the empty-canvas lie. B3.2 (#1337) added the inline Restart kaval button. |
No “dev / no-commit” state exists — kolu and kaval run only under nix, which always bakes both the server’s KOLU_COMMIT_HASH and kaval’s KAVAL_BUILD_ID / KAVAL_COMMIT_HASH. There is no off-nix fallback to render.
Decisions (resolved with the maintainer)
- Coincidence rendering — always two explicit columns + the
≡ in-processtag; verbose/explicit is favoured until the feature stabilizes, no collapse-when-equal. Revisited once the feature stabilized (B2/B3.4 shipped): the rail now shows the shared commit once instead of three times —clientcollapses to a muted≡when it matches, andkaval’s duplicate commit + closure-hash move into its panel — keeping the three columns but cutting the echo. See chrome-bar-declutter. - Labels —
srv/pty. - Identity values — nix is first-class, no fallback: both values are nix-injected on the
koluBinwrapper; no dev-derivation, no placeholder dance, no|| ""softening. - Closure scope — no severing, no excluded deps. The staleKey hashes the pty-host package’s own source closure — naturally tight (provider churn elsewhere can’t over-prompt) and complete for the package’s own wire+behaviour. The build-time test guards the one real regression: a wire/behaviour dependency landing outside the hashed package (the #1034 mis-scope) — it walks
index.ts’s transitive imports and fails on any reached module that’s neither in-package nor on a small allowlist of stable framework/leaf deps.
This design revised the parent plan’s “daemon chip lands in B” stance: the identity + connection rail consolidates in A2 (honest, in-process), and B adds only the divergence semantics to the existing pty column.