stream-chat-react-native-core 9.13.0 → 9.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/ai-components/conversation/package.json +6 -0
  2. package/lib/commonjs/a2ui/A2UISurface.js +38 -13
  3. package/lib/commonjs/a2ui/A2UISurface.js.map +1 -1
  4. package/lib/commonjs/ai-components/README.md +10 -1
  5. package/lib/commonjs/ai-components/conversation/AIConversation.js +17 -9
  6. package/lib/commonjs/ai-components/conversation/AIConversation.js.map +1 -1
  7. package/lib/commonjs/ai-components/conversation/index.js +0 -33
  8. package/lib/commonjs/ai-components/conversation/index.js.map +1 -1
  9. package/lib/commonjs/ai-components/conversation/list/useKeyboardOverlap.js +3 -4
  10. package/lib/commonjs/ai-components/conversation/list/useKeyboardOverlap.js.map +1 -1
  11. package/lib/commonjs/ai-components/conversation/useConversationRows.js.map +1 -1
  12. package/lib/commonjs/ai-components/conversation-state/controller/AIAgentError.js.map +1 -0
  13. package/lib/{module/ai-components/conversation → commonjs/ai-components/conversation-state}/controller/AIConversationController.js +7 -1
  14. package/lib/commonjs/ai-components/conversation-state/controller/AIConversationController.js.map +1 -0
  15. package/lib/commonjs/ai-components/conversation-state/controller/AIConversationExchange.js.map +1 -0
  16. package/lib/commonjs/ai-components/conversation-state/controller/index.js.map +1 -0
  17. package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/offers.js +10 -4
  18. package/lib/commonjs/ai-components/conversation-state/controller/offers.js.map +1 -0
  19. package/lib/commonjs/ai-components/conversation-state/controller/types.js.map +1 -0
  20. package/lib/commonjs/ai-components/conversation-state/controller/useAIConversationState.js.map +1 -0
  21. package/lib/commonjs/ai-components/{conversation → conversation-state}/fromStreamMessage.js +1 -0
  22. package/lib/commonjs/ai-components/conversation-state/fromStreamMessage.js.map +1 -0
  23. package/lib/commonjs/ai-components/conversation-state/index.js +37 -0
  24. package/lib/commonjs/ai-components/conversation-state/index.js.map +1 -0
  25. package/lib/commonjs/ai-components/conversation-state/types.js.map +1 -0
  26. package/lib/commonjs/ai-components/generative-ui/AIUIEntrance.js +9 -5
  27. package/lib/commonjs/ai-components/generative-ui/AIUIEntrance.js.map +1 -1
  28. package/lib/commonjs/ai-components/index.js +4 -4
  29. package/lib/commonjs/ai-components/index.js.map +1 -1
  30. package/lib/commonjs/ai-components/internal/keyboard.js +40 -0
  31. package/lib/commonjs/ai-components/internal/keyboard.js.map +1 -0
  32. package/lib/commonjs/ai-components/message/AIAssistantMessage.js +1 -0
  33. package/lib/commonjs/ai-components/message/AIAssistantMessage.js.map +1 -1
  34. package/lib/commonjs/ai-components/message-composer/ActionButton.js +2 -1
  35. package/lib/commonjs/ai-components/message-composer/ActionButton.js.map +1 -1
  36. package/lib/commonjs/ai-components/message-composer/AttachmentButton.js +2 -2
  37. package/lib/commonjs/ai-components/message-composer/AttachmentButton.js.map +1 -1
  38. package/lib/commonjs/ai-components/message-composer/ComposerView.js +20 -10
  39. package/lib/commonjs/ai-components/message-composer/ComposerView.js.map +1 -1
  40. package/lib/commonjs/ai-components/message-composer/EditingMessageChip.js +9 -8
  41. package/lib/commonjs/ai-components/message-composer/EditingMessageChip.js.map +1 -1
  42. package/lib/commonjs/ai-components/message-parts/AIMessageParts.js +25 -2
  43. package/lib/commonjs/ai-components/message-parts/AIMessageParts.js.map +1 -1
  44. package/lib/commonjs/i18n/ar.json +1 -0
  45. package/lib/commonjs/i18n/en.json +1 -0
  46. package/lib/commonjs/i18n/es.json +1 -0
  47. package/lib/commonjs/i18n/fr.json +1 -0
  48. package/lib/commonjs/i18n/he.json +1 -0
  49. package/lib/commonjs/i18n/hi.json +1 -0
  50. package/lib/commonjs/i18n/it.json +1 -0
  51. package/lib/commonjs/i18n/ja.json +1 -0
  52. package/lib/commonjs/i18n/ko.json +1 -0
  53. package/lib/commonjs/i18n/nl.json +1 -0
  54. package/lib/commonjs/i18n/pt-br.json +1 -0
  55. package/lib/commonjs/i18n/ru.json +1 -0
  56. package/lib/commonjs/i18n/tr.json +1 -0
  57. package/lib/commonjs/version.json +1 -1
  58. package/lib/module/a2ui/A2UISurface.js +38 -13
  59. package/lib/module/a2ui/A2UISurface.js.map +1 -1
  60. package/lib/module/ai-components/README.md +10 -1
  61. package/lib/module/ai-components/conversation/AIConversation.js +17 -9
  62. package/lib/module/ai-components/conversation/AIConversation.js.map +1 -1
  63. package/lib/module/ai-components/conversation/index.js +0 -33
  64. package/lib/module/ai-components/conversation/index.js.map +1 -1
  65. package/lib/module/ai-components/conversation/list/useKeyboardOverlap.js +3 -4
  66. package/lib/module/ai-components/conversation/list/useKeyboardOverlap.js.map +1 -1
  67. package/lib/module/ai-components/conversation/useConversationRows.js.map +1 -1
  68. package/lib/module/ai-components/conversation-state/controller/AIAgentError.js.map +1 -0
  69. package/lib/{commonjs/ai-components/conversation → module/ai-components/conversation-state}/controller/AIConversationController.js +7 -1
  70. package/lib/module/ai-components/conversation-state/controller/AIConversationController.js.map +1 -0
  71. package/lib/module/ai-components/conversation-state/controller/AIConversationExchange.js.map +1 -0
  72. package/lib/module/ai-components/conversation-state/controller/index.js.map +1 -0
  73. package/lib/module/ai-components/{conversation → conversation-state}/controller/offers.js +10 -4
  74. package/lib/module/ai-components/conversation-state/controller/offers.js.map +1 -0
  75. package/lib/module/ai-components/conversation-state/controller/types.js.map +1 -0
  76. package/lib/module/ai-components/conversation-state/controller/useAIConversationState.js.map +1 -0
  77. package/lib/module/ai-components/{conversation → conversation-state}/fromStreamMessage.js +1 -0
  78. package/lib/module/ai-components/conversation-state/fromStreamMessage.js.map +1 -0
  79. package/lib/module/ai-components/conversation-state/index.js +37 -0
  80. package/lib/module/ai-components/conversation-state/index.js.map +1 -0
  81. package/lib/module/ai-components/conversation-state/types.js.map +1 -0
  82. package/lib/module/ai-components/generative-ui/AIUIEntrance.js +9 -5
  83. package/lib/module/ai-components/generative-ui/AIUIEntrance.js.map +1 -1
  84. package/lib/module/ai-components/index.js +4 -4
  85. package/lib/module/ai-components/index.js.map +1 -1
  86. package/lib/module/ai-components/internal/keyboard.js +40 -0
  87. package/lib/module/ai-components/internal/keyboard.js.map +1 -0
  88. package/lib/module/ai-components/message/AIAssistantMessage.js +1 -0
  89. package/lib/module/ai-components/message/AIAssistantMessage.js.map +1 -1
  90. package/lib/module/ai-components/message-composer/ActionButton.js +2 -1
  91. package/lib/module/ai-components/message-composer/ActionButton.js.map +1 -1
  92. package/lib/module/ai-components/message-composer/AttachmentButton.js +2 -2
  93. package/lib/module/ai-components/message-composer/AttachmentButton.js.map +1 -1
  94. package/lib/module/ai-components/message-composer/ComposerView.js +20 -10
  95. package/lib/module/ai-components/message-composer/ComposerView.js.map +1 -1
  96. package/lib/module/ai-components/message-composer/EditingMessageChip.js +9 -8
  97. package/lib/module/ai-components/message-composer/EditingMessageChip.js.map +1 -1
  98. package/lib/module/ai-components/message-parts/AIMessageParts.js +25 -2
  99. package/lib/module/ai-components/message-parts/AIMessageParts.js.map +1 -1
  100. package/lib/module/i18n/ar.json +1 -0
  101. package/lib/module/i18n/en.json +1 -0
  102. package/lib/module/i18n/es.json +1 -0
  103. package/lib/module/i18n/fr.json +1 -0
  104. package/lib/module/i18n/he.json +1 -0
  105. package/lib/module/i18n/hi.json +1 -0
  106. package/lib/module/i18n/it.json +1 -0
  107. package/lib/module/i18n/ja.json +1 -0
  108. package/lib/module/i18n/ko.json +1 -0
  109. package/lib/module/i18n/nl.json +1 -0
  110. package/lib/module/i18n/pt-br.json +1 -0
  111. package/lib/module/i18n/ru.json +1 -0
  112. package/lib/module/i18n/tr.json +1 -0
  113. package/lib/module/version.json +1 -1
  114. package/lib/typescript/a2ui/A2UISurface.d.ts.map +1 -1
  115. package/lib/typescript/ai-components/conversation/AIConversation.d.ts +7 -4
  116. package/lib/typescript/ai-components/conversation/AIConversation.d.ts.map +1 -1
  117. package/lib/typescript/ai-components/conversation/entries.d.ts +1 -1
  118. package/lib/typescript/ai-components/conversation/entries.d.ts.map +1 -1
  119. package/lib/typescript/ai-components/conversation/index.d.ts +0 -3
  120. package/lib/typescript/ai-components/conversation/index.d.ts.map +1 -1
  121. package/lib/typescript/ai-components/conversation/list/useKeyboardOverlap.d.ts.map +1 -1
  122. package/lib/typescript/ai-components/conversation/useConversationRows.d.ts +1 -1
  123. package/lib/typescript/ai-components/conversation/useConversationRows.d.ts.map +1 -1
  124. package/lib/typescript/ai-components/conversation-state/controller/AIAgentError.d.ts.map +1 -0
  125. package/lib/typescript/ai-components/conversation-state/controller/AIConversationController.d.ts.map +1 -0
  126. package/lib/typescript/ai-components/conversation-state/controller/AIConversationExchange.d.ts.map +1 -0
  127. package/lib/typescript/ai-components/conversation-state/controller/index.d.ts.map +1 -0
  128. package/lib/typescript/ai-components/{conversation → conversation-state}/controller/offers.d.ts +4 -2
  129. package/lib/typescript/ai-components/conversation-state/controller/offers.d.ts.map +1 -0
  130. package/lib/typescript/ai-components/conversation-state/controller/types.d.ts.map +1 -0
  131. package/lib/typescript/ai-components/conversation-state/controller/useAIConversationState.d.ts.map +1 -0
  132. package/lib/typescript/ai-components/conversation-state/fromStreamMessage.d.ts.map +1 -0
  133. package/lib/typescript/ai-components/conversation-state/index.d.ts +4 -0
  134. package/lib/typescript/ai-components/conversation-state/index.d.ts.map +1 -0
  135. package/lib/typescript/ai-components/conversation-state/types.d.ts.map +1 -0
  136. package/lib/typescript/ai-components/generative-ui/AIUIEntrance.d.ts +4 -3
  137. package/lib/typescript/ai-components/generative-ui/AIUIEntrance.d.ts.map +1 -1
  138. package/lib/typescript/ai-components/generative-ui/AIUIHost.d.ts +1 -1
  139. package/lib/typescript/ai-components/generative-ui/AIUIHost.d.ts.map +1 -1
  140. package/lib/typescript/ai-components/index.d.ts +1 -1
  141. package/lib/typescript/ai-components/index.d.ts.map +1 -1
  142. package/lib/typescript/ai-components/internal/keyboard.d.ts +9 -0
  143. package/lib/typescript/ai-components/internal/keyboard.d.ts.map +1 -0
  144. package/lib/typescript/ai-components/message/AIAssistantMessage.d.ts.map +1 -1
  145. package/lib/typescript/ai-components/message-composer/ActionButton.d.ts +1 -1
  146. package/lib/typescript/ai-components/message-composer/ActionButton.d.ts.map +1 -1
  147. package/lib/typescript/ai-components/message-composer/ComposerView.d.ts +10 -5
  148. package/lib/typescript/ai-components/message-composer/ComposerView.d.ts.map +1 -1
  149. package/lib/typescript/ai-components/message-parts/AIMessageParts.d.ts +3 -1
  150. package/lib/typescript/ai-components/message-parts/AIMessageParts.d.ts.map +1 -1
  151. package/lib/typescript/i18n/ar.json +1 -0
  152. package/lib/typescript/i18n/en.json +1 -0
  153. package/lib/typescript/i18n/es.json +1 -0
  154. package/lib/typescript/i18n/fr.json +1 -0
  155. package/lib/typescript/i18n/he.json +1 -0
  156. package/lib/typescript/i18n/hi.json +1 -0
  157. package/lib/typescript/i18n/it.json +1 -0
  158. package/lib/typescript/i18n/ja.json +1 -0
  159. package/lib/typescript/i18n/ko.json +1 -0
  160. package/lib/typescript/i18n/nl.json +1 -0
  161. package/lib/typescript/i18n/pt-br.json +1 -0
  162. package/lib/typescript/i18n/ru.json +1 -0
  163. package/lib/typescript/i18n/tr.json +1 -0
  164. package/lib/typescript/utils/i18n/Streami18n.d.ts +1 -0
  165. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  166. package/package.json +1 -1
  167. package/src/a2ui/A2UISurface.tsx +36 -7
  168. package/src/a2ui/__tests__/A2UIRenderer.test.tsx +45 -0
  169. package/src/ai-components/README.md +10 -1
  170. package/src/ai-components/__tests__/ComposerView.test.tsx +21 -1
  171. package/src/ai-components/__tests__/conversation.test.tsx +85 -3
  172. package/src/ai-components/__tests__/conversationExchange.test.ts +1 -1
  173. package/src/ai-components/__tests__/conversationRows.test.ts +1 -1
  174. package/src/ai-components/__tests__/conversationUI.test.tsx +21 -1
  175. package/src/ai-components/__tests__/messagePartViews.test.tsx +42 -0
  176. package/src/ai-components/__tests__/withoutKeyboardController.test.tsx +60 -0
  177. package/src/ai-components/conversation/AIConversation.tsx +37 -15
  178. package/src/ai-components/conversation/entries.ts +1 -1
  179. package/src/ai-components/conversation/index.ts +0 -3
  180. package/src/ai-components/conversation/list/useKeyboardOverlap.ts +3 -2
  181. package/src/ai-components/conversation/useConversationRows.ts +2 -1
  182. package/src/ai-components/{conversation → conversation-state}/controller/AIConversationController.ts +7 -2
  183. package/src/ai-components/{conversation → conversation-state}/controller/offers.ts +13 -3
  184. package/src/ai-components/{conversation → conversation-state}/fromStreamMessage.ts +3 -1
  185. package/src/ai-components/conversation-state/index.ts +3 -0
  186. package/src/ai-components/generative-ui/AIUIEntrance.tsx +11 -5
  187. package/src/ai-components/generative-ui/AIUIHost.ts +1 -1
  188. package/src/ai-components/index.ts +1 -1
  189. package/src/ai-components/internal/keyboard.tsx +34 -0
  190. package/src/ai-components/message/AIAssistantMessage.tsx +1 -0
  191. package/src/ai-components/message-composer/ActionButton.tsx +5 -4
  192. package/src/ai-components/message-composer/AttachmentButton.tsx +2 -2
  193. package/src/ai-components/message-composer/ComposerView.tsx +25 -11
  194. package/src/ai-components/message-composer/EditingMessageChip.tsx +8 -7
  195. package/src/ai-components/message-parts/AIMessageParts.tsx +28 -3
  196. package/src/i18n/ar.json +1 -0
  197. package/src/i18n/en.json +1 -0
  198. package/src/i18n/es.json +1 -0
  199. package/src/i18n/fr.json +1 -0
  200. package/src/i18n/he.json +1 -0
  201. package/src/i18n/hi.json +1 -0
  202. package/src/i18n/it.json +1 -0
  203. package/src/i18n/ja.json +1 -0
  204. package/src/i18n/ko.json +1 -0
  205. package/src/i18n/nl.json +1 -0
  206. package/src/i18n/pt-br.json +1 -0
  207. package/src/i18n/ru.json +1 -0
  208. package/src/i18n/tr.json +1 -0
  209. package/src/version.json +1 -1
  210. package/lib/commonjs/ai-components/conversation/controller/AIAgentError.js.map +0 -1
  211. package/lib/commonjs/ai-components/conversation/controller/AIConversationController.js.map +0 -1
  212. package/lib/commonjs/ai-components/conversation/controller/AIConversationExchange.js.map +0 -1
  213. package/lib/commonjs/ai-components/conversation/controller/index.js.map +0 -1
  214. package/lib/commonjs/ai-components/conversation/controller/offers.js.map +0 -1
  215. package/lib/commonjs/ai-components/conversation/controller/types.js.map +0 -1
  216. package/lib/commonjs/ai-components/conversation/controller/useAIConversationState.js.map +0 -1
  217. package/lib/commonjs/ai-components/conversation/fromStreamMessage.js.map +0 -1
  218. package/lib/commonjs/ai-components/conversation/types.js.map +0 -1
  219. package/lib/module/ai-components/conversation/controller/AIAgentError.js.map +0 -1
  220. package/lib/module/ai-components/conversation/controller/AIConversationController.js.map +0 -1
  221. package/lib/module/ai-components/conversation/controller/AIConversationExchange.js.map +0 -1
  222. package/lib/module/ai-components/conversation/controller/index.js.map +0 -1
  223. package/lib/module/ai-components/conversation/controller/offers.js.map +0 -1
  224. package/lib/module/ai-components/conversation/controller/types.js.map +0 -1
  225. package/lib/module/ai-components/conversation/controller/useAIConversationState.js.map +0 -1
  226. package/lib/module/ai-components/conversation/fromStreamMessage.js.map +0 -1
  227. package/lib/module/ai-components/conversation/types.js.map +0 -1
  228. package/lib/typescript/ai-components/conversation/controller/AIAgentError.d.ts.map +0 -1
  229. package/lib/typescript/ai-components/conversation/controller/AIConversationController.d.ts.map +0 -1
  230. package/lib/typescript/ai-components/conversation/controller/AIConversationExchange.d.ts.map +0 -1
  231. package/lib/typescript/ai-components/conversation/controller/index.d.ts.map +0 -1
  232. package/lib/typescript/ai-components/conversation/controller/offers.d.ts.map +0 -1
  233. package/lib/typescript/ai-components/conversation/controller/types.d.ts.map +0 -1
  234. package/lib/typescript/ai-components/conversation/controller/useAIConversationState.d.ts.map +0 -1
  235. package/lib/typescript/ai-components/conversation/fromStreamMessage.d.ts.map +0 -1
  236. package/lib/typescript/ai-components/conversation/types.d.ts.map +0 -1
  237. /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/AIAgentError.js +0 -0
  238. /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/AIConversationExchange.js +0 -0
  239. /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/index.js +0 -0
  240. /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/types.js +0 -0
  241. /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/useAIConversationState.js +0 -0
  242. /package/lib/commonjs/ai-components/{conversation → conversation-state}/types.js +0 -0
  243. /package/lib/module/ai-components/{conversation → conversation-state}/controller/AIAgentError.js +0 -0
  244. /package/lib/module/ai-components/{conversation → conversation-state}/controller/AIConversationExchange.js +0 -0
  245. /package/lib/module/ai-components/{conversation → conversation-state}/controller/index.js +0 -0
  246. /package/lib/module/ai-components/{conversation → conversation-state}/controller/types.js +0 -0
  247. /package/lib/module/ai-components/{conversation → conversation-state}/controller/useAIConversationState.js +0 -0
  248. /package/lib/module/ai-components/{conversation → conversation-state}/types.js +0 -0
  249. /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/AIAgentError.d.ts +0 -0
  250. /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/AIConversationController.d.ts +0 -0
  251. /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/AIConversationExchange.d.ts +0 -0
  252. /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/index.d.ts +0 -0
  253. /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/types.d.ts +0 -0
  254. /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/useAIConversationState.d.ts +0 -0
  255. /package/lib/typescript/ai-components/{conversation → conversation-state}/fromStreamMessage.d.ts +0 -0
  256. /package/lib/typescript/ai-components/{conversation → conversation-state}/types.d.ts +0 -0
  257. /package/src/ai-components/{conversation → conversation-state}/controller/AIAgentError.ts +0 -0
  258. /package/src/ai-components/{conversation → conversation-state}/controller/AIConversationExchange.ts +0 -0
  259. /package/src/ai-components/{conversation → conversation-state}/controller/index.ts +0 -0
  260. /package/src/ai-components/{conversation → conversation-state}/controller/types.ts +0 -0
  261. /package/src/ai-components/{conversation → conversation-state}/controller/useAIConversationState.ts +0 -0
  262. /package/src/ai-components/{conversation → conversation-state}/types.ts +0 -0
