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-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).
[?let [= $rt [$xap:run {tenant: "demo" components: (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
surfaces: ([$xap:surface "guestbook" [$xap:panel guestbook {}]])
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/
Guestbook — cx-xap demo (D3)
Guestbook
- Ada
- Lin
# 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//CSS — those loaded once).
$ curl -s -X POST http://127.0.0.1:8443/intent/sign -d 'name=Sam'
- Ada
- Lin
- Sam
# 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]]]]