Architecture
@cognipeer/chat-ui is structured as a layered UI library:
- Components: visual building blocks (
Chat,ChatMinimal,EmbedChat,ChatMessage,ChatInput, the approval and question cards, ...). - Hooks: state and side-effect orchestration (
useChat,useChatHistory,useI18n,useBackgroundAlerts). - Transport: network abstraction for agents, conversations, streaming, runs, files and the portal routes.
AgentServerClientis the default; adapters exist for agent-sdk, Chat Completions, the Responses API and A2A. - Theme and i18n: CSS variables and theme tokens for dark / light / custom mode, and a message catalogue with locale fallback.
Data flow
- The user submits a message via
ChatInput(files are picked, dropped or pasted first; withupload-on-selecteach is stored as it is picked). useChatupdates optimistic local state.- The transport starts a streaming request to the backend. On agent-server 0.3.0+ the stream is only a subscriber of the run that executes the turn.
- Incoming events update the assistant message incrementally. Tool calls are rendered by
ToolCall; a pause becomes approval and question cards. - When the turn ends, the stored conversation is the truth: the live view is replaced by what the server kept (the answer, the cards of a pause, the stop reason, the usage).
useChatHistorysyncs conversation metadata (including each conversation'sruntime) for the sidebar and its status badges.
A page that opens or reloads a conversation while a turn is still running goes the other way: it asks the server where the run stands (runtime) and follows it from the event log, then shows the stored result.
Composition strategy
- Use
Chatfor a complete UX (history + full controls). - Use
ChatMinimalfor embedded chat surfaces on your own page. - Use
EmbedChatwhen the chat is the content of aniframeinside someone else's page and the identity comes from the host. - Use lower-level components + hooks only if a custom layout is required; then you wire the actions, the composer state and the file URL resolver yourself (see State Management).
Who decides what
| Concern | Decided by |
|---|---|
| What the chat can do (approvals, portal switches, budget actions, attachments) | The host page's props, narrowed by what the agent publishes (GET /agents: capabilities, metadata). The agent can only take features away from a switch the host turned on |
| Whether an action is allowed, who may answer, what a limit is | The server. Chat UI shows the refusal (403, 409, 402, ...) with a localized message |
| What the composer is doing | One value, composerState, derived from pending cards, a followed run, a used-up limit and readOnly |
Rendering model
- Message rendering is componentized to keep customization isolated.
- Markdown rendering is encapsulated in message components; Markdown the portal controls (welcome, announcements, terms) goes through a stricter renderer (no raw HTML, checked images).
- Tool call rendering remains visually separate from assistant text.
- Every URL that ends up in an
hreforsrcis checked first (see Defaults and limits).
Performance notes
- The library is optimized for typical chat session sizes. A long history is fetched as its newest page (100 messages on agent-server) and older messages load on request.
- Keep parent container dimensions explicit to avoid layout thrashing.
- Raw uploads send the file as the request body: no 33% base64 inflation and no giant strings in memory.

