117 lines
3.9 KiB
Markdown
117 lines
3.9 KiB
Markdown
|
|
# 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__/`.
|