singularity-forge/web/README.md

117 lines
3.9 KiB
Markdown
Raw Normal View History

# 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__/`.