pulam-web — the browser twin (kolu's surface-consumption leg, proven standalone)
A browser ↔ Node ↔ ssh app (drishti's twin for the terminal-workspace surface) that proves kolu's exact browser-consumption leg ahead of remote-terminals R9 — a live git-status view reaching a browser over websocketLink + surfaceClient + Solid reconcile, sourced from a mirrored pulam, auto-provisioned over ssh. Node + Vite, matching kolu's own client stack (the surface leg is identical @kolu/surface code, so it is a faithful reproduction of kolu's leg). Lands cheapest-first — R-pulamweb-1 (shipped) graduated the reactive stream consumer in drishti; R-pulamweb-2 stands up the whole framework (provision · fan-out · mirror · re-serve) rendering only a terminal list; R-pulamweb-3 layers the agent dashboard (every agent sorted by what needs you, with a live activity dot); R-pulamweb-4 adds the live git status (dirty/clean cell + drill-in).
pulam-web is drishti’s twin, pointed at the terminal-workspace surface. Where drishti monitors per-host processes in a browser, pulam-web is an agent dashboard — a bird’s-eye view of every agent across every host, sorted by what needs you (needs-you · working · idle), auto-provisioned over ssh. It’s the pulam-tui fleet board, in a browser. And on top of being a real product, it earns its engineering keep: it is kolu’s own browser-consumption leg, minus kolu — websocketLink → surfaceClient → Solid reconcile — so it de-risks the remote-consumption leg remote-terminals R9 leans on, in a build whose console you can actually see.
User-facing description
Leave it open on a second monitor: every agent across every host, sorted by what needs you. A blocked agent (awaiting_user) floats to the top and a warm strip breathes, so a glance tells you someone’s waiting before you’ve read a word; working agents spin cyan, idle ones sit dim. Each row carries a green activity dot when it’s moving bytes right now (like the Dock), its repo · branch, a compact dirty/clean mark, and how long it’s been in that state. You don’t care about terminals not running an agent, or sleeping ones — so they’re hidden by default, with one-click toggles to fold them back in. Hosts are added on demand and auto-provisioned over ssh (the provisioning… row is the live state), so a teammate opens one URL and watches the whole fleet. The per-agent git drill-in — a live changed-file tree — is the one heavier piece; it’s split out as R-pulamweb-4.
Architecture-level changes
It is the same @kolu/surface code kolu and drishti run — that is the whole point. Serve side: implementSurface → oRPC RPCHandler (@orpc/server/ws) → handler.upgrade(ws) on a /rpc/ws socket. Consume side: websocketLink → surfaceClient/surfaceClients → the reactive .use() hooks (surfaceClient.ts owns snapshot-then-deltas + reconcile). drishti proves this trio is electricity; kolu’s Code tab proves .streams.use() for a value-bearing gitStatus stream. pulam-web is the first standalone app to put those together — a live surface consumed over surfaceClient sourced from a mirror, the exact remote leg R9 needs. R-pulamweb-3 proves it with the value-bearing activity stream (the live dot); the git status leg — git.getStatus re-queried on the subscribeRepoChange pulse, the same shape R9’s Code tab adopts — is R-pulamweb-4’s, where it ships with the drill-in.
Node, matching kolu’s own stack. The surface leg is identical @kolu/surface code on any runtime — drishti even writes its server Node-style (@hono/node-server + the ws package) and runs it on Bun only for its Bun.build client bundler. We go Node, like kolu-server, and bundle the SolidJS frontend with Vite + vite-plugin-solid (kolu’s own client toolchain) rather than drishti’s Bun.build — so pulam-web is kolu’s stack end-to-end.
Implementation details
Three steps, each isolating one risk. R-pulamweb-1 (in drishti) graduates the reactive stream consumer — kolu’s failing leg — on its own. R-pulamweb-2 stands up the entire framework (provision · dial · fan-out · mirror · re-serve · browser-consume) rendering only a terminal list — no features, so a plumbing failure has nowhere to hide. R-pulamweb-3 is the agent dashboard — a render/sort/filter layer over the awareness collection plus the value-bearing activity stream (R-pulamweb-1’s proven .streams.use() consumer); R-pulamweb-4 adds the live git status (the dirty/clean cell) and the file-tree drill-in. R-pulamweb-1 and R-pulamweb-2 are independent (one’s a drishti change, the other a fresh app); R-pulamweb-3 needs both; R-pulamweb-4 needs R-pulamweb-3 and de-risks R9’s remote git leg.
R-pulamweb-1 — the reactive stream consumer, in drishti ✅ shipped
Shipped (drishti #72). Before pulam-web exists, prove its riskiest piece — a reactive stream consumer that survives delta accumulation — where the browser ⇄ ssh ⇄ mirror stack already runs: drishti. drishti’s processesSnapshot (packages/common/src/surface.ts) is mirrored per host and was consumed imperatively — unenrolledStreamCall(app.rpc.surface.processesSnapshot.get) + for await + a teardown controller. This graduates that one call site to a declarative reactive subscription. The exercise surfaced the trap that is R9’s lesson:
.streams.use() is for value-bearing streams; processesSnapshot is delta-accumulate. The first cut switched the call site to app.streams.processesSnapshot.use() and copied the latest frame into a reconcile store from a coarse createEffect. That silently drops same-shape delta frames: .streams.use() writes each frame into a reconcile store, and a coarse reader (const msg = sub()) only re-fires for the paths it actually reads — so two consecutive deltas differing only in a nested field (a hot PID’s cpuPct ticking under an unchanged key set) coalesce and the second is lost; live values freeze in steady state. processesSnapshot is snapshot-then-delta — each frame is a change, not the full state — so the consumer must accumulate.
The fix — and the rule it pins. Drop one level to createSubscription + reduce: the reducer folds every frame in the subscription’s own for await loop (no frame can be coalesced away), and the table renders fine-grained off the accumulated value (processes()[pid].cpuPct), so an in-place reconcile leaf update re-notifies exactly that cell. The hermetic test drives a same-shape delta (cpu 10 → 20 → 30) and asserts a fine-grained reader observes 30 — the exact case the coarse copy dropped. Because drishti’s CI is typecheck + nix only (no test lane), the review gauntlet, not CI, is what caught this — which is precisely why graduating it in drishti before the kolu work earns its keep. A drishti PR — independent of R-pulamweb-2’s framework, the thing R-pulamweb-3’s value-bearing activity consumer (and R-pulamweb-4’s git-status consumer) rides on.
R-pulamweb-2 — the framework (terminal list only) ✅ #1524
R-pulamweb-2 stands up everything hard — auto-provision + dial N pulam over ssh, mirror each, fan out, re-serve to the browser over /rpc/ws, and consume in the browser — but renders only a plain list of terminals (grouped by host). No git status, no drill-in, no features. The point is to prove the plumbing with a payload so small a failure has nowhere to hide.
- Backend (Node) — per host,
buildEntry(host):getHostSession({ binary:"pulam", resolveDrvPath, … })(the pooled, reconnecting session —getHostSession+makeClientCursor, not one-shotdialAgentOnce, so a transient ssh drop self-heals), a per-host re-serve of the wholeterminalWorkspaceSurfaceviaimplementSurface(implementSurfacefail-fast-throws on any unimplemented primitive, so the re-serve foldsversion+awarenessfrom the mirror and forwards the rest —fs.*/git.*procedures via the live-procedure holder, theactivity/repo/file streams via the live client — never a degraded stub), andpumpRemoteSurface(session, makeSink)(the shared reconnect-mirror loop) folding the agent’s frames in. The N entries live inbuildHostRegistry({ buildEntry }); oneRPCHandler.upgrade(ws)per/rpc/ws?host=<id>, dispatched by an app-local upgrade block (gateWsOrigin+gateStaleSocket+startWsHeartbeat, all already shared).pulam-tui’shostConnect.tsis the dial template. - Browser (SolidJS + Vite) — per host a
surfaceClient(pulamSurface, websocketLink(ws?host=<id>))— the browser consumes the mirrored surface (pulamSurface = mirroredSurface(terminalWorkspaceSurface)), the one carrying the composedconnectioncell, not the connection-free base — →app.collections.awareness.use({})→ render a list of terminals per host (cwd· foreground · agent), plus a coarseconnecting…overlay until the first frame. This is the.collections.use()consumer (which drishti already proves); the value-bearing stream consumer (.streams.use()) is deferred to R-pulamweb-3. (For R-pulamweb-2 the host roster is a static/api/hostsboot list; dynamic add still rides a small parent fleet surface in a follow-up. The per-host connection health, however, now ships: pulam-web’s browser surface —pulamSurface = mirroredSurface(terminalWorkspaceSurface)— carries a composed read-onlyconnectioncell (the baseterminalWorkspaceSurfacestays connection-free;mirroredSurfaceadds the cell only at the nix-host re-serve seam), the backend↔remote mirror’scopying → connecting → connected → disconnected → failed, so a dead mirror reads honestly instead of as an empty fleet. See pulam-web: a dead mirror lies as an empty fleet.)
Done when: the browser shows a live terminal list across N auto-provisioned pulam hosts — its terminals appear and come/go live (awareness deltas) — all over the real ws. The mirror → re-serve → browser-store path is proven deterministically by the hermetic test (below). No features yet. That alone proves the framework: provision · dial · reconnect · fan-out · ws-serve · browser collection-consume.
R-pulamweb-3 — the agent dashboard ✅ #1535
The dashboard you actually want — every agent across the fleet, sorted by what needs you — has nothing to build underneath it. The state you sort by (awaiting_user · working · idle) already lives in the awareness collection R-pulamweb-2 consumes (AgentInfoSchema, terminal-workspace/schema.ts:54; HostGroup.tsx:49 reads value.agent today), and pulam-tui already has the state-bucketing, needs-you-first sort, and colour map (render.ts:79-201, renderer-agnostic). So R-pulamweb-3 is a render / sort / filter layer — no surface change, no @pierre/trees, nothing gated:
- Sort by what needs you — port pulam-tui’s bucket + sort (
agentBucket/agentUrgency/the comparator,render.ts:77-92,324-335,452-465): a blocked agent (awaiting_user) floats to the top with a breathing alert strip; working spins cyan; idle sits dim. Per row: the agent, itsrepo · branch, the state, and how long it’s been there. The dirty/clean mark in the picture is not here —awareness.gitcarries onlyrepoName/branch, so the file count needs thegit.getStatusprocedure; it ships with the rest of git status in R-pulamweb-4 (the reuse map already filesRepoWatchSetthere). The ported logic lives in a pulam-web-localfleet.ts(pinned to the TUI’s behaviour byfleet.test.ts) — a TUI/OpenTUI package has no place in the Vite browser bundle, so it’s owned, not imported. - Live activity dot — a green dot left of the agent state when the terminal is moving bytes right now (like the Dock’s row dot). That’s the
activitystream (ActivitySet— the set of producing terminals,surface.ts:127), which R-pulamweb-2 already re-serves. It is value-bearing (each frame is the full current live set), so it consumes through.streams.use()— the replace-each-frame consumer — not R-pulamweb-1’screateSubscription+ reduce (that path is for the delta-accumulateprocessesSnapshot; conflating the two was a premise this note carried). So the dashboard reads two surface members — theawarenesscollection (state) + theactivitystream (the dot) — both already-proven consumers, still nothing gated. - Agents, not terminals — show every agent by default (active and idle, the full agent board); one-click toggles fold in non-agent terminals and sleeping ones. (Shipped initially with only active agents on by default; the idle-by-default widening came as a later follow-up — see History.)
Done when the browser shows the live agent dashboard across the fleet — blocked agents floated and breathing, states colour-coded, updating live over the real ws (video), with the toggles working. No file tree yet — that’s R-pulamweb-4.
R-pulamweb-4 — the live git drill-in (file tree)
The git-status phase, in two parts: the per-agent dirty/clean cell (the ✎5 / ✓ mark + ahead/behind), and — clicking an agent — a drill-in to its live changed-file tree (the file tree like kolu’s Code tab, not just git status text). Both consume the same git-status data, and it is the same git procedure + pulse R9’s Code tab reuses (which is why pulam-web de-risks it). It is a plain render/consume feature — the Pierre renderer everyone feared is not a blocker (risk note below).
Build it end-to-end. The load-bearing decision a first attempt got wrong (feat/pulamweb-git-drillin, discarded) is where the new streams live — pin it:
- Consume the surface’s existing git procedure + pulse — add nothing.
terminalWorkspaceSurfacealready serves git status the shared-surface way (R6):git.getStatus/fs.listAllare procedures (request→response) andsubscribeRepoChangeis a payload-free{seq}pulse. The drill-in calls the procedure for a snapshot, then re-queries on each pulse (surface live data explains the pattern). No new stream, nopollOnEvent, no surface change — pulam-web readsterminalWorkspaceSurfaceexactly as it is, which is the whole point. - Browser: the dirty/clean cell. Consume
app.streams.gitStatus.use(...)fine-grained (each frame the full status), reusingpulam-tui’sgitCellprojection — ported into pulam-web’s ownfleet.tsand pinned byfleet.test.ts(the reuse-map row below; do not import the TUI/OpenTUI package into the Vite bundle). Render✎<n>/✓+ ahead/behind in the agent row, replacing theNO git dirty/clean countplaceholder atpackages/pulam-web/src/client/HostGroup.tsx:27. - Browser: the drill-in file tree. On clicking an agent, render the changed-file tree through
@kolu/solid-pierre’s<FileTree>— kolu’s Code-tab wrapper (packages/client/src/right-panel/CodeTab.tsx,packages/solid-pierre/src/FileTree.tsx):app.streams.fsListAll.use(...)for the paths +app.streams.gitStatus.use(...)for decorations (thegitStatusprop →setGitStatus). Reuse the porcelain→Pierre mapping, don’t copy it — liftpackages/client/src/ui/gitStatusEntries.tsinto@kolu/solid-pierreso kolu and pulam-web import the one copy (the discarded attempt duplicated it into pulam-web). Add@kolu/solid-pierre+@pierre/treestopackages/pulam-web/package.json— pulam-web does not depend on Pierre today. Keep the tree mounted for flicker-free updates.
Surface coverage this phase adds. Before R-pulamweb-4 the browser consumed cells + the awareness collection + the activity value-bearing stream. This phase adds the procedure + pulse pattern — git.getStatus/fs.listAll called over the mirror, re-queried on each subscribeRepoChange pulse — over kolu’s exact surface, not a pulam-web variant, proving the remote/mirror leg. It’s the same git shape kolu’s Code tab adopts in R9 (the fs/git half), so R-pulamweb-4 de-risks it. (The file-content/diff viewer — git.getDiff / fs.readFile via @pierre/diffs — stays out: same pattern, rides R9.)
Done when the dirty/clean cell and the drill-in’s file tree update within ~1s of a working-tree change over the real ws (video), and a hermetic test fires a subscribeRepoChange pulse and asserts the drill-in re-queries git.getStatus/fs.listAll and repaints, over the agent → mirror → re-serve → browser leg. No Pierre patch is required (see the risk note). This is the same procedure + pulse git leg kolu’s Code tab adopts in R9.
R-pulamweb-5 — fleet notifications (the alertClass mirror)
The last Dock-mirror gap. kolu’s Dock fires an OS notification (+ PWA badge) when an agent crosses into the notify class — finished (waiting) or blocked (awaiting_user) — for a terminal you aren’t watching; the membership is the shared alertClass fold (@kolu/terminal-workspace/agentProjection). The fleet board is the exact surface that wants this — you “leave it open on a second monitor”, so a ping when something needs you is the point — and pulam-web’s server already serves the notify service worker (installFreshStatic({ serviceWorker: "notify" })), so the transport is in place. What’s missing is the client firing leg, which today lives kolu-local (useActivityAlerts / useTerminalAlerts — permission request, the fire-on-crossing effect, the SW click-routing).
So this phase is not a fold-consume — it’s a feature: (1) extract the renderer-agnostic firing channels (request-permission · fire-notification · SW message routing) out of the kolu client into a shared home — @kolu/surface-app already owns NOTIFICATION_SW_SOURCE, the natural electricity boundary — so kolu and pulam-web fire through one path, not two; (2) wire pulam-web: a per-host effect over the awareness collection that fires on an alertClass crossing for a background host, gated on a permission opt-in (the dashboard’s own toggle). The paint + rank mirror shipped with R-dock-unify (#1541); this is the third fold catching up. Done when a fleet agent finishing/blocking on an unfocused tab fires one OS notification whose click focuses pulam-web (video), and kolu + pulam-web fire through the same extracted channel (no duplicated firing logic).
Reuse map — how the framework (R-pulamweb-1/2) was built (grounded against installed code + /home/srid/code/drishti). Shared = consumed from the surface family, not copied; app-local = pulam-web’s own. The rows R-pulamweb-3/4 add are the last (agent-state cells, then the drill-in); everything else here already shipped.
| Concern | Source | Note |
|---|---|---|
| per-host mirror loop | shared @kolu/surface-nix-host pumpRemoteSurface |
extracted from drishti bridgeAgentToParent; makeClientCursor reconnect + mirrorRemoteSurface per spawn + live-procedure/live-client holders |
| fan-out registry | shared @kolu/surface-nix-host buildHostRegistry |
extracted from drishti hostRegistry.ts; Map<host,{session,handler}>, generic over the handler type |
| ws-serve (~18 lines) | app-local in pulam-web |
composed from shared gateWsOrigin (@kolu/surface/ws-origin) + gateStaleSocket + startWsHeartbeat (@kolu/surface-app/server); ?host= dispatch is the app’s (see the common-it-up callout above) |
| whole-surface re-serve | app-local implementSurface(terminalWorkspaceSurface, …) |
folds version+awareness from the mirror, forwards fs.*/git.* + the watcher streams (fail-fast: every primitive implemented) |
| autoprovision + dial | shared @kolu/surface-nix-host getHostSession (+ resolveSystem) |
dial template = pulam-tui/src/hostConnect.ts; pins binary:"pulam", PULAM_AGENT_DRVS_JSON. Provisioning lives inside the session’s reconnect cycle — no hand-rolled provisionAgent call |
| browser bootstrap | drishti app/src/client/wire.ts (pattern) |
one surfaceClient(pulamSurface, websocketLink(ws?host=)) per host — the browser reads the mirrored surface (pulamSurface = mirroredSurface(terminalWorkspaceSurface)), which carries the connection cell; the base is connection-free (@kolu/surface/solid + @kolu/surface/links/websocket) |
| serve the bundle | shared installFreshStatic (@kolu/surface-app/server) |
the same fresh-static contract kolu-server uses |
render.ts (state buckets · agentUrgency · sort · relativeTime) |
pulam-tui — R-pulamweb-3 ✅ |
ported to pulam-web’s own fleet.ts (pinned by fleet.test.ts), not imported — no TUI/OpenTUI dep in the Vite bundle; the agent rows + needs-you-first sort |
render.ts / fleet.ts (gitCell, gitDetail, RepoWatchSet) |
pulam-tui — R-pulamweb-4 |
the dirty/clean cell + the live file-tree drill-in (all the git-status consumption) |
| bundler + nix | Vite + vite-plugin-solid (kolu’s packages/client toolchain), Node |
in-repo package: @kolu/* are workspace deps (no npins); nix derivation + flake/justfile wiring modeled on packages/client |
The hermetic test. No in-memory websocketLink double exists (only directLink/stdio/unix-socket). Model on R7’s mirrorRemoteSurface.test.ts: stand up a real terminalWorkspaceSurface agent via implementSurface, connect it with directLink, drive pulam-web’s buildReServe fold path (the mirror sink) with that client, then consume the re-served surface through a second surfaceClient + a Solid reconcile store — asserting the re-served value re-notifies on the second delta (agent → mirror → re-serve → browser-store). buildReServe is split so the mirror step takes an injected client, which is exactly what makes it driveable without ssh. Per R-pulamweb-1’s correction, R-pulamweb-3’s value-bearing activity consumer reads fine-grained and its test drives a same-shape second frame — two same-cardinality membership swaps ([A]→[B]→[A]) that each must re-notify a liveSet().has(id) dot reader (the coalescing regression a coarse copy-into-store silently drops); R-pulamweb-2’s awareness consumer asserts the simpler key re-notify. (R-pulamweb-4’s git-status consumer is the procedure + pulse leg — re-query on the pulse; activity is R-pulamweb-3’s value-bearing leg.) That catches a coalescing/reconcile break deterministically; a ws-flush break only shows over a real socket — so pulam-web over real ws is the full proof, the unit test necessary-but-not-sufficient.
Definition of done — by phase. R-pulamweb-1 (drishti): ✅ shipped (drishti #72) — processesSnapshot rendered live via createSubscription + reduce, read fine-grained, guarded by a same-shape-delta regression test. R-pulamweb-2: a live terminal list across N auto-provisioned hosts over the real ws — the framework, no features. R-pulamweb-3: ✅ shipped (#1535) — the live agent dashboard across the fleet (blocked agents floated + breathing, states colour-coded, the activity dot live, the toggles working, over the real ws); the value-bearing activity consumer guarded by a same-shape-swap re-notify test. No git cells, no file tree, nothing gated. R-pulamweb-4: the dirty/clean cell + the drill-in’s file tree update live over the real ws, the git-status procedure + pulse pinned by a re-query-on-pulse test — no Pierre patch needed (the swallow-emit is measured harmless in kolu, #1534); the same git shape R9’s Code tab adopts.
History
- Pierre renderer de-gated — reproduced harmless in kolu (2026-06-26) — the long-standing “the file-tree renderer is R-pulamweb-4’s hard part — carry the
@pierre/treespatch” claim was reproduced and falsified: in kolu’s real@kolu/solid-pierre→@pierre/treespath the live git-status repaint rides Pierre’s guard-independentsetGitStatusprop and kolu’sFileTreenever re-subscribes its controller, so the swallow-emit (pierre#883) does not manifest. R-pulamweb-4 is now a plain render/consume feature; the one-line fix stays vendored-ready onorigin/r8as optional insurance. Reproduction + measurement filed on #1534. - Idle agents show by default (2026-06-24) — the dashboard opened showing only active agents (need/work), with idle ones behind the opt-in toggle, so a fleet whose agents had all gone quiet read as an empty board. The default now shows every agent — active and idle (one
DEFAULT_FLEET_FILTERS, pinned byfleet.test.ts) — leaving only the agentless categories (non-agent terminals, sleeping shells) opt-in. A small UX default flip; rode the R-activity-merge PR (#1555). - R-dock-unify filled in the paint mirror; R-pulamweb-5 born (2026-06-23) — #1541 made kolu’s Dock the third consumer of the shared
agentProjectionand, on the way, brought the two fleet MIRRORS up to consume theagentPaintClassfold too: pulam-web’s agent glyph (fleet.ts’sPAINT/paintClassFor) and pulam-tui’s state tone (render.ts’sagentTone) now follow PAINT, decoupled from the urgency sort — so a just-finishedwaitingagent keeps the lingering “awaiting” amber instead of dropping to idle grey, mirroring the Dock’s new order≠colour split. The remaining shared fold,alertClass(fire-a-notification membership), is the one mirror gap left — pulam-web fleet notifications are now R-pulamweb-5. A.apm/instructionsrule (dock-fleet-mirror) pins the three-surface contract so the next agent-state change keeps Dock + pulam-tui + pulam-web in lockstep. - R-pulamweb-3 shipped (2026-06-23) — #1535. The agent dashboard: a render / sort / filter layer over R-pulamweb-2’s framework, no surface change. pulam-tui’s bucket/urgency/sort/recency projection extracted into the shared
@kolu/terminal-workspace/agentProjection(a presentation-neutral leaf — no TUI/OpenTUI in the Vite bundle) and imported by bothpulam-tuiand pulam-web’sfleet.ts(the web-tone wrapper, pinned byfleet.test.ts); kolu’s Dock joins as the third consumer in R-dock-unify. The value-bearingactivitystream consumed via.streams.use()for the green dot, App-owned filters + a fleet-wide breathing “needs you” strip. Grounding against the installed code corrected two premises this note carried: (1) the dirty/clean count is not inawareness.git(onlyrepoName/branch) — it needsgit.getStatus, so it moved to R-pulamweb-4 with the rest of git-status consumption (where the reuse map already filedRepoWatchSet); (2)activityis value-bearing (full set each frame), so the simple.streams.use()consumer, not R-pulamweb-1’screateSubscription+reduce (that’s for delta-accumulateprocessesSnapshot). The hermetic proof is the value-bearing analog of R-pulamweb-1’s: two same-cardinality activity swaps each re-notify a fine-grained dot reader over the full agent→mirror→re-serve→browser leg. - R-pulamweb-2 shipped (2026-06-22) — #1524. The framework: the shared per-host fan-out (
pumpRemoteSurface+buildHostRegistryin@kolu/surface-nix-host, 11 registry tests) + the new@kolu/pulam-webpackage (whole-surface re-serve, app-local ws-upgrade from the shared gates, Solid+Vite awareness client, the hermetic agent→mirror→re-serve→browser-store proof). Deep-grounded against the installed code (drishti’sbridgeAgentToParent/hostRegistry, the@kolu/surface*family,pulam-tui’s dial, theterminalWorkspaceSurfaceshape, kolu’s Vite/nix client toolchain), which corrected the original sketch on two load-bearing points: (1) the common-it-up extraction is the per-host mirror-bridge + registry, not a ws-serve helper (surface core has nowsdep; drishti’s upgrade handler doesn’t fit a generic seam, so ws-serve stays app-local); (2) the per-host re-serve is the wholeterminalWorkspaceSurface(fail-fast on any omission), dialed via the reconnectinggetHostSession+makeClientCursor. drishti adopts the shared helpers in a linked gated PR. Packaged as a first-class Nix derivation —nix run .#pulam-web(a Vite client build + a tsx server wrapper baking the per-system pulam drv map,default.nix), with ajust pulam-webdev recipe — and proven end-to-end against a real two-host fleet (a Linux pu box + a macOS host,sincereintent) over the live ws. - R-pulamweb-1 shipped (2026-06-22) — graduated drishti’s
processesSnapshotconsumer (drishti #72). The review gauntlet caught a coalescing regression that corrected this note’s premise:processesSnapshotis delta-accumulate, not “value-bearing”, so.streams.use()+ a coarse copy-into-store drops same-shape delta frames (a hot PID’s metric freezes). The fix —createSubscription+reduce, rendered fine-grained — and its same-shape-delta test are now pinned as the git-consumer recipe here and in remote-terminals. drishti CI is typecheck+nix only, so the gauntlet (not CI) caught it — the case for graduating in drishti first. - Planned (2026-06-22) — branched out of pulam’s R4.8 once it grew its own UI and a layered plan (R-pulamweb-1 in drishti · R-pulamweb-2 framework · R-pulamweb-3 features). Grounded against
/home/srid/code/drishti(the surface leg is identical@kolu/surfacecode, so the runtime is free — chose Node + Vite to match kolu-server/client; Bun was only drishti’s bundler; autoprovision isgetHostSession+provisionAgent) and againstpulam-tui(thestartFleet/FleetSink/RepoWatchSetcore is renderer-agnostic and reusable). Gates remote-terminals R9.