--- name: ai-chat-guidelines description: Architecture rules and standards for this repository's AI chat feature, covering layered feature slices, ports and adapters, pure reducers, the shadcn/ui chat components, naming, comments and commits. Use when adding or changing anything under src/features, wiring a real model or provider, adding a message part type, editing src/components/ui, or writing commits for this repo. metadata: repository: revix-test-ai-chat --- # AI chat guidelines Rules the chat feature already follows. Apply them to any change in this repo, and read the referenced files before a change that spans layers. - [references/architecture.md](references/architecture.md): layer contract, allowed imports, worked examples for common changes. - [references/shadcn-chat.md](references/shadcn-chat.md): the chat component APIs, composition order and the gotchas already hit. - [references/conventions.md](references/conventions.md): naming, comments, commits, verification. ## Layers One feature per slice under `src/features//`, four layers, dependencies pointing inward: ``` ui → application → domain ← infrastructure ``` | Layer | Holds | May import | | --- | --- | --- | | `domain/` | types, factories, selectors, ports | nothing but itself | | `application/` | reducers, hooks, orchestration | `domain/` | | `infrastructure/` | port implementations, I/O, fake data | `domain/` | | `ui/` | components | `domain/`, `application/`, `components/ui/` | Rules: - `domain/` imports no React, no Next, no browser API beyond `crypto.randomUUID`. - `application/` never imports `infrastructure/`. It receives a port. - `infrastructure/` never imports `ui/` or `application/`. - Cross-feature imports go through the feature's `index.ts` and nothing deeper. - `src/app/` composes only. A route file renders a component and does nothing else. ## Ports and adapters Every external capability is an interface in `domain/`, implemented in `infrastructure/`, and chosen at one composition root in `ui/`. ```ts // domain/agent.ts export interface ChatAgent { respond(turn: AgentTurn): AsyncIterable } ``` - Stream with `AsyncIterable`, not callbacks. It cancels, it composes, and it matches how model SDKs already stream. - Every port that does work takes an `AbortSignal` and honours it. - Pick the implementation in the top component of the feature (`useMemo(() => createFakeAgent(), [])` in `chat-panel.tsx`). No module-level singletons, no provider lookup by string. - Swapping the fake agent for a real one must not touch `domain/`, `application/` or `ui/`. If it would, the port is wrong. ## State - The whole lifecycle lives in one pure reducer in `application/`. No `setState` chains, no state derived in two places. - Actions are a discriminated union named after what happened (`message-sent`, `turn-finished`), not after the setter they call. - Status is an explicit union (`idle | thinking | streaming | awaiting-answer`), never a pile of booleans. - The hook owns effects only: running the stream, dispatching, aborting. - When a handler needs state the render has not seen yet, mirror it in a ref by running the same reducer, so the ref and the store can never disagree: ```ts const apply = React.useCallback((action: ChatAction) => { const next = chatReducer(conversationRef.current, action) conversationRef.current = next dispatch(action) return next }, []) ``` - Guard transitions in the hook with domain predicates (`canSendMessage`), not with `if (status === "idle")` scattered through components. ## Messages - A message holds `parts: MessagePart[]`, a discriminated union (`text | reasoning | question`). Never a bare string. - New capability means a new part type plus one component in `ui/parts/`, and nothing else changes. - Streaming text appends to the trailing `text` part; other events push a part. ## UI - Components read `status` and dispatch intent. No business rules in JSX. - One component per file, named after the file, no default exports except route files. - `"use client"` only on files that need it: hooks, state, event handlers. - Presentational part components stay server-compatible when they can. - Never edit generated files in `src/components/ui/`. Wrap or compose instead. If a generated component references a missing CSS utility, add the utility in `src/app/globals.css` with `@utility`. ## Fake data - Lorem ipsum and every other placeholder lives in one module under `infrastructure/`. No inline fake strings in components or reducers. - Randomness goes through the helpers in `infrastructure/random.ts` so behaviour stays swappable and readable. - Fake timing is configurable through the adapter's options, with defaults in one `DEFAULTS` object. ## Before committing Run all three, in this order: ```bash npx tsc --noEmit && pnpm lint && pnpm build ``` Commit in conventional-commit style, one commit per layer or concern, each one building on its own. Full rules in [references/conventions.md](references/conventions.md).