# Slice 11: web and TUI surfaces (spec) Status: **approved** (owner, 2026-10-30/11-01: P1-P9 in the decisions "Slice 11's points" and "Slice 11, P3" to "P9", then "go ahead, just do all the things"). Built after slices 12 and 13: the surfaces are generic plumbing that the app kit wires in (`startApp` serves the web surface; a TUI client entry). The app keeps everything visible: HTML shell, CSS, components, themes, navigation, layout, keys. Read in full before writing code: this spec; `decisions.md` (slice 11's entries, the plugin model, the app kit); `design.md` §3 (clients), §21, §23 (all, "Web and TUI" included); `slice-10.md`, `slice-12.md`, `slice-13.md`; p2's `src/hosts/web.ts`, `src/hosts/web/main.tsx`, `src/web/reconnect.ts`, `src/runtime/web.tsx`, `src/hosts/bundles.ts`, `src/hosts/tui.ts`, `src/hosts/tui-client.ts`, `src/hosts/tui-sandbox.ts`, `src/hosts/tui-bootstrap.mjs`, `src/hosts/tui-worker-runtime.ts`, `src/runtime/tui.ts` (`TuiScope`). ## 1. `chord/surfaces` (the web surface's server side) ```ts // inside startApp (slice 13), or by hand: const web = webSurface(server, { kind: "web", plugins, entry: "./src/web/main.tsx", // the app's web start code: the base bundle's entry page: (request, assets) => shell(request.nonce, assets), // the app's HTML; assets: base script, plugins file, styles }, ctx); // its facet (routes to Http, the WebScripts object) goes into the server's core facets ``` - **Base bundle** per client kind: built on the server by esbuild from `entry`, with the kind's host packages inlined once (the app's API modules for the kind, Chord, React), content-hashed (`/_chord/web/base..js`), cached forever (`Cache-Control: immutable`, overriding the baseline's `no-store` for these routes only); rebuilt when base code changes (the server restarts anyway: built at start). - **Plugin scripts:** each web facet served as a script that registers itself (`chordFacet("", "", function (require, module, exports) { … })`, P2), at `/_chord/web/plugin/..js`, immutable; the current set as one combined file `/_chord/web/plugins..js` (P3); stylesheets and assets of the build likewise, under `publicPath`. - **`WebScripts`** (a `remote` object the surface serves, params `{}`, any principal that may load the app): its state is the current set `{ plugins: [{ name, digest, url, css? }], base }`; a rollout that ships the web kind updates it (the plugins component's `scripts("web")` changes; the surface follows them) without waiting for pages (P5). - Routes and the page are `Http` contributions of the surface's facet; the page route is `page: true` (navigation from another site allowed) and public or not as the app says. ## 2. `chord/web` (browser side) ```ts // src/web/main.tsx (the app's entry, in the base bundle) startPage

({ connect: () => new WebSocket(`${location.origin.replace(/^http/, "ws")}/chord`), modules: { "@acme/app/v3": api, "@acme/app/v3/web": apiWeb, react: React, "react/jsx-runtime": jsx }, core: [{ name: "@acme/web", facet: webCore }], canRetry: async () => true, onConnection: (state) => setBanner(state), render: (host) => createRoot(document.getElementById("root")!).render(), }); ``` - A client host (no address); the connection loop (P6, p2's `keepConnected`: reconnect 250 ms to 5 s, wake on visibility, `online` and network change, replace a silent socket, stop on an outdated protocol (reload the page) or when `canRetry` says no); `onConnection` reports `connecting | connected | waiting`. - Loads the combined plugins file, evaluates each registered facet with `modules` (the `require` they get answers only host packages and `@earendil-works/chord/plugins`'s browser half if any), loads them with `core` in one `host.load`, then holds `WebScripts`: on a new set it fetches the changed scripts (`