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]]]]