docs/references/chat/conventions.md
Status: target architecture (design). This describes the chat layer's intended structure, which lands incrementally across carve PRs. As of this PR, only
layout/,primitives/,tokens/,utils/are onmain; the other layers and the@renderer/components/chatpackage barrel referenced below are the design target and arrive in later carves.
How the renderer chat UI under src/renderer/components/chat is divided by
responsibility, and the conventions every module follows. The target is to import from the
package entry @renderer/components/chat, not deep paths, except when working inside a
module — that barrel is added as the layers below land.
The UI is split by responsibility, not by feature. Each kind of module owns one concern
and nothing else. The tree below is the target; the landed note marks what exists on
main today, everything else is planned and arrives in later carves:
primitives/, tokens/) — landed. Stateless, themed through
@cherrystudio/ui. No business logic, no data access; everything arrives through props.layout/) — landed. Small, self-contained
pieces of interface state (layout mode, viewport insets, navbar visibility). Never
holds business or persisted data.adapters/) — planned. Pure projections of business entities (topic /
session / message) into stable UI shapes, plus the pane / action registries. Will fetch
nothing and own no cache; the single boundary between business hooks and shared UI. See
Chat Adapters.messages/, composer/) — planned. Renders a conversation from the
projected shapes; owns no send/stop/persistence, only the rendering.shell/, panes/, resources/, settings/, actions/, and the
pages/) — planned. Wires the above into screens. Owns composition, not rendering
details.State flows one way: business hooks → a contract projection → presentation. Presentation never reaches back for business state.
Create with createContext. Provide with <SomeContext value={…}> directly. Read
through a dedicated hook that calls use(SomeContext):
const ChatLayoutModeContext = createContext<ChatLayoutModeContextValue>({ … })
export const ChatLayoutModeProvider = ({ children }) => {
const value = useMemo(() => ({ forceWideLayout, setForceWideLayout }), [forceWideLayout])
return <ChatLayoutModeContext value={value}>{children}</ChatLayoutModeContext>
}
export const useChatLayoutMode = () => use(ChatLayoutModeContext)
Memoize the provider value so consumers don't rerender when an unrelated parent does.
A slice that can render outside its provider exposes an optional reader that returns
use(Context) and lets callers handle the absent case, rather than throwing.
forwardRef.useMemo; never
map raw entities inline in JSX. Keep send / stop / handler callbacks stable. The
contract layer is a pure projection, so churn comes only from fresh arrays / objects /
callbacks created during render — keep that boundary stable.useDeferredValue (e.g. the
partial tool-call arguments streamed into the agent execution timeline).