113 lines
5.4 KiB
Markdown
113 lines
5.4 KiB
Markdown
# Frontend (Next.js) — architecture and conventions
|
|
|
|
Scoped guidance for `apps/web/`. Repo-wide rules (commit format, code-quality limits, etc.) live in the root `AGENTS.md`.
|
|
|
|
## UI Components
|
|
|
|
**Shadcn Components:** Import from `@kandev/ui` package:
|
|
|
|
```typescript
|
|
import { Badge } from "@kandev/ui/badge";
|
|
import { Button } from "@kandev/ui/button";
|
|
import { Dialog } from "@kandev/ui/dialog";
|
|
// etc...
|
|
```
|
|
|
|
**Do NOT** import from `@/components/ui/*` - always use `@kandev/ui` package.
|
|
|
|
- Always prefer native shadcn components over custom implementations.
|
|
- Check `apps/packages/ui/src/` for available components (pagination, table, dialog, etc.).
|
|
- For data tables, use `@kandev/ui/table` with TanStack Table; use shadcn Pagination components.
|
|
- Only create custom components when shadcn doesn't provide what's needed.
|
|
|
|
## Data Flow Pattern (Critical)
|
|
|
|
```text
|
|
SSR Fetch -> Hydrate Store -> Components Read Store -> Hooks Subscribe
|
|
```
|
|
|
|
**Never fetch data directly in components.**
|
|
|
|
## Store Structure (Domain Slices)
|
|
|
|
```text
|
|
lib/state/
|
|
├── store.ts # Root composition
|
|
├── default-state.ts # Default state + initial state merge
|
|
├── slices/ # Domain slices
|
|
│ ├── kanban/ # boards, tasks, columns
|
|
│ ├── session/ # sessions, messages, turns, worktrees
|
|
│ ├── session-runtime/ # shell, processes, git, context
|
|
│ ├── workspace/ # workspaces, repos, branches
|
|
│ ├── settings/ # executors, agents, editors, prompts (incl. userSettings)
|
|
│ ├── comments/ # code review diff comments
|
|
│ ├── github/ # GitHub PRs, reviews
|
|
│ └── ui/ # preview, connection, active state, sidebar views
|
|
├── hydration/ # SSR merge strategies
|
|
|
|
hooks/domains/{kanban,session,workspace,settings,comments,github}/ # Domain-organized hooks
|
|
lib/api/domains/ # API clients
|
|
├── kanban-api, session-api, workspace-api, settings-api, process-api
|
|
├── plan-api, queue-api, workflow-api, stats-api, github-api
|
|
├── user-shell-api, debug-api, secrets-api, sprites-api, vscode-api
|
|
├── health-api, utility-api
|
|
```
|
|
|
|
**Key State Paths:**
|
|
|
|
- `messages.bySession[sessionId]`, `shell.outputs[sessionId]`, `gitStatus.bySessionId[sessionId]`
|
|
- `tasks.activeTaskId`, `tasks.activeSessionId`, `workspaces.activeId`
|
|
- `repositories.byWorkspace`, `repositoryBranches.byRepository`
|
|
|
|
**Hydration:** `lib/state/hydration/merge-strategies.ts` has `deepMerge()`, `mergeSessionMap()`, `mergeLoadingState()` to avoid overwriting live client state. Pass `activeSessionId` to protect active sessions.
|
|
|
|
**Hooks Pattern:** Hooks in `hooks/domains/` encapsulate WS subscription + store selection. WS client deduplicates subscriptions automatically.
|
|
|
|
## WebSockets
|
|
|
|
**Format:** `{id, type, action, payload, timestamp}`.
|
|
|
|
Use subscription hooks only; the WS client auto-deduplicates.
|
|
|
|
## Component conventions
|
|
|
|
- Components: <200 lines, extract to domain components, composition over props.
|
|
- Hooks: domain-organized in `hooks/domains/`, encapsulate subscription + selection.
|
|
- **Interactivity:** all buttons and links with actions must have `cursor-pointer` class.
|
|
- **Radix tooltip on disabled buttons:** disabled buttons do not receive pointer/focus events, so wrap the disabled `Button` in a focusable span and put `TooltipTrigger asChild` on that span:
|
|
```tsx
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span tabIndex={disabled ? 0 : -1} className="inline-flex">
|
|
<Button disabled={disabled}>Run</Button>
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{disabledReason}</TooltipContent>
|
|
</Tooltip>
|
|
```
|
|
Keep the wrapper focusable only while disabled; when enabled, the button itself owns focus.
|
|
- **Renaming a `data-testid`:** set the new id as `data-testid="<new>"` and keep
|
|
the old id as `data-legacy-testid="<old>"`, then migrate e2e specs to the new
|
|
id in the same PR. JSX rejects two `data-testid` attributes on one element,
|
|
and Playwright's `getByTestId` only matches one attribute name — the
|
|
`data-legacy-testid` alias lets existing specs keep selecting the element
|
|
while the migration is in flight.
|
|
|
|
## Code-quality limits
|
|
|
|
Enforced by `apps/web/eslint.config.mjs` (warnings, will become errors):
|
|
|
|
- Files: ≤600 lines · Functions: ≤100 lines
|
|
- Cyclomatic complexity: ≤15 · Cognitive complexity: ≤20
|
|
- Nesting depth: ≤4 · Parameters: ≤5
|
|
- No duplicated strings (≥4 occurrences) · No identical functions · No unused imports
|
|
- No nested ternaries
|
|
|
|
When you hit a limit, extract a helper function, custom hook, or sub-component. Prefer composition over growing a single function.
|
|
|
|
## Testing notes
|
|
|
|
- jsdom drops `secure` cookies over `http`, so `document.cookie` reads back empty. To assert a cookie write in a Vitest unit test, intercept the setter with `Object.defineProperty(document, "cookie", { set: ... })` and restore it after.
|
|
- In Playwright tests, avoid strict locators that assume only one `terminal-panel` or `.xterm` exists. Mobile and dockview layouts can mount multiple terminal instances; scope to the active panel or use `.first()` / `.last()` deliberately with a comment or helper.
|
|
- Shared E2E helpers that inspect mounted React/DOM internals must be scoped to the active panel/container, not global selectors, because hidden or stale mounted panels can coexist in dock/mobile layouts.
|