Repository navigation
fix(ai-components): align agent conversations with the other SDKs - #3974
Merged
Merged
Conversation
…y fail AIConversationController.loadOlderMessages swallowed the transport's error, so the conversation's list took the load for a success and asked again every 300 ms while the person stayed at the top. It now rejects, and the list waits 3 s before it asks again.
A pending question whose server copy arrived in the same update it was first seen in showed twice: the copy counted as a message the question couldn't be. It's now matched against the previous update's messages. A page of older messages that started with a reply took that reply into the turn above it, which changed the turn's key and re-pinned the latest one. A row that started a turn now keeps starting one.
StreamingMessageView typed a fixed 2 letters a frame, so a fast model left it behind, and the reply's actions, which wait for it, came late. Without letterInterval or renderingLetterCount it now types each new part within about 0.4 seconds, at least a letter a frame, as the web SDK does. Passing either keeps a fixed pace. The view and useStreamingMessage also offer skipAnimation, to show the rest at once.
A step without an id is told apart by its place in the reply, such as ai_tool_call-0, which another reply repeats, so a later call looked already sent and never ran. The runner now skips such calls, as Android's does. A maxAttempts below 1, which silently never ran anything, now throws.
AIUIHost.capabilities() sent the tools' definitions as given, and an empty list without tools.
It now sends them as the web SDK does, as Model Context Protocol tools with an inputSchema ({
type: 'object' } when there is none) and a boolean readOnlyHint, and leaves tools out when
there are none. aiUIClientToolJSON is exported for backends that write the capabilities
themselves.
…or failed to A custom AssistantMessage couldn't tell a reply that answers with UI, or whose UI failed to show, from one that answered with nothing. It now gets showsUI and uiFailed, as on the web, and AIUIHost has hasFailed(messageId).
…s it The approval card asked only the agent's question, so the user allowed a call without seeing what it would send. As on the web, it now shows the call's arguments as indented JSON under "What it sends", folded when long. The approver's argumentsPreview(call) gives a view of its own, or null for none, AIToolApprovalCard takes the preview as argumentsPreview, and AIToolApproval's render function gets it as its 4th argument. AIToolArguments is the default view.
A reply that answers only with UI or attachments showed copy and share, and sharing it shared an empty string. Both now show only when the reply has text, as on iOS, and the actions render nothing when there's nothing to offer.
Once a sign-in came back connected, the card said "Connecting..." until the agent changed the call's step, which it may never do. After 30 seconds it now offers Connect again, as Android does.
Dictation always recognized en-US; it now uses the device's language, as every other SDK does, unless a language is given. The composer's placeholder, the voice visualizations' name and state labels, and the default text of AIAgentError were English only; they now go through the Chat SDK's translations, with keys in every locale. AI_AGENT_ERROR_MESSAGE is the error's key, so the samples show t(error.message).
The web SDK names the app's catalog stream-app-catalog, so an agent set up from a web app's capabilities wrote surfaces this renderer refused. With the default catalog id, getstream.io:app-catalog, which iOS and Android also use, the renderer now accepts that id too, and so does a2uiProblems. It's exported as A2UI_WEB_CATALOG_ID.
The A2UI instructions gave the app's tools only as functions of the catalog, so the model could bind a tool that changes data to a display, which the host then refuses to run. They now list the tools, mark the read-only ones, and say that the others run only from a button the user taps, as Android's do.
A2UI's formatting always used en-US, formatCurrency always used 2 decimals, and formatDate returned nothing for epoch timestamps and times of day. Numbers and dates now follow the device's locale unless the renderer is given one, each currency gets its own decimals unless decimals is given, and formatDate reads epoch seconds and milliseconds and HH:mm, as Android's does.
Inline code in messages, Markdown code in the AI components and the syntax highlighter's default asked Android for the font 'Monospace'. Android knows it only as 'monospace', and falls back to the default font without a warning, so code was drawn in a proportional font. iOS is unchanged.
…rawn in The fast syntax highlighter's native text measured its width without its fontFamily, in Android's default font, then drew in monospace, which is wider. The width was padded by a flat 1.6 to make up for it, which left empty space after most code and still clipped and wrapped lines of narrow characters, such as i and l. The measurement now uses the font the text is drawn in, and the padding is gone, so a block is as wide as its longest line.
oliverlaz
approved these changes
Nov 11, 2026
… every message Matching a pending question's copy that arrives in the same update the question is first seen in kept the ids of all the person's messages from every update, so every streamed chunk of a reply went over the whole list. The copy of a question just sent is always the person's newest message, so the tracker now remembers only that one, found by stepping back from the end, and takes it for the copy when it arrived with the update.
Contributor
SDK Size
|
oliverlaz
added a commit
to GetStream/stream-chat-react
that referenced
this pull request
Nov 11, 2026
…ns (#3428) Ticket: https://linear.app/stream/issue/REACT-1559/ai-components-add-aiconversationcontroller-port-from-react-native-and ### Goal Every app built on the AI components works out `messages`, `pendingMessage`, `isThinking`, ratings, retries and editing itself. StreamChatAI (iOS) and stream-chat-react-native already share one conversation-state contract (`AIConversation` / `AIConversationController`). This brings the same controller to the web kit, so both JS kits read the same and an agent SDK transport (`agentChatTransport`) can plug into either. ### Implementation details - `Conversation/controller/`: `AIConversationController`, `AIConversationExchange`, `AIAgentError` + `AI_AGENT_ERROR_MESSAGE`, `offers`, `types`, `useAIConversationState`, copied from React Native `v9-release` (including GetStream/stream-chat-react-native#3974). Names and behaviour are kept: send, stop, regenerate, edit/cancelEditing, retry, clearError, rate, loadOlderMessages, `actions`, `chatOption`, `custom`, the reply timeout and React Native's retry. A generated UI's action travels as `request.custom.uiAction`. Web adaptations only: - request attachments are `File`s; a pending message shows them through object URLs the controller makes once per file and revokes on `dispose()`; - the composer store is the web `AIMessageComposerStore`, which gains `isEditing`, `chatOption` and `custom` (new `createAIMessageComposerStore()`, React Native's `createNewComposerStore`); - ids come from `crypto.randomUUID` (with a fallback for insecure contexts), deep comparison uses `react-fast-compare`, and `useStateStore` is the SDK's own; - `AIMessage` gains `rating`; - `AI_AGENT_ERROR_MESSAGE` stays English text, as in React Native; web translation keys are dotted ids, so the README shows how to detect the error and show copy of your own. - `<AIConversation conversation={…}>`: reads messages, the pending message, thinking (status `submitted`), older messages, the approver and reply actions (rating, regenerate when offered), and offers edit on the person's latest message. The existing props keep working; `messages` is now optional. - `<AIMessageComposer conversation={…}>`: uses the controller's composer store; submitting sends `{ text, attachments, chatOption, custom }` through `conversation.send` unless `onSubmit` calls `preventDefault()`; `SubmitButton` turns into a `StopButton` while the agent answers, and Enter doesn't send meanwhile; an "Editing" chip with a cancel button shows while `isEditing`, and the text input takes focus. `ChatOptions` keeps the picked option in the store, where it stays picked across a form reset. - `AIPendingMessageTracker` / `turnsOf`: ported the fixes from GetStream/stream-chat-react-native#3974. A question no longer shows twice when its copy arrives in the same update, and a turn keeps its key when older messages load above it. The new tests fail on master. - Two new translation keys: `aiComponents.messageComposer.editing.text` and `aiComponents.messageComposer.cancelEditing.ariaLabel`. - README: a new `AIConversationController` section, including the StrictMode-safe pattern (create the controller in a `useEffect` and `dispose()` it in that effect's cleanup, never `useState(() => new …)` plus dispose). Tests: React Native's controller and exchange suites ported to Vitest (100 tests), UI tests for the controlled conversation, the composer and a StrictMode mount, and the tracker regressions. `yarn test`, `yarn lint`, `yarn types` and `yarn validate-translations` pass; `yarn types:tests` reports no errors in the touched files. ### UI Changes - Composer: an "Editing" chip with a cancel button above the text while a message is edited, styled like the chat-option chip; in `layout='inline'` it takes a full-width row. Not yet checked in a browser. - Composer with a conversation: the send button turns into the stop button while the agent answers. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added controller-backed AI conversations with response status, message editing, retry and regeneration, stopping responses, reply ratings, and loading older messages. * AI conversation and composer components can connect to a conversation controller. The composer supports editing drafts, canceling edits, and showing a stop control while a response is in progress. * Added conversation state subscriptions and configurable chat-option selection. * Added translated labels for canceling edits and the editing status. * **Documentation** * Updated AI component guidance with controller setup, lifecycle, and usage details. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
oliverlaz
added a commit
to GetStream/stream-chat-react
that referenced
this pull request
Nov 11, 2026
…ns (v14) (#3429) Ticket: https://linear.app/stream/issue/REACT-1559/ai-components-add-aiconversationcontroller-port-from-react-native-and ### Goal The v14 port of #3428, for the `release-v14` line. Every app built on the AI components works out `messages`, `pendingMessage`, `isThinking`, ratings, retries and editing itself. StreamChatAI (iOS) and stream-chat-react-native already share one conversation-state contract (`AIConversation` / `AIConversationController`). This brings the same controller to the web kit, so both JS kits read the same and an agent SDK transport (`agentChatTransport`) can plug into either. ### Implementation details - `Conversation/controller/`: `AIConversationController`, `AIConversationExchange`, `AIAgentError` + `AI_AGENT_ERROR_MESSAGE`, `offers`, `types`, `useAIConversationState`, copied from React Native `v9-release` (including GetStream/stream-chat-react-native#3974). Names and behaviour are kept: send, stop, regenerate, edit/cancelEditing, retry, clearError, rate, loadOlderMessages, `actions`, `chatOption`, `custom`, the reply timeout and React Native's retry. A generated UI's action travels as `request.custom.uiAction`. Web adaptations only: - request attachments are `File`s; a pending message shows them through object URLs the controller makes once per file and revokes on `dispose()`; - the composer store is the web `AIMessageComposerStore`, which gains `isEditing`, `chatOption` and `custom` (new `createAIMessageComposerStore()`, React Native's `createNewComposerStore`); - ids come from `crypto.randomUUID` (with a fallback for insecure contexts), deep comparison uses `react-fast-compare`, `StateStore` comes from `stream-chat`, and `useStateStore` is the SDK's own; - `AIMessage` gains `rating`; - `AI_AGENT_ERROR_MESSAGE` stays English text, as in React Native; the README shows how to detect the error and show copy of your own. - `<AIConversation conversation={…}>`: reads messages, the pending message, thinking (status `submitted`), older messages, the approver and reply actions (rating, regenerate when offered), and offers edit on the person's latest message. The existing props keep working; `messages` is now optional. - `<AIMessageComposer conversation={…}>`: uses the controller's composer store; submitting sends `{ text, attachments, chatOption, custom }` through `conversation.send` unless `onSubmit` calls `preventDefault()`; `SubmitButton` turns into a `StopButton` while the agent answers, and Enter doesn't send meanwhile; an "Editing" chip with a cancel button shows while `isEditing`, and the text input takes focus. `ChatOptions` keeps the picked option in the store, where it stays picked across a form reset. - `AIPendingMessageTracker` / `turnsOf`: ported the fixes from GetStream/stream-chat-react-native#3974. A question no longer shows twice when its copy arrives in the same update, and a turn keeps its key when older messages load above it. The new tests fail on `release-v14`. - Two new translation keys, `Editing` and `aria/Cancel editing`, filled in all 12 locales (React Native's wording where it has them; German and Portuguese are new). - README: a new `AIConversationController` section, including the StrictMode-safe pattern (create the controller in a `useEffect` and `dispose()` it in that effect's cleanup, never `useState(() => new …)` plus dispose). Tests: React Native's controller and exchange suites ported to Vitest (100 tests), UI tests for the controlled conversation, the composer and a StrictMode mount, and the tracker regressions. `yarn lint` (including translation validation), `yarn types` and `yarn test` (266 files, 3550 tests) pass. ### UI Changes - Composer: an "Editing" chip with a cancel button above the text while a message is edited, styled like the chat-option chip; in `layout='inline'` it takes a full-width row. Not yet checked in a browser. - Composer with a conversation: the send button turns into the stop button while the agent answers.
github-actions Bot
pushed a commit
to GetStream/stream-chat-react
that referenced
this pull request
Nov 11, 2026
## [15.0.1](v15.0.0...v15.0.1) (2026-11-11) ### Bug Fixes * **AIComponents:** add AIConversationController for agent conversations ([#3428](#3428)) ([b6793a2](b6793a2)), closes [GetStream/stream-chat-react-native#3974](GetStream/stream-chat-react-native#3974) * **AIMessageComposer:** add Actions, StopButton and an inline layout ([#3424](#3424)) ([b65bfa6](b65bfa6)), closes [GetStream/getstream.io#1034](https://github.com/GetStream/getstream.io/issues/1034) * **styling:** drop [@charset](https://github.com/charset) from the published CSS ([#3430](#3430)) ([be9a3aa](be9a3aa)), closes [#3423](#3423) * **VirtualizedMessageList:** clear a jump's highlight once the message was viewed ([#3420](#3420)) ([d2c1796](d2c1796))
github-actions Bot
pushed a commit
to GetStream/stream-chat-react
that referenced
this pull request
Nov 11, 2026
## [14.17.0](v14.16.1...v14.17.0) (2026-11-11) ### Bug Fixes * **AIComponents:** add AIConversationController for agent conversations (v14) ([#3429](#3429)) ([1b67c7b](1b67c7b)), closes [#3428](#3428) [GetStream/stream-chat-react-native#3974](GetStream/stream-chat-react-native#3974) * **styling:** drop [@charset](https://github.com/charset) from the published CSS (v14) ([#3431](#3431)) ([6d0cf05](6d0cf05)), closes [#3423](#3423) ### Features * **AIMessageComposer:** add Actions, StopButton and an inline layout (v14) ([#3425](#3425)) ([cbc9f80](cbc9f80)), closes [#3424](#3424) [#3424](#3424) [#3424](#3424) [GetStream/getstream.io#1034](https://github.com/GetStream/getstream.io/issues/1034)
4 of 6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket: https://linear.app/stream/issue/RN-485
🎯 Goal
Fixes from comparing the AI components with React (
release-v14), iOS and Android. Only what every other SDK already does, or what's a bug, not parity for its own sake. One commit per change.🛠 Implementation details
AIConversationController.loadOlderMessages()swallowed errors, so a failing load was retried every 300 ms. It now rejects, and the list waits 3 s.skipAnimation.maxAttemptsbelow 1 throws.capabilities().toolsare MCP-shaped and left out when there are none;showsUI/uiFailed, and the host hashasFailed().argumentsPreviewon the approver and the card.stream-app-catalogid;HH:mm.Monospace→monospace, so code is actually monospace;Kept as is on purpose: closing the sign-in browser still declines the call, since the router's sign-in page opens only once.
🎨 UI Changes
🧪 Testing
yarn lint,yarn typecheck,yarn test:unitandyarn buildpass.ExpoChatGPTSampleagainst the agents router: A2UI replies, streaming, placeholder;ExpoCrewAssistanton the hosted router: approval arguments, decline;☑️ Checklist
developbranch (targetsv9-release, where the agent components live)