D3 · Guestbook on the web
Attaches a browser and a live terminal client to the SAME runtime: the surface materializes as HTML while a TUI rides an SSE feed of the same fold. Same capability, same intents, new media. [$xap:serve] holds GET /events open and pushes a frame on every committed intent.
Run it
cx --allow-net serve.cx
Source — serve.cx
[?lib 'cx-platform/xap' as=xap]
# D3: reuse the D2 `guestbook` capability UNCHANGED. Only the client changes: a
# browser now attaches alongside the D2 CLI/TUI (medium-agnostic surface, §13.2).
[$xap:component guestbook
{bind: "/guestbook"
emits: ([do :sign [name :string]])
view: [?fn ($gs)
[panel
[list [?for [in $g $gs] [yield [item $g/name]]]]
[control :sign [label "Sign"] [input :name]]]]
working-panel: :none}]
# serve bootstraps the surface onto the core [?http-service] / [$http:serve]
# engine (xap.md §9) -- XAP opens no socket of its own. The web client is
# content-negotiated to text/html (htmx; controls bound inline via the `html`
# module); its §16 feed is SSE (bridge-supplied until the http streaming
# amendment, §24). The D2 CLI/TUI clients keep their in-process bus feed -- same
# runtime, same surface, three media. The document shell (shell/) is the
# swappable bridge layer, served as static [resource]s -- NOT part of the surface
# (§13.2) and NOT produced by xap. The cascade auto-appends [do :sign] events;
# "/guestbook" is their fold (§2.1/§14).
# One runtime, seeded with the D2 signatures, then SERVED -- the browser
# attaches to the same fold the D2 CLI/TUI clients ride (§16 shared state).
# Components AND surfaces are registered on the RUNTIME -- `serve` takes the
# five keys {runtime, tenant, shell, auth, block} and nothing else (xap.md §3,
# RULED: 1221-b-1a). It binds a socket to a runtime; it does not build one.
[?let [= $rt [$xap:run {tenant: "demo"
components: (guestbook)
surfaces: ([$xap:surface "guestbook" [$xap:panel guestbook {}]])}]]
[?let [= $a [$xap:emit $rt [do :sign [name "Ada"]]]]
[?let [= $b [$xap:emit $rt [do :sign [name "Lin"]]]]
[$xap:serve "http://127.0.0.1:8443"
{runtime: $rt
shell: "spec/03-approved/xap/demos/d3-guestbook-web/shell"}]]]]
Expected output
# D3 expected HTTP behavior — the web client over the bridge shell.
# serve is long-running; "output" is the request/response behavior, not a
# printed value. Run it (from the repo root) and curl it:
#
# cx --allow-net spec/xap/demos/d3-guestbook-web/serve.cx &
#
# The runtime is seeded with the D2 signatures (Ada, Lin), then served.
# 1. Initial GET — the document shell (shell/layout.html) with the guestbook
# surface spliced into its #guestbook-panel mount, materialized as
# text/html (controls bound inline as htmx attributes).
$ curl -s http://127.0.0.1:8443/
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Guestbook — cx-xap demo (D3)</title>
<!-- the ONLY place CSS is referenced; loaded once, reapplied to every swapped
fragment. Served as a static [resource] by the http layer, not by xap. -->
<link rel="stylesheet" href="/static/app.css">
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<h1>Guestbook</h1>
<!-- the cx-xap `guestbook` surface materializes here as a text/html fragment;
htmx swaps #guestbook-panel out-of-band on each committed [do :sign].
The <main> below is the bridge's server-side mount point — the bridge
splices xap:render(text/html) output into it. -->
<main id="guestbook-panel">
<ul class="guestbook"><li>Ada</li><li>Lin</li></ul>
<form hx-post="/intent/sign" hx-target="#guestbook-panel" hx-swap="outerHTML">
<input name="name"><button type="submit">Sign</button>
</form>
</main>
</body>
</html>
# 2. The stylesheet is served as a static asset beside the cascade (not by xap).
$ curl -s http://127.0.0.1:8443/static/app.css
/* D3 styling — the .class hooks ride on the view-tree (the html module keeps
class/id/style on its allowlist). The stylesheet is a static asset served
beside the cascade, never by xap. */
body { font: 16px/1.5 system-ui, sans-serif; max-width: 32rem; margin: 2rem auto; }
h1 { font-size: 1.25rem; }
.guestbook { list-style: none; padding: 0; }
.guestbook li { padding: .25rem 0; border-bottom: 1px solid #eee; }
form { display: flex; gap: .5rem; margin-top: 1rem; }
input { flex: 1; padding: .4rem; }
button { padding: .4rem .8rem; }
# 3. Sign via the web control (htmx POST) — the cascade commits [do :sign],
# and the response is the RE-RENDERED #guestbook-panel FRAGMENT ONLY
# (htmx swaps it; no doctype/<head>/CSS — those loaded once).
$ curl -s -X POST http://127.0.0.1:8443/intent/sign -d 'name=Sam'
<main id="guestbook-panel">
<ul class="guestbook"><li>Ada</li><li>Lin</li><li>Sam</li></ul>
<form hx-post="/intent/sign" hx-target="#guestbook-panel" hx-swap="outerHTML">
<input name="name"><button type="submit">Sign</button>
</form>
</main>
# The signature persists in the shared fold: a CLI/TUI client (D2) attached
# to the same runtime sees "Sam" appear too — one journal, three media.
# 4. The live SSE feed (§24) — GET /events holds the connection open and PUSHES
# a surface frame (the application/cx view-tree as `data:`) on connect and on
# every committed intent. A remote TUI/agent rides it with [$http:sse-events]
# instead of polling. (Hold it open with curl -N and sign in another terminal:
# a new frame arrives the moment the server commits it.)
$ curl -sN http://127.0.0.1:8443/events # then, elsewhere: POST /intent/sign name=Sam
data: [surface name=guestbook [panel [list ([item 'Ada'], [item 'Lin'])] [control :sign [label 'Sign'] [input :name]]]]
data: [surface name=guestbook [panel [list ([item 'Ada'], [item 'Lin'], [item 'Sam'])] [control :sign [label 'Sign'] [input :name]]]]