Web Development
Develop Kandev's Vite/React routes, domain data flow, settings, workbench, state, and responsive UI.
apps/web/ is a React 19 single-page application built by Vite. In production, the Go backend serves its output and remains the API, WebSocket, and authorization boundary.
Quick path
- Run
make devand use the printed URLs. - Trace the route through
src/spa-routes.tsxorsrc/settings-routes.tsx. - Put requests in the owning domain client/hook and live updates in the WebSocket handler.
- Test loading, error, reconnect, keyboard, desktop, and mobile states before review.
Run the application
make devUse the combined development supervisor for feature work. It selects available ports, starts Go and Vite, and makes Go proxy the Vite UI. make dev-web starts Vite alone on localhost:37429; it does not provide a backend. Do not hardcode the auto-selected combined-development ports.
Trace a route
Startup flows through:
src/main.tsx
-> src/boot-payload.ts
-> components/state-provider.tsx
-> src/app-shell.tsx
-> src/spa-routes.tsxThe client first uses window.__KANDEV_BOOT_PAYLOAD__, or fetches /api/v1/app-state, then hydrates state before rendering route content. Routing is Kandev's own SPA router in lib/routing/, not React Router or Next.js. Files such as app/tasks/[id]/page.tsx are components; their paths do not create routes.
Top-level matching lives in src/spa-routes.tsx. Settings use src/settings-routes.tsx; task detail enters through src/task-detail-route.tsx. Add a route to the explicit table and its tests rather than relying on the filesystem.
Trace domain data
lib/api/domains/owns typed HTTP clients.hooks/domains/owns feature query/mutation behavior.lib/state/store.ts,lib/state/slices/, andlib/state/hydration/own shared Zustand state.lib/ws/client.ts,lib/ws/router.ts, andlib/ws/handlers/apply live updates.app/holds page-level feature components.components/holds feature and reusable composition.apps/packages/ui/,types/, andtheme/hold cross-workspace packages.
Use @kandev/ui for shared primitives. Add a state slice only when data is genuinely shared or event-driven; route-local server data does not automatically need global state.
The backend remains authoritative. A mutation should use the owning domain client/hook, and state must tolerate duplicates, reconnects, late events, and a route changing during a response. Do not reimplement workflow transitions, executor lifecycle, provider permissions, or Git rules in a component.
When a wire field changes, update the Go DTO/event, TypeScript type, HTTP client, hydration, WebSocket handler, and compatibility tests that consume it. Prefer additive changes when released clients can overlap.
Add or change settings
Settings pages live under app/settings/ and components/settings/. Route matching is in src/settings-routes.tsx. Navigation is assembled under components/app-sidebar/sections/settings/ and, for general settings, components/settings/general-nav.ts.
A setting normally needs:
- a backend-owned type, validation, scope, and persistence/secret strategy;
- a typed client in
lib/api/domains/and domain hook; - route and navigation registration;
- page/components for loading, empty, invalid, saved, error, and missing-dependency states;
- state/hydration only if other screens consume it;
- route/unit tests and a Playwright user journey.
Preserve workspace, instance, or global scope in every request and link. Make restart requirements, unavailable dependencies, and experimental status visible.
Change the task workbench
/t/:id resolves through src/task-detail-route.tsx, app/tasks/[id]/kanban-task-shell.tsx, and components/task/task-page-content.tsx. The advanced desktop layout uses components/task/dockview-desktop-layout.tsx, lib/state/dockview-store.ts, and lib/state/layout-manager/. Mobile task layout lives separately under components/task/mobile/.
Chat, plan, changes, files, terminal, editor/diff, preview, commit, and pull-request panels share task, session, repository, and executor state. Test a changed action with:
- no repository and multiple repositories;
- preparing, running, stopped, failed, and completed sessions;
- stale or delayed HTTP/WebSocket data;
- a restored dock layout and a narrow mobile route;
- absent optional providers or executor capabilities.
Localize user-facing copy
Every new user-facing string must go through i18next: t("namespace:key") or <Trans>, never an inline literal. That covers visible copy, placeholder, aria-label, title, alt, toasts, validation and empty-state text, and dialog copy. Externalization of the existing UI is complete, so a hardcoded literal is now a regression rather than unfinished migration.
Catalogs live in apps/web/src/locales/<locale>/<namespace>.json. English is the source locale and the only hand-authored catalog; it is bundled into the entry chunk, while each other locale is a lazily fetched chunk loaded before that locale activates. Shipped locales are English, European Portuguese, and Simplified Chinese, plus a generated pseudo catalog that only development and E2E builds contain. A missing key falls back to English at runtime.
Three mistakes cause silent bugs: never translate a string compared with ===, never call t() at module scope, and never pass an English plural ending as a value; use count with _one/_other keys.
pnpm run i18n:check # gate: plurals, module-scope t(), Trans indices
pnpm run i18n:ratchet # gate: no new hardcoded literals in changed lines
pnpm run i18n:parity # advisory report: per-locale missing and extra keys
pnpm run i18n:sweep <path> # advisory report: copy the jsx-only lint rule cannot seeThe first two run in pre-commit and CI. The last two are reports that always exit zero and are wired into neither, because real-locale translation is maintained out of band and must not fail an English-only change. A clean lint does not prove a file is done: the rule only inspects JSX literals and skips anything assigned to a SCREAMING_CASE identifier, so review config tables by eye and use the pseudo-locale (Settings > General > Appearance in development builds) as the completeness check. Shared @kandev/ui primitives cannot import the app's i18n runtime, so their accessibility labels are localized at the call site. Full guide: docs/i18n.md.
Security, accessibility, and responsiveness
Frontend checks improve the experience; they are not authorization. The backend must enforce identity, workspace/task scope, and destructive operations. Treat provider text, repository content, Markdown/HTML, URLs, and agent output as untrusted. Keep raw HTML rendering paired with the existing sanitizer and never interpolate shell commands or credentials in the browser.
Use semantic controls and labels, visible focus, complete keyboard interaction, named icon buttons, status text beyond color, and reduced-motion behavior. Verify narrow viewports without overflow or desktop-only hover assumptions.
Test and review
make test-web
make typecheck-web
make lint-web
make test-e2eVitest tests are colocated with source. Playwright fixtures and specs are under apps/web/e2e/. Repository policy requires every apps/web/ change to add or update a Playwright scenario; seed isolated test data and never use a developer's normal database or workspace.
Before review, confirm loading/empty/error/reconnect states, keyboard names, desktop and mobile layouts, Go/TypeScript wire compatibility, focused unit tests, truthful Playwright coverage, and updated public screenshots or terminology.
Related: Architecture, Testing, and Developer tools.
