Serve a responsive HTML/CSS/JavaScript interface and connect its session browser to typed FastAPI resource routes.
The first visible full-stack feature is session browsing. A user opens /, creates a session, selects it, and refreshes into the same state. This chapter serves committed browser assets from FastAPI, models sessions as REST resources, and treats the DOM as a projection of API data. WebSocket streaming arrives in Chapter 03 after this request/response path is stable.
Start with a complete request/response path
The browser loads index.html, app.css, and the JavaScript module from the same FastAPI origin as the API. Same-origin serving removes cross-origin configuration from the learning path and makes one process sufficient for local development. The initial JavaScript boot performs two reads: /api/config describes the selected agent mode, and /api/sessions returns session summaries.
Creating a session is resource-shaped work, so it uses POST /api/sessions. Loading one uses GET /api/sessions/{session_id}. These routes call AutocodeApplication; they do not issue SQL directly.
from tempfile import TemporaryDirectoryfrom fastapi.testclient import TestClientfrom autocode.runner import DemoAgentRunnerfrom autocode_service.api import create_appwith TemporaryDirectory() as directory: app = create_app( database_path=f"{directory}/sessions.db", runner=DemoAgentRunner(), agent_mode="demo", )with TestClient(app) as client: page = client.get("/") created = client.post("/api/sessions", json={"title": "Learn the stack"}) session_id = created.json()["session_id"] listed = client.get("/api/sessions").json() loaded = client.get(f"/api/sessions/{session_id}").json()assert page.status_code ==200assert created.status_code ==201assert listed[0]["session_id"] == session_idassert loaded["title"] =="Learn the stack"print("browser route:", page.status_code, "session:", session_id)
The test crosses the HTTP adapter and SQLite repository rather than calling route functions directly. Status 201 distinguishes creation from an ordinary read, while the returned identifier becomes the link shared by later REST and WebSocket requests. A refresh can issue the same GET and reconstruct the selected session without relying on JavaScript memory.
Let each transport match the interaction shape
REST works well when one request produces one bounded response. Session create, list, load, and event replay have that shape. An agent run does not: one submitted task produces a sequence of text deltas, tool states, and a terminal event over time. Chapter 03 uses a WebSocket for that long-lived exchange instead of forcing every browser interaction through one fashionable transport.
FastAPI generates an OpenAPI document for the REST surface. That schema is useful for inspecting paths and status codes, but it does not describe the browser’s WebSocket protocol; the course defines that command and event vocabulary separately.
from tempfile import TemporaryDirectoryfrom autocode_service.api import create_appwith TemporaryDirectory() as directory: schema = create_app(database_path=f"{directory}/sessions.db").openapi()session_paths = { path: sorted(method for method in operations if method in {"get", "post"})for path, operations in schema["paths"].items()if path.startswith("/api/sessions")}assert session_paths["/api/sessions"] == ["get", "post"]assert session_paths["/api/sessions/{session_id}"] == ["get"]print("REST resource surface:", session_paths)
The resource names stay stable across adapters: session_id, title, version, updated_at, and the ordered events list come from the domain record. The list route intentionally returns summaries, while the detail route returns event history. This avoids loading every transcript to draw the sidebar.
Build the browser as an accessible projection
The committed frontend uses semantic landmarks, labels the composer, announces timeline changes through aria-live, and preserves keyboard-native buttons and form submission. JavaScript creates dynamic elements and assigns untrusted content through textContent. It never interpolates an agent response into innerHTML, which would turn model or repository text into executable markup.
The UI state is deliberately small: known sessions, selected session id, events keyed by event id, last cursor, socket, and streaming state. API data remains authoritative; refreshing discards in-memory state and rebuilds the projection.
These checks are source-level guards, not a substitute for rendering the page. The chapter’s browser walkthrough verifies focus, responsive layout, empty state, session creation, and refresh behavior. Automated API tests protect the data contract; Chapter 12 adds the complete browser-to-database release check.
Exercises
Extend the session sidebar with a visible loading, empty, success, and failure state. Keep the API response as the source of truth, use DOM-safe text projection, and state what a refresh should do in every state.
[P02.1] Make a UI bug replayable
Give a sequence of reducer actions that reproduces a phantom spinner after a disconnect, then state the smallest transition that fixes it.