Frontend guidelines
Conventions for working on frontend/. This is a contributor page, not a design
specification — the source of truth for every colour and type scale is app/globals.css.
Non-negotiables
Four rules are enforced by greps in CI, over app/ components/ lib/ hooks/. They are not
lint rules, and npm run lint will not catch them.
- No
dangerouslySetInnerHTML, norehype-raw. Reports are model-generated Markdown rendered in the user's browser; raw HTML there is an injection sink. - No hardcoded hex colours. Every colour is a token. This is what makes both themes switchable and contrast-auditable.
- No hardcoded backend URLs. The browser talks to the same-origin
/apiproxy, which is why there is no CORS preflight in normal operation. - No web storage without an inline
ci-allow-web-storage: <reason>marker on the same line. Auth is httpOnly cookies only; the marker exists so a genuine non-auth UI preference is reviewable rather than invisible.
They are GNU greps that cannot tell a use from a mention: a comment naming a banned token fails the build as surely as calling one. Describe the rule without writing the names.
Theming
next-themes with the class strategy — .dark on <html>, and Tailwind's dark:
variant redefined to follow it rather than prefers-color-scheme.
Define a colour once in :root and again in .dark. Never introduce a colour whose only
definition lives in one of the two blocks: a half-themed component looks correct to whoever
wrote it and broken to everyone on the other theme.
Values are contrast-audited against both the page ground and the card surface. If you change a token, check it clears WCAG AA (4.5:1) on both surfaces, in both themes.
Agent colours carry meaning. One distinct hue per pipeline stage, consumed through a shared map rather than inline. They were once three-of-five identical, which made different stages look like the same stage. Keep them distinct — and keep hue as reinforcement: the pipeline rail also numbers and positions each node, so meaning never rests on colour alone.
Components
- Server components by default.
"use client"only where interaction demands it. - All server state goes through TanStack Query and the shared API client. Components never
call
fetchdirectly, and there is no hand-rolled fetch-in-effect. - No
setStatein an effect to derive state. Remount with akeyinstead. - Project scoping comes from the active-project context, not from a route parameter. Putting the id in the URL as well would be two sources of truth for one choice.
- Immutable state updates; keys are stable ids, never array indices for dynamic lists.
- Extract a component before writing its third copy. Every drift bug this repository has catalogued started as a second copy of something.
Streaming
SSE parsing is a pure string state machine so it can be unit-tested with no network. Two things are load-bearing and easy to undo by accident:
- decode with
{ stream: true }, so a multi-byte character split across two network chunks survives; - carry an incomplete event across feeds, emitting only on a complete
\n\n-terminated block.
Both had real regressions — corrupted characters and dropped events at chunk boundaries — and both have named tests.
Accessibility
- Both themes pass WCAG AA on core pages; that is a merge criterion, not an aspiration.
- Every interactive control is reachable and operable by keyboard, with a visible focus state.
- Status is never conveyed by colour alone.
- Live regions for streaming content are announced without being chatty.
- Icon-only controls carry an accessible name.
Build targets
Three, not two: the standalone server image, the Tauri desktop static export, and the GitHub Pages static export. A flag read at build time collapses to dead code in the other two, which keeps them isolated — and means a branch is only exercised by the target that builds it.
npm run build # server
npm run build:desktop # desktop
npm run build:pages # public site
CI runs the first two. Anything touching app/(site)/ or app/layout.tsx needs all three
run locally.
app/(app)/session/ is generated and gitignored. Edit app-routes/session/{web,desktop}/
instead.
Tests
Vitest plus Testing Library, behaviour-focused and accessibility-first — query by role and label rather than by class name. Pure logic (the SSE parser, citation rendering, pipeline derivations, formatters) is unit-tested; whole journeys are Playwright's job.
npm test
npm run e2e
The public site
app/(site)/ is generated from the app: the documentation it renders is the repository's
docs/ tree, and the comparison table is a module the app itself imports. That is
deliberate — its predecessor was one hand-maintained HTML file that described a product
which no longer existed, and nobody noticed.
Three things next to it are still hand-written and go stale silently, because nothing fails when they do: the release list, the README download badge, and the pipeline diagram. Update them when you tag a release.