<Chat> under ChatProvider. The component loads chat history, streams messages, persists the active chat ID, and exposes slots for application-specific content.
chatId and onChatIdChange for controlled routing. Use defaultChatId when the component should manage selection internally. headerSlot, composerStartSlot, composerEndSlot, and panel add product-specific controls without replacing the shell.
Chat renders no sidebar by default because the FrogBot admin navigation already lists conversations. Pass renderSidebar to embed your own history surface; it receives { chats, activeChatId, selectChat, actions }, so the exported ChatHistory component drops straight in:
Chat props
Dock
The dock sits between the queued messages and the composer, for requests that need the user before the chat can go on. A waiting client tool call with a registered renderer, such as a question, docks there on its own and hides the composer until it is answered or dismissed; it then shows in the conversation as usual. Pass your own cards throughdockSlot, below any docked call, and set dockBlocking while they wait for an answer. The composer stays mounted while hidden, so its text and attachments come back with it.
ChatDock gives a docked card FrogBot’s look: an optional header, the body, and an optional footer with a top border for actions.
Typed commands
Register/ commands with commands on ChatProvider: typing / opens a menu of them, and /id rest runs one that takes arguments. For anything else, use onBeforeSubmit.
onBeforeSubmit runs before each message is sent and receives { text, mentions, attachments }. Return true and nothing is sent: the text clears and attachments stay in the composer. Return false to send as usual. If it throws or rejects, nothing is sent and the chat shows Command failed: <message>. A registered command that matches runs first, and onBeforeSubmit isn’t called for it. While a reply runs, a send is queued without calling it.
matchesModelSearch, normalizeModelSearch and compactModelSearch are the model list’s search, so /model sonnet can pick the model the list would show first:
Queued messages
While a reply runs, Enter or the Queue message button beside Stop adds the prompt, with its attachments, to a list above the composer and clears the composer. Stop only stops the reply; it never sends what you’ve typed. Attachments, drops and the microphone keep working while a reply runs. A command withqueue: true waits in the list as its / text; other commands run at once.
Each entry has Edit, which moves it back into an empty composer, and Remove. When a reply ends normally, with no question waiting, the first entry is sent with the current model, and the next waits for that reply. Stop, a failed reply or switching chats pauses the entries; a prompt you send then goes out first, and Send now sends a paused entry. Entries are kept per chat while the page is open and are lost on reload.
With an adapter steerMessage, a queued message also has Steer while a reply runs: it is handed to the running turn and shows Steering until the reply takes it, as a data-steered part, or the turn ends.
Errors
When a reply or a chat action fails,Chat shows the error above the composer. Provider failures read as actionable messages, such as The AI provider rejected the API key. Check OPENAI_API_KEY in .env. for a rejected key, and never include the provider’s raw response. The full error is still logged on the server.
Pass onError to handle failures yourself, and errorContent to replace the inline error, or false to hide it. The FrogBot admin uses both to show failures as toasts:
Chat resolves by reloading the conversation are not reported.
Chat row actions
Admin recents include Rename and Delete actions. Hover a row to reveal its menu button, or right-click the row to open the same keyboard-accessible menu. Rename opens with the current title selected and requests one AI title suggestion; selecting the suggestion fills the input without saving it. Delete always asks for confirmation and removes the chat with its messages. Pass the adapter to add the same actions to a portableChatHistory. Rename shows only when the adapter has renameChat, Delete only when it has deleteChat, and a row with neither has no menu. Use onNewChat to clear controlled routing after the active chat is deleted:
ChatRowActions, ChatRowMenuItems, ChatHistoryActions, and RenameChatDialog are independently exported from @frogbotai/ui/chat for custom row composition. They work without ChatProvider. The rename dialog’s suggestion comes from the adapter’s suggestChatTitle; the REST adapter calls POST /frogbot/chat/suggest-title, which resolves to undefined when generation produces no title.
ChatHistoryActions wraps one row with the menu, rename dialog, and delete confirmation:
ChatRowActions adds the hover menu button and right-click menu to a row without any requests of its own. Leave out a handler to hide its item; with neither, the row is rendered without a menu:
ChatRowMenuItems renders the Rename and Delete items for a menu you build yourself:
RenameChatDialog requests a title suggestion from the adapter’s suggestChatTitle each time it opens, and shows none when the adapter has no suggester:
Message actions
Completed user messages include Copy and Edit actions. Assistant messages include Copy and Branch; Branch needs the adapter’sbranchChat and is hidden without it. Editing uses the composer’s editor with its mention chips; Enter sends and Escape cancels. It preserves the other non-text parts, replaces the text and data-mention parts, replaces the selected message, removes later responses, and retries the conversation from that point. Branch creates and opens a separate chat containing the conversation through the selected message.
User and assistant message text, the message editor and the composer render at 14px from the --fb-text-sm token, with compact headings and 13px inline code. The sizes are in rem, so they follow the app’s root font size, and apps can override --fb-text-sm or the .fb-markdown__* classes with unlayered CSS.
Copy writes rendered HTML and original Markdown to the clipboard. Formatting is preserved when pasted into rich-text editors, while plain-text destinations receive the original Markdown.
Each message’s send time appears with its actions: leading the controls on user messages and trailing them on assistant messages. Messages from the last seven days read Saturday 6:58 PM, older messages read Jan 12 6:58 PM, and messages from earlier years include the year. Stored history uses the recorded creation time.
Pass components to userMessageActions and assistantMessageActions to compose application actions with the defaults. Because each slot is a component, it can use your provider hooks. Render defaultActions to retain FrogBot’s controls, omit it to replace them, or pass false to hide that role’s actions.
The built-in controls are also independently importable for custom compositions:
Channel conversations
Chat reads the active chat’s document, from the initialChat prop when it matches the active chat and otherwise with a request. Pass initialChat when your server already has the document, so a channel conversation renders read-only from the first paint. When the chat belongs to a channel, Chat shows the conversation read-only: ChannelConversationNotice replaces the composer, user messages have no Edit action, and tool renderers receive isReadonly. Branch stays on the notice and on assistant messages.
Custom shells can build the same view from useChatDocument and ChannelConversationNotice. useChatDocument returns { chat, error, refresh }, where chat includes the chat’s channel and channelLabel. It accepts:
ChannelConversationNotice takes:
Composer selectors
AgentSelector reads the agents available to the current user from ChatProvider. Keep the selected agent in your application state and pass it to both the selector and <Chat>. Changing it starts a new conversation. Place selectors in composerStartSlot, as the admin chat does; keep headerSlot for content above the messages.
For any other choice in the composer, such as a mode, use
ComposerSelect. AgentSelector is built on it, so the two look the same.
ModelSelector shows the active model and reasoning level as compact text. Clicking it opens a reasoning slider for that model; clicking the model name switches to the model list, grouped by provider when there is more than one. A group is labelled with its first model’s providerName, then FrogBot’s name for the provider (OpenAI for openai), then the provider slug; models without a provider go under Other. While the selector is open, its trigger reads Select effort on the slider and Select model on the list. A model without reasoning levels opens straight on the list, which then has no Back button, and choosing such a model closes the selector; choosing a model with levels shows its slider. The reasoning slider shows the active level’s name above a track with one dot per level. The selector keeps one width in both views and while searching, and long model names are truncated. Its selection is fully controlled, and it renders nothing when models is empty.
Each row starts with its provider’s logo. Logos ship with FrogBot for every catalog provider plus FrogBot (frogbot), Ollama (ollama) and LM Studio (lmstudio), so the list makes no network requests; custom and unknown providers get a generic icon. Hovering a row shows its model ID, and rows that share a name also show their IDs.
A search field above the list filters by name, ID, provider slug, and group label, ignoring case, spaces, and punctuation. Typing while a row has focus goes straight to the field, Down moves from the field to the first result, and Enter in the field chooses the first result. Escape clears the search first; with an empty search it closes the selector.
Pass the same choice to
<Chat> through model and reasoning; each message is sent with them. The agent manifest lists allowed models, their display names in names, and each model’s reasoning options, so a selector fed from it never offers a choice the server rejects. names holds catalog names such as Nova Micro (US) and the name of custom provider models; a model FrogBot has no name for is left out, so fall back to its ID. When the model changes, reset or restore the level yourself: a key from one model does not apply to another.
ToolSelector accepts a caller-supplied tool list and controlled multi-select state. Place it in composerStartSlot or composerEndSlot. It renders nothing when its list is empty.
The selectors intentionally differ from Firmware where FrogBot has no matching concept: agents have no “Default” row, model billing locks are omitted, reasoning levels use a per-model slider, tools use multi-select availability instead of prompt injection, and Radix handles menu positioning.
Page context
PageContextButton provides Firmware’s open-tab picker for browser extension and desktop hosts. FrogBot does not mount it by default. A host supplies its tab bridge and places the button in composerEndSlot.
The host remains responsible for reading tab content and creating
data-page-context parts. The button only lists and toggles tab IDs.
Attachments
Dropping or selecting files in the composer uploads each one through the adapter’suploadAttachment, then sends a file-reference part carrying the stored id. With the REST adapter, files go to the chat assets collection advertised by the manifest as chat.assetsSlug; the server links the asset to the chat on the first message that references it, and on every agent turn sends the file in a form the selected model can read. See chat assets for storage and access rules, and how attachments reach the agent for what the model receives.
Pass an adapter with uploadAttachment to a standalone Composer to enable the attachment control; <Chat> passes the provider’s adapter automatically, along with the selected model’s input types and display name (modelInputs and modelName, see Composer props).
A file-reference part has these fields:
Which files the composer accepts
The composer sorts each picked, dropped, or pasted file with the same rules the server uses when it prepares attachments for the agent:
The file picker offers only the accepted types. A file that isn’t accepted, for example one dropped onto the composer, gets a red card and is never uploaded.
Code files that FrogBot’s restricted file types would block, such as
app.js, main.py, or page.html, are read in the browser and uploaded as plain text named app.js.txt. The message still refers to the real name, so the agent receives Attached file "app.js": followed by the code. A restricted file that isn’t text is refused.
Word and Excel files are uploaded as they are, without being read in the browser. The server reads them during the upload, and the card shows the text from the upload response. A file the server refuses, such as a corrupt, password-protected, or renamed file, gets a red card without Retry, because uploading the same file again can’t succeed.
A file’s state is decided when it is added. Switching models afterwards changes nothing: files already in the composer keep their cards and are sent, and the server replaces any the new model can’t read with a marker.
Attachment cards
Each file in the composer shows one card in one state:
The model name in a refusal is the model’s display name from the manifest, or its ID without the provider when it has none. A file the browser can’t read at all, such as one changed on disk after it was picked, shows Upload failed. “This file couldn’t be read.” without Retry.
The three red states have a red border and icon; their text keeps the normal text colour in light and dark themes. Text cards are tagged with the file’s extension in capitals when it has five characters or fewer (
MD, JS, JSON), TEXT otherwise, and PASTED for pasted text. Word and Excel files are tagged DOCX or XLSX, even without an extension. Click a text card, or focus it and press Enter, to open the full text in a viewer with Copy and Close buttons. For a Word or Excel file, the viewer shows exactly the text the model receives after the file’s label, including any note about rows or characters left out.
You can send while red cards are showing. Only ready and text attachments are sent, and every card that was in the composer when you sent, red ones included, clears with the draft. The send button appears once there is text or at least one ready or text attachment, and stays hidden while files are uploading or being checked.
Large files
An attachment over 10 MB (10,000,000 bytes) that isn’t red gets an amber edge and a Large file tag. Its tooltip, also read by screen readers as the card’s description, says “Large file (11.2 MB). In long chats, older files may be left out to keep requests small.” The file is still sent; the hint refers to the server’s size limit on each model request. Word and Excel files never get the tag: the model receives only their text, which is capped, not the original file.Sent messages
A sentfile-reference part shows the same text card for text attachments, named by the part’s filename and opening the same viewer. For an asset with a stored text field, such as a Word or Excel file, the card uses that text and the file itself isn’t downloaded; one stored without text shows a download link until a turn reads it. Images show the image; PDFs, audio, and video show a download link with the file name. Either kind gets the large-file tag over 10 MB, except a Word or Excel text card. While the file loads, the part shows “Loading attachment”; if it can’t be loaded, or the adapter has no readAttachment, “Attachment unavailable”.
Screen readers
Each card is a group named after the file, its type tag, and its state, such as “app.js, JS, ready” or “photo.png, PNG, won’t be sent”. Red cards are described by their heading and reason. The buttons are named “Open notes.md”, “Remove notes.md”, and “Retry uploading notes.md”. One polite status region per composer announces each change:
Several refusals in one drop are announced together, separated by full stops.
Pasted text
Pasting more than 650 characters into the composer turns the paste into a Pasted text card taggedPASTED; a paste of 650 characters or fewer stays in the input. With upload storage (an adapter with uploadAttachment), the text is uploaded as a plain-text asset named pasted-<time>.txt and sent as a file-reference part with filename: 'Pasted text' and origin: 'paste'. FrogBot sets no size limit on pasted text. A composer without upload storage keeps the text in the browser and sends it as a data-paste part instead. Either way, the agent receives Pasted text: followed by the text.
Message data parts
Chat renders these AI SDK v7 data part types:
Use a new
data-<name> type for application-specific structured content. Register a renderer for it with dataRenderers on ChatProvider, or pass renderData to the lower-level MessagePart component; otherwise DataPart displays its JSON payload.
A data-paste part holds pasted text inline. The composer sends one only when it has no upload storage; see Pasted text. Chat shows it as a Pasted text card that opens the full text, and the agent receives it as Pasted text: followed by the text.
Voice input
The composer shows a microphone when theChatProvider adapter has transcribe and its manifest sets transcribe. With the REST adapter, that is when ai.transcriptionModel is set, the signed-in user may transcribe with it, and the browser supports MediaRecorder and microphone access. Tap once to record and again to transcribe. The resulting text is appended to the current draft.
ai.access.transcribe allows them for the transcription model. For an alias, the rule receives the model behind the alias, so with the AI policy plugin the user’s models list must include that model, not the alias. Without transcriptionModel, or for a user who may not transcribe, the composer shows no microphone.
Recording uses the browser’s supported audio container: Opus in WebM where available, MP4 on Safari, or the browser default. Audio is sent for batch transcription after recording stops; transcription is not streamed.
Permission, access, network, and empty-audio failures appear beside the microphone without clearing the draft. The recording button retains Firmware’s pulse and ripple feedback. The separate live waveform is intentionally omitted because local batch recording does not require Firmware’s streaming audio analyzer.
Read aloud
Each AI reply gets a Read aloud button next to Copy when theChatProvider adapter has speak and its manifest sets speak. With the REST adapter, that is when ai.speechModel is set and the signed-in user may generate with it. Set ai.speechVoice to pick a voice; without it the provider’s default voice is used.
ai.access.generate, the rule checked for the speech model. For an alias, the rule receives the model behind the alias, so with the AI policy plugin the user’s models list must include that model. A failed request shows a short message beside the button.