This directory holds the Next.js source for the embedded libra code browser UI. The build is consumed two ways:
pnpm devduring local development serves the UI on the Next.js dev server's defaulthttp://localhost:3000. All API calls use relative/api/...paths withsame-origincredentials (seesrc/lib/code-ui/client.ts), so the dev server must share its origin with a runninglibra codeprocess. The typical workflow is to launch the backend on a non-default port —libra code --web-only --port 4400— and then runpnpm dev -- --port 4400so both share the loopback origin. There is noLIBRA_DEV_API_BASE-style env-var-based proxy: the client speaks to/api/*directly and the Rust side'sensure_loopback_api_requestguard refuses remote callers regardless.pnpm buildemits a static export toweb/out/. The Rust binary embeds that directory at compile time viaWebAssets(src/command/web_assets.rs) and serves it fromaxum::Router::fallback. Any production change to the UI therefore requirespnpm buildso the embedded snapshot stays current; CI fails closed ifweb/out/falls behind the source.
pnpm install # install deps (uses pnpm-lock.yaml)
pnpm dev # local dev server with HMR
pnpm lint # eslint, no warnings allowed
pnpm build # static export → web/out/The browser only talks to its same-origin server. The Rust side enforces loopback at every /api/* route, so this client does not host-check. Non-loopback HTML navigation receives the embedded remote-notice/ static page instead of the SPA, and non-loopback asset/API fallbacks return 404. Source of truth: src/internal/ai/web/mod.rs.
| Endpoint | Verb | Purpose |
|---|---|---|
/api/health |
GET | Liveness probe — returns plain "ok". Cheapest sanity check that the embedded server is bound. |
/api/repo |
GET | Repository identity (id, name, description). |
/api/repo/status |
GET | Working-tree status — same JSON envelope as libra status --json ({ ok, command: "status", data }). |
/api/code/session |
GET | Initial CodeUiSessionSnapshot. |
/api/code/events |
GET (SSE) | session_updated / status_changed / controller_changed frames; server lag emits a full session_updated snapshot, and clients fall back to GET /api/code/session on disconnect. |
/api/code/threads?limit&offset |
GET | Active thread projections for the sidebar ({ items, nextOffset }). |
/api/code/diagnostics |
GET | Redacted runtime info (PID, ports, log file, controller). |
/api/code/controller/attach |
POST | Issue a lease ({ clientId, kind: "browser" }). Returns controllerToken. |
/api/code/controller/detach |
POST | Release the lease (header X-Code-Controller-Token). |
/api/code/messages |
POST | Submit a user message (header X-Code-Controller-Token, body ≤256 KiB). |
/api/code/interactions/{id} |
POST | Resolve a pending CodeUiInteractionRequest. |
/api/code/control/cancel |
POST | Cancel the active turn. Browser leases need only the controller token; automation leases additionally require X-Libra-Control-Token. |
The wire types are pinned in two places — keep them in lock-step:
- TypeScript:
web/src/lib/code-ui/types.ts. - Rust:
src/internal/ai/web/code_ui.rs(#[serde(rename_all = "camelCase")]on every struct,#[serde(rename_all = "snake_case")]on every enum). The serde golden tests intests/ai_code_ui_wire_test.rsfail loudly when the JSON shape drifts.
web/src/
├── app/ # Next.js app router entry
├── components/icons/ # Inline SVG icon set
├── components/workspace/ # Main three-pane layout
│ ├── chat/ # Chat pane + composer + InteractionPanel
│ ├── sidebar/ # Thread list + repo state
│ ├── terminal/ # Read-only event log derived from snapshot
│ └── workflow/ # Pipeline / Summary / Diff / Settings tabs
└── lib/
├── code-ui/ # Wire types, HTTP client, store, controller hook
├── persisted-state.ts # Splitter widths persisted to localStorage
└── storage.ts / utils.ts # Small UI helpers
web/src/lib/code-ui/store.tsx owns the CodeUiSessionSnapshot and the SSE reconnect loop. web/src/lib/code-ui/controller.tsx owns the browser controller lease. Both expose React context providers; workspace.tsx mounts them once at the top of the tree so every pane shares one client id, one lease, and one snapshot.
The composer / cancel / interaction-panel writers all flow through useBrowserController() (provided by <BrowserControllerProvider>). On the first write the hook calls POST /api/code/controller/attach, caches controllerToken + leaseExpiresAt in memory, and replays the original request. Reloading the page drops the lease so the next browser session can attach cleanly.
Recovery semantics in controller.tsx:
MISSING_CONTROLLER_TOKEN/INVALID_CONTROLLER_TOKEN— clear cache, retry once.CONTROLLER_CONFLICT— surface the current owner; do not loop on retry.BROWSER_CONTROL_DISABLED— show a hint pointing to the--browser-control loopbackCLI flag.PAYLOAD_TOO_LARGE— surfaced inline; the client also caps body at 256 KiB before posting.
beforeunload issues a best-effort fetch("/api/code/controller/detach", { keepalive: true }) so the next browser session can attach without bumping into a stale lease. navigator.sendBeacon cannot set custom headers and is therefore not used for the detach call.
Every writable control is gated on snapshot.capabilities.* plus snapshot.controller.canWrite. The current capability set is set by the Rust runtime: --web-only --provider codex advertises the full set, HeadlessCodeRuntime advertises messageInput + streamingText + toolCalls, and the read-only placeholder advertises none.
pnpm devdoes not embed assets into the Rust binary; you'll see "Loading…" placeholders for any feature that depends on a livelibra codeAPI. Run a TUI session in another terminal so the SSE channel has data to stream.pnpm buildandcargo buildare independent — when you modify both layers, runpnpm buildfirst soweb/out/is up to date before the Rust crate compiles.- The static export needs
output: "export",trailingSlash: true, andimages.unoptimized(configured innext.config.ts). Don't toggle these without updatingWebAssetsaccordingly.