# 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 <token>` header (via
  `authFetch` / `authHeaders` in `web/lib/auth.ts`).
- **SSE routes** — `?_token=<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/<name>/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<Response> {
  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__/`.