@@ -6,6 +6,7 @@ import React, {
6
6
  useContext,
7
7
  useLayoutEffect,
8
8
  useMemo,
9
+ useState,
9
10
  useSyncExternalStore,
10
11
  } from 'react';
11
12
  import { type AccessibilityProps, View as RNView, type ViewProps } from 'react-native';
@@ -147,7 +148,8 @@ const PlacedComponent = ({
147
148
  /**
148
149
  * One part of a surface: its view from the basic catalog, or the app's component of its type,
149
150
  * coming in when it shows, with its weight and what the agent tells assistive technology. A part
150
- * whose type no catalog has, or that is still on its way, shows nothing.
151
+ * whose type no catalog has, or that is still on its way, shows nothing, and one that holds other
152
+ * parts, such as a card, shows once one of them does while the agent writes it.
151
153
  */
152
154
  // The path of the data model a TextField writes what the person types to, when it is obscured,
153
155
  // such as a password, which must never reach the agent.
@@ -161,6 +163,18 @@ const secretPathOf = (node: ComponentNode, resolved: Record<string, unknown>) =>
161
163
  : undefined;
162
164
  };
163
165
 
166
+ // Tells the part around a part that it shows.
167
+ const A2UIPartShowsContext = createContext<(() => void) | undefined>(undefined);
168
+
169
+ // Whether a part's props hold other parts, as a card's child or a column's children do.
170
+ const holdsParts = (value: unknown): boolean =>
171
+ isComponentNode(value) ||
172
+ (Array.isArray(value)
173
+ ? value.some(holdsParts)
174
+ : isPlainObject(value) && Object.values(value).some(holdsParts));
175
+
176
+ const hiddenStyle = { display: 'none' } as const;
177
+
164
178
  const A2UINode = memo(({ node }: { node: ComponentNode }) => {
165
179
  const engine = useContext(A2UIEngineContext);
166
180
  const axis = useContext(A2UIAxisContext);
@@ -172,11 +186,26 @@ const A2UINode = memo(({ node }: { node: ComponentNode }) => {
172
186
  useLayoutEffect(() => {
173
187
  if (secretPath && surface) keepSecretPath(surface, secretPath);
174
188
  }, [secretPath, surface]);
175
- const comesIn = useAIUIComesIn();
176
- if (!engine || node.isPlaceholder || node.state !== 'resolved') return null;
177
- const View = engine.components[node.type];
178
- const isApp = !View && !!engine.appComponent(node.type);
179
- if (!View && !isApp) return null;
189
+ const View = engine?.components[node.type];
190
+ const isApp = !View && !!engine?.appComponent(node.type);
191
+ const isReady = !!engine && !node.isPlaceholder && node.state === 'resolved' && (!!View || isApp);
192
+ const holds = useMemo(() => holdsParts(props), [props]);
193
+ const [hasShownPart, setHasShownPart] = useState(false);
194
+ const isWritten = !useContext(A2UIReplyContext)?.isStreaming;
195
+ const shows = isReady && (!holds || hasShownPart || isWritten);
196
+ const partShows = useContext(A2UIPartShowsContext);
197
+ useLayoutEffect(() => {
198
+ if (shows) partShows?.();
199
+ }, [partShows, shows]);
200
+ const onPartShown = useCallback(() => setHasShownPart(true), []);
201
+ const comesIn = useAIUIComesIn(shows);
202
+ if (!isReady) return null;
203
+ const content = (
204
+ <A2UIPartShowsContext.Provider value={onPartShown}>
205
+ {View ? <View node={node} props={props} /> : <PlacedComponent node={node} props={props} />}
206
+ </A2UIPartShowsContext.Provider>
207
+ );
208
+ if (!shows) return <RNView style={hiddenStyle}>{content}</RNView>;
180
209
  // A weighted part shares the room its row leaves, as flex-grow from nothing does.
181
210
  const weight = typeof props.weight === 'number' && axis === 'row' ? props.weight : undefined;
182
211
  const Part = comesIn ? EnteringPart : ShownPart;
@@ -186,7 +215,7 @@ const A2UINode = memo(({ node }: { node: ComponentNode }) => {
186
215
  style={weight !== undefined ? { flexBasis: 0, flexGrow: weight } : undefined}
187
216
  testID={`a2ui-${node.componentId}`}
188
217
  >
189
- {View ? <View node={node} props={props} /> : <PlacedComponent node={node} props={props} />}
218
+ {content}
190
219
  </Part>
191
220
  );
192
221
  });
@@ -427,6 +427,51 @@ describe('a reply written in A2UI', () => {
427
427
  await waitFor(() => expect(screen.getByTestId('trip-map')).toBeTruthy());
428
428
  });
429
429
 
430
+ it('holds back a card until something in it shows, while the agent writes it', async () => {
431
+ const { ui } = setup();
432
+ const view = (lines: string[], status = 'streaming') => (
433
+ <StreamTheme>
434
+ <AIUIView document={documentOf(lines, status)} host={ui} messageId='a1' />
435
+ </StreamTheme>
436
+ );
437
+ const written = [
438
+ { children: ['title', 'card'], component: 'Column', id: 'root' },
439
+ { component: 'Text', id: 'title', text: 'Fit check' },
440
+ { child: 'list', component: 'Card', id: 'card' },
441
+ { children: ['tag'], component: 'Column', id: 'list' },
442
+ ];
443
+ const { rerender } = render(view(surfaceOf(written)));
444
+ await waitFor(() => expect(screen.getByTestId('a2ui-title')).toBeTruthy());
445
+ expect(screen.queryByTestId('a2ui-card')).toBeNull();
446
+ expect(screen.queryByTestId('a2ui-list')).toBeNull();
447
+
448
+ rerender(
449
+ view([
450
+ ...surfaceOf(written),
451
+ line({
452
+ updateComponents: {
453
+ components: [{ component: 'Text', id: 'tag', text: 'Name tag' }],
454
+ surfaceId: 's',
455
+ },
456
+ }),
457
+ ]),
458
+ );
459
+ await waitFor(() => expect(screen.getByTestId('a2ui-card')).toBeTruthy());
460
+ expect(screen.getByText('Name tag')).toBeTruthy();
461
+ });
462
+
463
+ it('shows a card that holds nothing yet once the agent has written the reply', async () => {
464
+ const { ui } = setup();
465
+ await renderReply(
466
+ ui,
467
+ surfaceOf([
468
+ { child: 'list', component: 'Card', id: 'root' },
469
+ { children: ['tag'], component: 'Column', id: 'list' },
470
+ ]),
471
+ );
472
+ expect(screen.getByTestId('a2ui-root')).toBeTruthy();
473
+ });
474
+
430
475
  it('shows a placeholder while the first surface is being written', async () => {
431
476
  const { ui } = setup();
432
477
  await renderReply(ui, ['{"version":"1.0","createSur'], 'streaming');
@@ -36,6 +36,14 @@ yarn add victory-native @shopify/react-native-skia
36
36
 
37
37
  `react-native-reanimated`, `react-native-gesture-handler` and `react-native-svg` are already required by the chat SDK.
38
38
 
39
+ `AIConversation` comes from its own entry point, `stream-chat-react-native/ai-components/conversation` (or `stream-chat-expo/ai-components/conversation`), and needs [`react-native-keyboard-controller`](https://kirillzyusko.github.io/react-native-keyboard-controller/) with its `KeyboardProvider` above it:
40
+
41
+ ```bash
42
+ yarn add react-native-keyboard-controller
43
+ ```
44
+
45
+ The rest of the AI components work without it. With it installed and a `KeyboardProvider` above them, `ComposerView` rides the keyboard up; without it, the composer doesn't move with the keyboard.
46
+
39
47
  The `zod` dependency ships `export * as` syntax, which the React Native Babel preset does not transform, so add the `export-namespace-from` plugin to your `babel.config.js`:
40
48
 
41
49
  ```bash
@@ -299,7 +307,8 @@ An agent can answer with UI instead of text: a form, a list of options, a card w
299
307
 
300
308
  ```tsx
301
309
  import { createA2UIRenderer } from 'stream-chat-react-native/a2ui'; // or 'stream-chat-expo/a2ui'
302
- import { AIConversation, AIUIHost, aiUIActionJSON } from 'stream-chat-react-native/ai-components';
310
+ import { AIUIHost, aiUIActionJSON } from 'stream-chat-react-native/ai-components';
311
+ import { AIConversation } from 'stream-chat-react-native/ai-components/conversation';
303
312
 
304
313
  const Conversation = ({ messages, session }) => {
305
314
  // One host per conversation, kept for as long as it shows, so what a reply's UI holds (a
@@ -238,6 +238,26 @@ describe('ComposerView trailing', () => {
238
238
  });
239
239
  });
240
240
 
241
+ describe('ComposerView SpeechToTextButton', () => {
242
+ const Dictate = () => <Text>dictate</Text>;
243
+
244
+ it('shows the one it is given while the message box is empty, and the send button after', () => {
245
+ renderComposer({ SpeechToTextButton: Dictate });
246
+ expect(screen.getByText('dictate')).toBeTruthy();
247
+
248
+ fireEvent.changeText(screen.getByPlaceholderText('Ask anything'), 'Hello');
249
+ expect(screen.queryByText('dictate')).toBeNull();
250
+ expect(screen.getByTestId('send-up')).toBeTruthy();
251
+ });
252
+
253
+ it('shows none with null', () => {
254
+ renderComposer({ SpeechToTextButton: null });
255
+ expect(screen.queryByText('dictate')).toBeNull();
256
+ expect(screen.queryByTestId('send-up')).toBeNull();
257
+ expect(screen.getByPlaceholderText('Ask anything')).toBeTruthy();
258
+ });
259
+ });
260
+
241
261
  describe('AISuggestions', () => {
242
262
  it('hands the picked suggestion over, and shows nothing without any', () => {
243
263
  const onSelect = jest.fn();
@@ -325,7 +345,7 @@ describe('ComposerView with a conversation', () => {
325
345
 
326
346
  act(() => controller.edit(message));
327
347
 
328
- expect(screen.getByText('Editing message')).toBeTruthy();
348
+ expect(screen.getByText('Editing')).toBeTruthy();
329
349
  expect(screen.getByPlaceholderText('Ask anything').props.value).toBe('Plan Lisbon');
330
350
 
331
351
  fireEvent.press(screen.getByRole('button', { name: 'Cancel editing' }));
@@ -1,17 +1,19 @@
1
1
  import React, { type ReactElement, useEffect } from 'react';
2
- import { Text } from 'react-native';
2
+ import { StyleSheet, Text } from 'react-native';
3
3
 
4
4
  import { act, fireEvent, render, screen } from '@testing-library/react-native';
5
5
 
6
- import { AIPendingMessageTracker, turnsOf } from '../conversation/entries';
7
6
  import {
8
7
  AIConversation,
9
8
  type AIConversationAssistantMessageProps,
9
+ type AIConversationUserMessageProps,
10
+ } from '../conversation';
11
+ import { AIPendingMessageTracker, turnsOf } from '../conversation/entries';
12
+ import {
10
13
  AIConversationController,
11
14
  type AIConversationControllerOptions,
12
15
  type AIConversationFeed,
13
16
  type AIConversationTransport,
14
- type AIConversationUserMessageProps,
15
17
  type AIMessage,
16
18
  type AIToolApprover,
17
19
  createFromStreamMessage,
@@ -337,6 +339,44 @@ describe('AIConversation', () => {
337
339
  expect(screen.getByText('actions')).toBeTruthy();
338
340
  });
339
341
 
342
+ it('keeps the latest reply at its tallest, and lets the person’s message shrink', async () => {
343
+ const UserMessage = ({ text }: AIConversationUserMessageProps) => <Text>{text}</Text>;
344
+ const AssistantMessage = ({ text }: AIConversationAssistantMessageProps) => <Text>{text}</Text>;
345
+ await renderConversation(
346
+ <AIConversation
347
+ AssistantMessage={AssistantMessage}
348
+ messages={[user('u1', 'Hi'), reply('a1', 'Hello')]}
349
+ ReplyActions={null}
350
+ UserMessage={UserMessage}
351
+ />,
352
+ );
353
+ // The row is the view around the message with the list's side and top gaps.
354
+ const rowOf = (text: string) => {
355
+ let node = screen.getByText(text).parent;
356
+ while (node) {
357
+ const style = typeof node.type === 'string' ? StyleSheet.flatten(node.props.style) : null;
358
+ if (style?.paddingHorizontal !== undefined && style.paddingTop !== undefined) return node;
359
+ node = node.parent;
360
+ }
361
+ return null;
362
+ };
363
+ const minHeightOf = (text: string) =>
364
+ StyleSheet.flatten(rowOf(text)?.props.style)?.minHeight ?? 0;
365
+ const layOutRow = (text: string, height: number) =>
366
+ act(() => {
367
+ rowOf(text)?.props.onLayout?.(layoutOf(height));
368
+ });
369
+
370
+ layOutRow('Hello', 120);
371
+ layOutRow('Hello', 60);
372
+ expect(minHeightOf('Hello')).toBe(120);
373
+
374
+ layOutRow('Hi', 120);
375
+ layOutRow('Hi', 60);
376
+ expect(rowOf('Hi')?.props.onLayout).toBeUndefined();
377
+ expect(minHeightOf('Hi')).toBe(0);
378
+ });
379
+
340
380
  it('renders again only the messages that changed', async () => {
341
381
  const renders: string[] = [];
342
382
  const AssistantMessage = ({ message, text }: AIConversationAssistantMessageProps) => {
@@ -541,6 +581,27 @@ describe('fromStreamMessage', () => {
541
581
  expect(reasoningText).toHaveBeenCalledWith('m1', 'r1');
542
582
  });
543
583
 
584
+ it('leaves out the link previews Stream made from the text', () => {
585
+ const message = fromStreamMessage({
586
+ ai_generated: true,
587
+ attachments: [
588
+ {
589
+ image_url: 'https://cal.com/og/index.png',
590
+ og_scrape_url: 'Cal.com',
591
+ title: 'Cal.com | Scheduling Software for Online Bookings',
592
+ title_link: 'https://cal.com',
593
+ type: 'image',
594
+ },
595
+ { image_url: 'https://cdn.example.com/a.jpg', title: 'a.jpg', type: 'image' },
596
+ ],
597
+ id: 'm1',
598
+ text: 'Book it on Cal.com',
599
+ });
600
+ expect(message.attachments).toEqual([
601
+ { kind: 'image', mimeType: undefined, name: 'a.jpg', url: 'https://cdn.example.com/a.jpg' },
602
+ ]);
603
+ });
604
+
544
605
  it('adapts the person’s message', () => {
545
606
  expect(fromStreamMessage({ attachments: [], id: 'u1', text: 'Hi' })).toMatchObject({
546
607
  isGenerating: false,
@@ -613,6 +674,27 @@ describe('AIConversation with a controller', () => {
613
674
  controller.dispose();
614
675
  });
615
676
 
677
+ it('leaves the other messages alone while a reply is written', async () => {
678
+ let renders = 0;
679
+ const UserMessage = ({ text }: AIConversationUserMessageProps) => {
680
+ renders++;
681
+ return <Text>{text}</Text>;
682
+ };
683
+ const history = [user('u1', 'Hello'), reply('a1', 'Hi!'), user('u2', 'Plan my day')];
684
+ const { controller, update } = conversationOf(history);
685
+ await renderConversation(
686
+ <AIConversation conversation={controller} UserMessage={UserMessage} />,
687
+ );
688
+ await update([...history, reply('a2', 'Here', { isGenerating: true })]);
689
+ const before = renders;
690
+ for (const text of ['Here is', 'Here is your', 'Here is your plan.']) {
691
+ await update([...history, reply('a2', text, { isGenerating: true })]);
692
+ }
693
+ expect(screen.getByText('Here is your plan.')).toBeTruthy();
694
+ expect(renders).toBe(before);
695
+ controller.dispose();
696
+ });
697
+
616
698
  it('shows the agent thinking while a reply is written again', async () => {
617
699
  const { controller } = conversationOf([user('u1', 'Hello'), reply('a1', 'Hi!')]);
618
700
  await renderConversation(<AIConversation conversation={controller} />);
@@ -1,5 +1,5 @@
1
1
  import type { AIConversationRequest, AIMessage } from '..';
2
- import { AIConversationExchange } from '../conversation/controller/AIConversationExchange';
2
+ import { AIConversationExchange } from '../conversation-state/controller/AIConversationExchange';
3
3
 
4
4
  const user = (id: string, text = 'Plan a weekend in Lisbon'): AIMessage => ({
5
5
  id,
@@ -1,11 +1,11 @@
1
1
  import { renderHook } from '@testing-library/react-native';
2
2
 
3
- import type { AIMessage, AIPendingMessage } from '../conversation/types';
4
3
  import {
5
4
  type ConversationRow,
6
5
  useConversationRows,
7
6
  type UseConversationRowsOptions,
8
7
  } from '../conversation/useConversationRows';
8
+ import type { AIMessage, AIPendingMessage } from '../conversation-state/types';
9
9
  import type { AIMessagePartData } from '../message-parts/types';
10
10
 
11
11
  const user = (id: string, text: string): AIMessage => ({ id, role: 'user', text });
@@ -3,8 +3,8 @@ import { Pressable, StyleSheet, Text, View } from 'react-native';
3
3
 
4
4
  import { act, fireEvent, render, screen } from '@testing-library/react-native';
5
5
 
6
+ import { AIConversation } from '../conversation';
6
7
  import {
7
- AIConversation,
8
8
  AIConversationController,
9
9
  type AIMessage,
10
10
  AIUIComponentView,
@@ -264,6 +264,26 @@ describe('AIConversation with generated UI', () => {
264
264
  expect(stageOpacityOf('Being written')).toBe(0);
265
265
  });
266
266
 
267
+ it('brings in a UI that arrives whole once a reply seen being written finishes', async () => {
268
+ const ui = makeHost();
269
+ const { rerender } = await renderConversation(
270
+ <AIConversation
271
+ messages={[user('u1', 'Plan'), reply('a1', 'Planning', undefined, { generating: true })]}
272
+ ui={ui}
273
+ />,
274
+ );
275
+ await rerender(
276
+ <AIConversation
277
+ messages={[
278
+ user('u1', 'Plan'),
279
+ reply('a1', '', { content: 'Arrived whole', format: 'a2ui' }),
280
+ ]}
281
+ ui={ui}
282
+ />,
283
+ );
284
+ expect(stageOpacityOf('Arrived whole')).toBe(0);
285
+ });
286
+
267
287
  it('hands the replies’ documents to the host as they change', async () => {
268
288
  const ui = makeHost();
269
289
  const receive = jest.spyOn(ui, 'receive');
@@ -18,6 +18,7 @@ import {
18
18
  StreamTheme,
19
19
  } from '../index';
20
20
  import { forgetCallStates } from '../message-parts/callStates';
21
+ import { AIThinkingClock, AIThinkingTurnContext } from '../reasoning/thinkingClock';
21
22
 
22
23
  // Each test's questions start unanswered, though tests reuse calls.
23
24
  afterEach(forgetCallStates);
@@ -326,6 +327,47 @@ describe('AIMessageParts', () => {
326
327
  version: 1,
327
328
  });
328
329
 
330
+ // Whether the step a view is of comes in: an entrance on the way up to the list of steps.
331
+ const comesIn = (testID: string) => {
332
+ let node = screen.getByTestId(testID).parent;
333
+ while (node && node.props.testID !== 'ai-message-parts') {
334
+ if (node.props.entering) return true;
335
+ node = node.parent;
336
+ }
337
+ return false;
338
+ };
339
+ const call = (id: string): AIMessagePartData => ({
340
+ id,
341
+ kind: 'ai_tool_call',
342
+ payload: {},
343
+ toolCall: toolCall({ id }),
344
+ version: 1,
345
+ });
346
+
347
+ it('brings in the steps of a reply being written, and shows those of one written before', () => {
348
+ const { rerender } = renderWithTheme(
349
+ <AIMessageParts isGenerating messageId='m1' parts={[call('c1')]} />,
350
+ );
351
+ expect(comesIn('ai-tool-call')).toBe(true);
352
+
353
+ rerender(<AIMessageParts messageId='m1' parts={[call('c1')]} />);
354
+ expect(comesIn('ai-tool-call')).toBe(true);
355
+
356
+ renderWithTheme(<AIMessageParts messageId='m2' parts={[reasoning('r1', 'Done')]} />);
357
+ expect(comesIn('ai-reasoning')).toBe(false);
358
+ });
359
+
360
+ it('shows at once the step that carries on the agent’s thinking', () => {
361
+ const clock = new AIThinkingClock();
362
+ clock.update('t1', true, false, false);
363
+ renderWithTheme(
364
+ <AIThinkingTurnContext.Provider value={{ clock, firstStepId: 'r1', key: 't1' }}>
365
+ <AIMessageParts isGenerating messageId='m1' parts={[reasoning('r1', 'Hmm')]} />
366
+ </AIThinkingTurnContext.Provider>,
367
+ );
368
+ expect(comesIn('ai-reasoning')).toBe(false);
369
+ });
370
+
329
371
  it('updates its views for another message’s steps, each starting afresh', () => {
330
372
  const { rerender } = renderWithTheme(
331
373
  <AIMessageParts messageId='m1' parts={[reasoning('r1', 'First')]} />,
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { Keyboard, Text } from 'react-native';
3
+
4
+ import { act, fireEvent, render, screen } from '@testing-library/react-native';
5
+
6
+ import { ComposerView, sheetStore, sheetStoreApi, StreamTheme } from '../index';
7
+ import { dismissKeyboard, KeyboardStickyView, keyboardHeight } from '../internal/keyboard';
8
+
9
+ jest.mock('react-native-keyboard-controller', () => {
10
+ throw new Error("Cannot find module 'react-native-keyboard-controller'");
11
+ });
12
+
13
+ const flushPromises = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
14
+
15
+ describe('without react-native-keyboard-controller', () => {
16
+ afterEach(() => {
17
+ act(() => {
18
+ sheetStoreApi.closeSheet();
19
+ });
20
+ jest.restoreAllMocks();
21
+ });
22
+
23
+ it('lays the sticky view out as a plain view', () => {
24
+ render(
25
+ <KeyboardStickyView offset={{ closed: 0, opened: 34 }} testID='sticky'>
26
+ <Text>inside</Text>
27
+ </KeyboardStickyView>,
28
+ );
29
+
30
+ const sticky = screen.getByTestId('sticky');
31
+ expect(sticky.type).toBe('View');
32
+ expect(sticky.props.offset).toBeUndefined();
33
+ expect(screen.getByText('inside')).toBeTruthy();
34
+ expect(keyboardHeight()).toBe(0);
35
+ });
36
+
37
+ it('closes the keyboard with React Native', async () => {
38
+ const dismiss = jest.spyOn(Keyboard, 'dismiss');
39
+
40
+ await expect(dismissKeyboard()).resolves.toBeUndefined();
41
+ expect(dismiss).toHaveBeenCalled();
42
+ });
43
+
44
+ it('renders the composer and opens its sheet', async () => {
45
+ const dismiss = jest.spyOn(Keyboard, 'dismiss');
46
+ render(
47
+ <StreamTheme>
48
+ <ComposerView onSendMessage={() => Promise.resolve()} />
49
+ </StreamTheme>,
50
+ );
51
+
52
+ await act(async () => {
53
+ fireEvent.press(screen.getByText('+'));
54
+ await flushPromises();
55
+ });
56
+
57
+ expect(dismiss).toHaveBeenCalled();
58
+ expect(sheetStore.getLatestValue().open).toBe(true);
59
+ });
60
+ });
@@ -12,18 +12,22 @@ import Animated, { Easing, FadeIn } from 'react-native-reanimated';
12
12
 
13
13
  import type { LegendListRenderItemProps } from '@legendapp/list/react-native';
14
14
 
15
- import type {
16
- AIConversationController,
17
- AIConversationControllerState,
18
- } from './controller/AIConversationController';
19
- import { editActionOf, replyActionsOf } from './controller/offers';
20
- import { useAIConversationState } from './controller/useAIConversationState';
21
15
  import type { AIConversationEntry } from './entries';
22
16
  import { AIChatList } from './list/AIChatList';
23
- import type { AIMessage, AIPendingMessage } from './types';
24
17
  import { type ConversationRow, type RowPosition, useConversationRows } from './useConversationRows';
25
18
 
26
19
  import { useTheme } from '../contexts';
20
+ import type {
21
+ AIConversationController,
22
+ AIConversationControllerState,
23
+ } from '../conversation-state/controller/AIConversationController';
24
+ import {
25
+ editActionOf,
26
+ latestUserMessageIdOf,
27
+ replyActionsOf,
28
+ } from '../conversation-state/controller/offers';
29
+ import { useAIConversationState } from '../conversation-state/controller/useAIConversationState';
30
+ import type { AIMessage, AIPendingMessage } from '../conversation-state/types';
27
31
  import type { AIUIHost } from '../generative-ui/AIUIHost';
28
32
  import { AIUIGenerated, useAIUIHostRefresh } from '../generative-ui/AIUIView';
29
33
  import { primitives } from '../internal/tokens';
@@ -103,8 +107,7 @@ export type AIConversationProps = {
103
107
  * How far above the bottom of the screen the composer sits, such as above a tab bar. The
104
108
  * keyboard comes up over the conversation, which makes room for it below the latest turn (the
105
109
  * room the pinned question leaves takes it first), and covers it only as far as it rises past
106
- * this. Defaults to the bottom safe area. Needs a `KeyboardProvider` from
107
- * react-native-keyboard-controller above it.
110
+ * this. Defaults to the bottom safe area.
108
111
  */
109
112
  keyboardOffset?: number;
110
113
  /**
@@ -182,6 +185,8 @@ const DefaultAssistantMessage = ({
182
185
  // shrinks (a sign-in going, reasoning folding, the thinking going) only a frame later, and in that
183
186
  // frame the question would be pulled down. Below a pinned turn there is room anyway, so it looks
184
187
  // the same, and the room stays the list's, which the keyboard takes before it lifts anything.
188
+ // The person's own message is left free: something folding in it, such as a transcript, would
189
+ // leave a gap above the reply.
185
190
  type TurnFloor = { heights: Map<string, number>; turnKey: string | undefined };
186
191
 
187
192
  type ConversationContextValue = {
@@ -224,6 +229,7 @@ type EntryViewProps = {
224
229
  const NO_MESSAGES: AIMessage[] = [];
225
230
 
226
231
  const REPLY_ACTIONS_IN = FadeIn.duration(200).easing(Easing.out(Easing.ease));
232
+ const THINKING_IN = REPLY_ACTIONS_IN;
227
233
 
228
234
  const useReplyWriting = (messageId: string | undefined, isGenerating: boolean) => {
229
235
  const [writing, setWriting] = useState<string>();
@@ -295,14 +301,18 @@ const EntryView = ({
295
301
  />
296
302
  {showsUI ? (
297
303
  <AIUIGenerated
298
- isStreaming={uiDocument.isStreaming}
304
+ isStreaming={uiDocument.isStreaming || wasWatched}
299
305
  messageId={message.id}
300
306
  onError={(error) => ui?.fail(message.id, error)}
301
307
  >
302
308
  {generated}
303
309
  </AIUIGenerated>
304
310
  ) : null}
305
- {thinkingSince !== undefined ? <AIThinking since={thinkingSince} /> : null}
311
+ {thinkingSince !== undefined ? (
312
+ <Animated.View entering={THINKING_IN}>
313
+ <AIThinking since={thinkingSince} />
314
+ </Animated.View>
315
+ ) : null}
306
316
  {ReplyActions && !isGenerating && !isWriting && hasAnswer && !isWritingUI ? (
307
317
  <Animated.View entering={wasWatched ? REPLY_ACTIONS_IN : undefined}>
308
318
  <ReplyActions isLatestReply={entry.isLatestReply} message={message} text={message.text} />
@@ -328,7 +338,10 @@ const Row = memo(({ row }: { row: ConversationRow }) => {
328
338
  // a worklet sent to the UI thread. The floor only rises, long before a row shrinks, so it can
329
339
  // follow a frame after the row grows.
330
340
  const turnFloor = context?.turnFloor;
331
- const isHeld = !!turnFloor && row.turnKey === turnFloor.turnKey;
341
+ const isHeld =
342
+ !!turnFloor &&
343
+ row.turnKey === turnFloor.turnKey &&
344
+ !(row.kind === 'entry' && row.entry.isFromUser);
332
345
  const floor = isHeld ? (turnFloor?.heights.get(row.key) ?? 0) : 0;
333
346
  const [, setFloorRaised] = useState(0);
334
347
  const onLayout = isHeld
@@ -357,7 +370,11 @@ const Row = memo(({ row }: { row: ConversationRow }) => {
357
370
  />
358
371
  ) : (
359
372
  <>
360
- {row.isThinking ? <AIThinking /> : null}
373
+ {row.isThinking ? (
374
+ <Animated.View entering={THINKING_IN}>
375
+ <AIThinking />
376
+ </Animated.View>
377
+ ) : null}
361
378
  {row.footer}
362
379
  </>
363
380
  )}
@@ -423,6 +440,10 @@ const DefaultConversationReplyActions = ({
423
440
  * - A button scrolls back to the latest message when the end is out of view.
424
441
  * - A message the person sent shows at once (`pendingMessage`) and the agent thinks under it
425
442
  * (`AIThinking`) until its reply shows something.
443
+ *
444
+ * Import it from `stream-chat-react-native/ai-components/conversation` (or
445
+ * `stream-chat-expo/ai-components/conversation`). It needs react-native-keyboard-controller, with
446
+ * its `KeyboardProvider` above it.
426
447
  */
427
448
  export const AIConversation = (props: AIConversationProps) =>
428
449
  props.conversation ? (
@@ -461,9 +482,10 @@ const ControlledConversation = ({
461
482
  replyActionsOf(conversation, { ratings, status }, message, isLatestReply),
462
483
  [conversation, ratings, status],
463
484
  );
485
+ const latestUserMessageId = latestUserMessageIdOf(messages);
464
486
  const editAction = useMemo<EditAction>(
465
- () => (message) => editActionOf(conversation, { messages, status }, message),
466
- [conversation, messages, status],
487
+ () => (message) => editActionOf(conversation, { latestUserMessageId, status }, message),
488
+ [conversation, latestUserMessageId, status],
467
489
  );
468
490
  const { ui } = props;
469
491
  // A reply that shows only its UI has started, which the controller can't tell without the
@@ -1,4 +1,4 @@
1
- import type { AIMessage, AIPendingMessage } from './types';
1
+ import type { AIMessage, AIPendingMessage } from '../conversation-state/types';
2
2
 
3
3
  import type { AIUIDocument } from '../generative-ui/AIUIDocument';
4
4
  import type { AIMessageAttachmentData } from '../message/AIMessageAttachment';
@@ -1,5 +1,2 @@
1
1
  export * from './AIConversation';
2
- export * from './controller';
3
- export * from './fromStreamMessage';
4
- export * from './types';
5
2
  export type { AIConversationCanShowUI, AIConversationEntry, AIConversationTurn } from './entries';
@@ -1,7 +1,8 @@
1
1
  import { useRef } from 'react';
2
- import { KeyboardController, useKeyboardHandler } from 'react-native-keyboard-controller';
3
2
  import { runOnJS, type SharedValue, useSharedValue } from 'react-native-reanimated';
4
3
 
4
+ import { keyboardHeight, useKeyboardHandler } from '../../internal/keyboard';
5
+
5
6
  export type KeyboardFrame = { height: number; progress: number };
6
7
 
7
8
  /**
@@ -19,7 +20,7 @@ export const keyboardOverlapOf = (keyboard: KeyboardFrame, offset: number) => {
19
20
  * once it has finished moving.
20
21
  */
21
22
  export const useKeyboardFrame = (onSettled?: () => void): SharedValue<KeyboardFrame> => {
22
- const initial = KeyboardController.state()?.height ?? 0;
23
+ const initial = keyboardHeight();
23
24
  const keyboard = useSharedValue<KeyboardFrame>({
24
25
  height: initial,
25
26
  progress: initial > 0 ? 1 : 0,
@@ -8,7 +8,8 @@ import {
8
8
  hasFinishedSteps,
9
9
  turnsOf,
10
10
  } from './entries';
11
- import type { AIMessage, AIPendingMessage } from './types';
11
+
12
+ import type { AIMessage, AIPendingMessage } from '../conversation-state/types';
12
13
 
13
14
  import type { AIMessageAttachmentData } from '../message/AIMessageAttachment';
14
15
  import { AIThinkingClock } from '../reasoning/thinkingClock';