§ 04 — Frontend

A pure SPA, by choice.

React 19 + Vite + TypeScript + Tailwind. No SSR, no server-rendering, no router library. The bet: keep the surface small, ship features fast, type the API contracts strictly. Static hosting works; deep links work; refresh works; file:// works.

Top-level structure

frontend/
├── src/
│   ├── pages/        — top-level routes (one component per route)
│   ├── components/   — shared UI: chat, approval cards, doc viewer, terminal, sync, admin
│   ├── hooks/        — data-fetching + integrations (one per concern)
│   ├── contexts/     — AuthContext only
│   └── lib/          — api.ts, wsClient.ts, router.ts, auth.ts, cn.ts
├── index.html
├── vite.config.ts
├── tailwind.config.js
└── package.json

Routing

frontend/src/lib/router.ts — ~123 lines

A custom hash router. There is no react-router. The hash is the source of truth for the current route; useRoute() parses it on every render. Helper functions like routeToProject(projectId, chatId?), routeToTerminal() mutate window.location.hash directly.

Hash routePage componentPurpose
#/ · #/chats/{chatId}ChatsPageWorkspace-level standalone chats
#/projectsProjectsPageProject list + create modal
#/projects/{id} · #/projects/{id}/chats/{cid}ProjectPageProject hub: chats, instructions, artifacts, sync
#/terminal/{sid}TerminalPagexterm.js shell session
#/settings (+ /<path…>)SettingsPageTenant / user settings, integrations, MCP
#/adminAdminPageTenant-admin console
#/scheduledScheduledPageScheduled-job CRUD + history
#/loginLoginPageOAuth or local login
#/auth-callbackhandled by AuthContextReceives ?token=… from OAuth redirect
#/access-deniedAccessDeniedPagePermission failures

Auth context

frontend/src/contexts/AuthContext.tsx · frontend/src/lib/auth.ts

Storage
localStorage["agenticit_token"]. Cleared on 401 with redirect to #/login.
AUTH_ENABLED
Vite env flag (VITE_AUTH_ENABLED). When false, the token is optional — useful for some local-dev paths.
Bootstrap
If the URL hash is #/auth-callback?token=…, extract token, store it, redirect to #/. Then call GET /api/auth/me to populate the user object.
login(provider)
Redirects to /api/auth/<provider>. The backend handles OAuth and round-trips back via #/auth-callback.
loginWithCredentials
POSTs to /api/auth/local/login; same token shape as OAuth.
logout
POSTs to /api/auth/logout, clears the token, redirects.

Build & dev

frontend/vite.config.ts — ~18 lines

Dev port
VITE_PORT, default 3000. Parallel-worker dev uses 3000 + WORKER_ID·100.
Proxy
/api and /wsVITE_API_TARGET (default http://localhost:8000). WebSocket upgrade is handled by Vite's proxy.
Production build
tsc -b && vite build. Output in dist/; served as static assets.
VITE_API_URL
Compile-time base URL for both REST and WS. Empty in dev (uses proxy); set to the production API host in deployed builds.

Why no router library

The router is ~123 lines of pure parsing. Adding react-router would buy us:

The cost of staying lean is occasional duplication when computing breadcrumbs or active-route highlighting. Worth it.

Why hash routing

Where to dive