# sf web Next.js 15 (App Router) frontend for `sf --web`. Ships as a standalone bundle baked into the sf release; can also be run from source for development. ## What this is The web UI is a browser workspace for sf. It connects to a bridge service (`src/web/bridge-service.ts`) that manages an sf subprocess per project CWD and proxies RPC commands over stdio. The page is a single-page app: no server-side rendering, client-only via `dynamic(..., { ssr: false })`. ## How to run **Packaged (normal use)** ```sh sf --web # launches Next.js standalone server and opens browser sf --web --port 3000 # pick a specific port ``` **Source dev mode** (requires the repo checked out) ```sh npm --prefix web run dev ``` The dev server needs these env vars (set automatically by `sf --web`; set manually for source dev): | Variable | Description | |---|---| | `SF_WEB_AUTH_TOKEN` | Bearer token for all API requests | | `SF_WEB_PROJECT_CWD` | Absolute path of the project being served | | `SF_WEB_HOST` | Host to bind (default `127.0.0.1`) | | `SF_WEB_PORT` | Port to bind | ## Auth On first page load the client reads the bearer token from the URL fragment (`#token=…`), stores it in `localStorage` under `sf-auth-token`, and strips the fragment from the URL. All subsequent requests attach it: - **Fetch / API routes** — `Authorization: Bearer ` header (via `authFetch` / `authHeaders` in `web/lib/auth.ts`). - **SSE routes** — `?_token=` query parameter (EventSource doesn't support custom headers). ## Architecture ``` Browser └─ page.tsx (dynamic, ssr:false) └─ SFAppShell ├─ WorkspaceChrome — layout chrome, sidebar, status bar │ └─ 7 views (see below) └─ CommandSurface — slash-command palette Next.js API routes (web/app/api/**/route.ts) └─ delegate to *-service.ts files in src/web/ └─ bridge-service.ts — per-CWD singleton sf subprocess (RPC over stdio) ``` `bridge-service.ts` spawns `sf` as a child process, speaks JSON-RPC over stdio, and multiplexes all API routes onto that single bridge. Auth is enforced before requests reach the bridge via `requireProjectCwd()` (which validates the token and resolves the CWD from `SF_WEB_PROJECT_CWD`). ## The 7 views | View key | Component | Purpose | |---|---|---| | `dashboard` | `Dashboard` | Live project status, metrics, quick-start panel | | `chat` | `ChatMode` | Conversational agent interface | | `power` | `DualTerminal` | Full-screen split terminal (agent + shell) | | `roadmap` | `Roadmap` | Milestone and slice plan explorer | | `files` | `FilesView` | Project file browser with syntax highlighting | | `activity` | `ActivityView` | Event log and session history | | `visualize` | `VisualizerView` | Dependency graph and architecture visualizer | ## Adding a new API route 1. Create `web/app/api//route.ts` that calls `requireProjectCwd(request)` for auth/CWD resolution, then delegates to a service: ```ts // web/app/api/my-feature/route.ts import { requireProjectCwd } from "../../../../src/web/bridge-service.ts"; import { collectMyFeatureData } from "../../../../src/web/my-feature-service.ts"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function GET(request: Request): Promise { const projectCwd = requireProjectCwd(request); const data = await collectMyFeatureData(projectCwd); return Response.json(data, { headers: { "Cache-Control": "no-store" } }); } ``` 2. Implement `src/web/my-feature-service.ts` with the actual logic (may call the bridge or read disk directly). ## Tests Tests for web utilities live in `web/lib/__tests__/` and run via Vitest: ```sh npx vitest run web/lib --config vitest.config.ts ``` > **Note:** co-located `*.test.ts` files inside `web/` outside of `__tests__/` > subdirectories are silently skipped by the root Vitest config. Always place > web tests under `web/lib/__tests__/`.