The chat UI
Introduction
Section titled “Introduction”For a DOM app, @pilotoo/ui/chat gives you a working chat in two components. ChatProvider wires the connection (transport, catalogs, session) from a server URL and a token. ChatPanel renders the thread and the composer. Everything else on this page is customisation. For React Native, see the components of @pilotoo/chat-ui at the end.
Mounting the chat
Section titled “Mounting the chat”This is the mount of a SaaS app that already has its own login, anonymized:
import { ChatPanel, ChatProvider } from '@pilotoo/ui/chat';
export function AssistantPage({ agentId }: { agentId: string }) { return ( <ChatProvider key={agentId} baseUrl={ASSISTANT_URL} getToken={() => getAccessToken() ?? undefined} agentId={agentId} > <ChatPanel /> </ChatProvider> );}getToken returns the host’s own session token. It is read on every request and every socket reconnect, so a refreshed token is used. See Connecting a client.
One chat per agent
Section titled “One chat per agent”ChatProvider reads baseUrl and transport once, because a transport owns one connection for its whole life. To point the chat elsewhere, change key and the provider remounts. The example above does it per agent.
agentId binds new conversations to one agent and filters the conversation list to it. Set unscopedAgentId when older conversations bound to no agent should count as belonging to that agent, usually the server’s default one.
Showing the conversation list
Section titled “Showing the conversation list”ConversationList reads the same session, so place it inside the provider:
import { ChatPanel, ChatProvider, ConversationList } from '@pilotoo/ui/chat';
<ChatProvider baseUrl={ASSISTANT_URL} getToken={getToken}> <aside><ConversationList /></aside> <main><ChatPanel /></main></ChatProvider>;Check ConversationList props in packages/ui/src/chat/conversation-list.tsx before relying on more than the defaults. To build your own list, use the useChat() hook.
Translating the labels
Section titled “Translating the labels”Every string in the panel can be overridden with labels; the ones you leave out keep their English default.
<ChatPanel labels={{ newConversation: 'Nouvelle conversation', conversations: 'Discussions' }} />ChatLabels lists the keys (resolveLabels merges them). Voice has its own voiceLabels prop.
Naming your tools in the transcript
Section titled “Naming your tools in the transcript”By default a tool call shows a humanised tool name. Give your tools readable text with toolLabels, keyed by tool name:
<ChatPanel toolLabels={{ get_order: { running: 'Looking up the order', done: 'Looked up the order', describe: (args) => (typeof args.id === 'string' ? `Order ${args.id}` : undefined), }, }}/>running shows while the call is pending, done afterwards. describe receives the parsed call arguments and returns the detail line; when it is missing or returns undefined, the generic argument summary is used.
Turning links into app links
Section titled “Turning links into app links”The assistant writes Markdown. linkify rewrites the text before it is rendered, for example to turn an order number into a link. renderLink draws links itself, so relative ones can go through your router:
<ChatPanel linkify={(markdown) => markdown.replace(/#(\d+)/g, '[#$1](/orders/$1)')} renderLink={({ href, className, children }) => href.startsWith('/') && !href.startsWith('//') ? ( <Link to={href} className={className}>{children}</Link> ) : undefined }/>Return undefined to fall back to a plain anchor that opens in a new tab.
Replacing the empty state
Section titled “Replacing the empty state”empty replaces the new-conversation screen. It receives send, prefill, the composer to place and an error:
<ChatPanel empty={({ composer, send }) => ( <div> <h2>How can I help?</h2> <button onClick={() => void send('Summarise my open orders')}>Summarise my open orders</button> {composer} </div> )}/>Leading the first message with context
Section titled “Leading the first message with context”prepareFirstMessage on ChatProvider rewrites the first message of a new conversation before it is sent, so you can lead it with what the user is looking at. Later messages go as typed.
<ChatProvider baseUrl={ASSISTANT_URL} getToken={getToken} prepareFirstMessage={(text) => `[Open order: ${currentOrder.id}]\n${text}`}>Using the building blocks
Section titled “Using the building blocks”ChatPanel is assembled from ChatThread and ChatComposer. Use those, plus UserMessage, AssistantMessage, Markdown and the rest, when you want your own layout. The demo web app does this: it builds its own shell on @pilotoo/chat-core and @pilotoo/ui.
Options
Section titled “Options”ChatProvider props
Section titled “ChatProvider props”| Prop | Meaning |
|---|---|
baseUrl |
Origin of the chat server (REST and Socket.IO share it). Read once. |
getToken |
Bearer token provider; read on every request and reconnect. |
transport |
Replaces the default Socket.IO transport. Read once. |
agentId, unscopedAgentId |
The agent new conversations are bound to, and the one older unbound conversations count as. |
storage, storagePrefix |
Where the last open conversation is remembered. Defaults to localStorage. |
prepareFirstMessage |
Rewrites the first message of a new conversation. |
clientTools |
Tools only this host can run. See Client tools. |
ChatPanel props
Section titled “ChatPanel props”empty, labels, voiceLabels, toolLabels, linkify, renderLink, clientToolViews and onComposerReady (the composer’s handle, to prefill drafts). The authority is packages/ui/src/chat/chat-panel.tsx.
Package entries
Section titled “Package entries”| Entry | Contents |
|---|---|
@pilotoo/ui |
Primitives and overlays, plus cn |
@pilotoo/ui/chat |
ChatProvider, ChatPanel, ChatThread, ChatComposer, ConversationList, messages, Markdown, CodeBlock, ToolCall, AgentPicker, ModelPicker, attachment and client-tool pieces |
@pilotoo/ui/settings |
SettingsLayout, AgentsList, AgentEditor, ProfilesList, UsageSection, SchedulesSection, ThemePicker |
@pilotoo/ui/voice |
VoiceProvider, ReadAloudButton, DictationButton, hooks |
@pilotoo/ui/desktop |
Sidebar, Toolbar, applyPlatformClasses, useVibrancy, syncNativeTheme |
@pilotoo/ui/tokens.css, /fonts.css |
Design tokens; opt-in fonts |
React Native
Section titled “React Native”@pilotoo/chat-ui ships pieces, not a finished screen: a screen that owned keyboard avoidance and safe areas would be one product’s chat. You arrange MessageList, Message, Composer, MessageActions and the rest on top of @pilotoo/chat-core; the demo mobile app (apps/mobile) is the worked example.
<MessageList messages={messages} renderContent={(m) => (m.role === 'assistant' ? <Markdown>{m.content}</Markdown> : null)} renderActions={(m) => <MessageActions content={m.content} />}/>- A slot is a
ReactNodeprop (header,actions,footer,empty), never ashow*boolean. - An action renders only when its handler is given: retry needs
onRetry, stop needsonStop. Composeris controlled (value) or uncontrolled (defaultValue), decided on first render. In controlled mode you clear it once you accepted the message.classNameis layout only; visual tokens belong tovariant,sizeandtone.- Types such as
ChatMessagecome from@pilotoo/chat-core. - Native ports (
ClipboardProvider,AttachmentPickerProvider,AttachmentImageProvider) are supplied by the app. See Clipboard and Attachments.packages/chat-ui/README.mdhas the full conventions.
Next steps
Section titled “Next steps”- Connect the client to your server and your login.
- Style it with theming.
- Add voice, attachments or client tools.