@assistant-ui/react-native 0.1.42 → 0.1.44

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 (228) hide show
  1. package/dist/context/AssistantContext.d.ts +2 -2
  2. package/dist/context/AssistantContext.d.ts.map +1 -0
  3. package/dist/context/providers/AttachmentByIndexProvider.d.ts +2 -2
  4. package/dist/context/providers/AttachmentByIndexProvider.d.ts.map +1 -0
  5. package/dist/context/providers/RuntimeAdapterProvider.d.ts +2 -2
  6. package/dist/context/providers/RuntimeAdapterProvider.d.ts.map +1 -0
  7. package/dist/context/providers/ThreadListItemRuntimeProvider.d.ts +2 -2
  8. package/dist/context/providers/ThreadListItemRuntimeProvider.d.ts.map +1 -0
  9. package/dist/index.d.ts +54 -22
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +5 -4
  12. package/dist/internal.d.ts +4 -3
  13. package/dist/internal.d.ts.map +1 -0
  14. package/dist/primitives/actionBar/ActionBarCopy.d.ts +9 -11
  15. package/dist/primitives/actionBar/ActionBarCopy.d.ts.map +1 -1
  16. package/dist/primitives/actionBar/ActionBarCopy.js +23 -19
  17. package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
  18. package/dist/primitives/actionBar/ActionBarEdit.d.ts +7 -7
  19. package/dist/primitives/actionBar/ActionBarEdit.d.ts.map +1 -1
  20. package/dist/primitives/actionBar/ActionBarEdit.js +21 -11
  21. package/dist/primitives/actionBar/ActionBarEdit.js.map +1 -1
  22. package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts +8 -10
  23. package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts.map +1 -1
  24. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +35 -17
  25. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
  26. package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts +8 -10
  27. package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts.map +1 -1
  28. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +35 -17
  29. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarReload.d.ts +7 -7
  31. package/dist/primitives/actionBar/ActionBarReload.d.ts.map +1 -1
  32. package/dist/primitives/actionBar/ActionBarReload.js +21 -11
  33. package/dist/primitives/actionBar/ActionBarReload.js.map +1 -1
  34. package/dist/primitives/actionBar/ActionBarSpeak.d.ts +12 -0
  35. package/dist/primitives/actionBar/ActionBarSpeak.d.ts.map +1 -0
  36. package/dist/primitives/actionBar/ActionBarSpeak.js +61 -0
  37. package/dist/primitives/actionBar/ActionBarSpeak.js.map +1 -0
  38. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts +12 -0
  39. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -0
  40. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +61 -0
  41. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -0
  42. package/dist/primitives/actionBar.d.ts +7 -10
  43. package/dist/primitives/actionBar.d.ts.map +1 -1
  44. package/dist/primitives/actionBar.js +6 -2
  45. package/dist/primitives/actionBar.js.map +1 -1
  46. package/dist/primitives/attachment/AttachmentName.d.ts +4 -7
  47. package/dist/primitives/attachment/AttachmentName.d.ts.map +1 -1
  48. package/dist/primitives/attachment/AttachmentRemove.d.ts +4 -7
  49. package/dist/primitives/attachment/AttachmentRemove.d.ts.map +1 -1
  50. package/dist/primitives/attachment/AttachmentRoot.d.ts +5 -8
  51. package/dist/primitives/attachment/AttachmentRoot.d.ts.map +1 -1
  52. package/dist/primitives/attachment/AttachmentThumb.d.ts +4 -7
  53. package/dist/primitives/attachment/AttachmentThumb.d.ts.map +1 -1
  54. package/dist/primitives/attachment.d.ts +4 -9
  55. package/dist/primitives/attachment.d.ts.map +1 -1
  56. package/dist/primitives/branchPicker/BranchPickerCount.d.ts +3 -6
  57. package/dist/primitives/branchPicker/BranchPickerCount.d.ts.map +1 -1
  58. package/dist/primitives/branchPicker/BranchPickerNext.d.ts +7 -7
  59. package/dist/primitives/branchPicker/BranchPickerNext.d.ts.map +1 -1
  60. package/dist/primitives/branchPicker/BranchPickerNext.js +21 -11
  61. package/dist/primitives/branchPicker/BranchPickerNext.js.map +1 -1
  62. package/dist/primitives/branchPicker/BranchPickerNumber.d.ts +3 -6
  63. package/dist/primitives/branchPicker/BranchPickerNumber.d.ts.map +1 -1
  64. package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts +7 -7
  65. package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts.map +1 -1
  66. package/dist/primitives/branchPicker/BranchPickerPrevious.js +21 -11
  67. package/dist/primitives/branchPicker/BranchPickerPrevious.js.map +1 -1
  68. package/dist/primitives/branchPicker.d.ts +4 -9
  69. package/dist/primitives/branchPicker.d.ts.map +1 -1
  70. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts +4 -7
  71. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts.map +1 -1
  72. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.d.ts +5 -8
  73. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.d.ts.map +1 -1
  74. package/dist/primitives/chainOfThought.d.ts +3 -8
  75. package/dist/primitives/chainOfThought.d.ts.map +1 -1
  76. package/dist/primitives/composer/ComposerAddAttachment.d.ts +4 -7
  77. package/dist/primitives/composer/ComposerAddAttachment.d.ts.map +1 -1
  78. package/dist/primitives/composer/ComposerAttachments.d.ts +5 -8
  79. package/dist/primitives/composer/ComposerAttachments.d.ts.map +1 -1
  80. package/dist/primitives/composer/ComposerCancel.d.ts +4 -7
  81. package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
  82. package/dist/primitives/composer/ComposerIf.d.ts +2 -2
  83. package/dist/primitives/composer/ComposerIf.d.ts.map +1 -0
  84. package/dist/primitives/composer/ComposerInput.d.ts +10 -13
  85. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  86. package/dist/primitives/composer/ComposerInput.js +1 -1
  87. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  88. package/dist/primitives/composer/ComposerQueue.d.ts +2 -2
  89. package/dist/primitives/composer/ComposerQueue.d.ts.map +1 -0
  90. package/dist/primitives/composer/ComposerQuote.d.ts +5 -8
  91. package/dist/primitives/composer/ComposerQuote.d.ts.map +1 -1
  92. package/dist/primitives/composer/ComposerQuoteDismiss.d.ts +4 -7
  93. package/dist/primitives/composer/ComposerQuoteDismiss.d.ts.map +1 -1
  94. package/dist/primitives/composer/ComposerQuoteText.d.ts +5 -8
  95. package/dist/primitives/composer/ComposerQuoteText.d.ts.map +1 -1
  96. package/dist/primitives/composer/ComposerRoot.d.ts +5 -8
  97. package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
  98. package/dist/primitives/composer/ComposerSend.d.ts +4 -7
  99. package/dist/primitives/composer/ComposerSend.d.ts.map +1 -1
  100. package/dist/primitives/composer.d.ts +11 -16
  101. package/dist/primitives/composer.d.ts.map +1 -1
  102. package/dist/primitives/error/ErrorMessage.d.ts +7 -10
  103. package/dist/primitives/error/ErrorMessage.d.ts.map +1 -1
  104. package/dist/primitives/error/ErrorRoot.d.ts +7 -10
  105. package/dist/primitives/error/ErrorRoot.d.ts.map +1 -1
  106. package/dist/primitives/error/index.d.ts +2 -7
  107. package/dist/primitives/error/index.d.ts.map +1 -1
  108. package/dist/primitives/message/MessageAttachments.d.ts +5 -8
  109. package/dist/primitives/message/MessageAttachments.d.ts.map +1 -1
  110. package/dist/primitives/message/MessageContent.d.ts +47 -49
  111. package/dist/primitives/message/MessageContent.d.ts.map +1 -1
  112. package/dist/primitives/message/MessageContent.js +27 -21
  113. package/dist/primitives/message/MessageContent.js.map +1 -1
  114. package/dist/primitives/message/MessageIf.d.ts +8 -11
  115. package/dist/primitives/message/MessageIf.d.ts.map +1 -1
  116. package/dist/primitives/message/MessageParts.d.ts +6 -8
  117. package/dist/primitives/message/MessageParts.d.ts.map +1 -1
  118. package/dist/primitives/message/MessageRoot.d.ts +5 -8
  119. package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
  120. package/dist/primitives/message.d.ts +6 -11
  121. package/dist/primitives/message.d.ts.map +1 -1
  122. package/dist/primitives/messagePart/MessagePartImage.d.ts +9 -0
  123. package/dist/primitives/messagePart/MessagePartImage.d.ts.map +1 -0
  124. package/dist/primitives/messagePart/MessagePartImage.js +39 -0
  125. package/dist/primitives/messagePart/MessagePartImage.js.map +1 -0
  126. package/dist/primitives/messagePart/MessagePartInProgress.d.ts +2 -0
  127. package/dist/primitives/messagePart/MessagePartInProgress.d.ts.map +1 -0
  128. package/dist/primitives/messagePart/MessagePartInProgress.js +2 -0
  129. package/dist/primitives/messagePart/MessagePartText.d.ts +9 -0
  130. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -0
  131. package/dist/primitives/messagePart/MessagePartText.js +32 -0
  132. package/dist/primitives/messagePart/MessagePartText.js.map +1 -0
  133. package/dist/primitives/messagePart.d.ts +5 -0
  134. package/dist/primitives/messagePart.d.ts.map +1 -0
  135. package/dist/primitives/messagePart.js +16 -0
  136. package/dist/primitives/messagePart.js.map +1 -0
  137. package/dist/primitives/queueItem/QueueItemRemove.d.ts +4 -7
  138. package/dist/primitives/queueItem/QueueItemRemove.d.ts.map +1 -1
  139. package/dist/primitives/queueItem/QueueItemSteer.d.ts +4 -7
  140. package/dist/primitives/queueItem/QueueItemSteer.d.ts.map +1 -1
  141. package/dist/primitives/queueItem/QueueItemText.d.ts +5 -8
  142. package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -1
  143. package/dist/primitives/queueItem.d.ts +3 -8
  144. package/dist/primitives/queueItem.d.ts.map +1 -1
  145. package/dist/primitives/suggestion/SuggestionDescription.d.ts +5 -8
  146. package/dist/primitives/suggestion/SuggestionDescription.d.ts.map +1 -1
  147. package/dist/primitives/suggestion/SuggestionTitle.d.ts +5 -8
  148. package/dist/primitives/suggestion/SuggestionTitle.d.ts.map +1 -1
  149. package/dist/primitives/suggestion/SuggestionTrigger.d.ts +15 -18
  150. package/dist/primitives/suggestion/SuggestionTrigger.d.ts.map +1 -1
  151. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
  152. package/dist/primitives/suggestion.d.ts +3 -8
  153. package/dist/primitives/suggestion.d.ts.map +1 -1
  154. package/dist/primitives/thread/ThreadEmpty.d.ts +4 -7
  155. package/dist/primitives/thread/ThreadEmpty.d.ts.map +1 -1
  156. package/dist/primitives/thread/ThreadIf.d.ts +6 -9
  157. package/dist/primitives/thread/ThreadIf.d.ts.map +1 -1
  158. package/dist/primitives/thread/ThreadMessages.d.ts +41 -37
  159. package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
  160. package/dist/primitives/thread/ThreadMessages.js +123 -72
  161. package/dist/primitives/thread/ThreadMessages.js.map +1 -1
  162. package/dist/primitives/thread/ThreadRoot.d.ts +5 -8
  163. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  164. package/dist/primitives/thread/ThreadSuggestion.d.ts +17 -20
  165. package/dist/primitives/thread/ThreadSuggestion.d.ts.map +1 -1
  166. package/dist/primitives/thread.d.ts +8 -11
  167. package/dist/primitives/thread.d.ts.map +1 -1
  168. package/dist/primitives/threadList/ThreadListItems.d.ts +8 -11
  169. package/dist/primitives/threadList/ThreadListItems.d.ts.map +1 -1
  170. package/dist/primitives/threadList/ThreadListItems.js +1 -1
  171. package/dist/primitives/threadList/ThreadListNew.d.ts +7 -10
  172. package/dist/primitives/threadList/ThreadListNew.d.ts.map +1 -1
  173. package/dist/primitives/threadList/ThreadListNew.js +21 -17
  174. package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
  175. package/dist/primitives/threadList/ThreadListRoot.d.ts +5 -8
  176. package/dist/primitives/threadList/ThreadListRoot.d.ts.map +1 -1
  177. package/dist/primitives/threadList.d.ts +3 -8
  178. package/dist/primitives/threadList.d.ts.map +1 -1
  179. package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts +4 -7
  180. package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts.map +1 -1
  181. package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts +4 -7
  182. package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts.map +1 -1
  183. package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts +5 -8
  184. package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
  185. package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts +7 -10
  186. package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts.map +1 -1
  187. package/dist/primitives/threadListItem/ThreadListItemTrigger.js +21 -17
  188. package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
  189. package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts +4 -7
  190. package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts.map +1 -1
  191. package/dist/primitives/threadListItem.d.ts +6 -11
  192. package/dist/primitives/threadListItem.d.ts.map +1 -1
  193. package/dist/runtimes/useLocalRuntime.d.ts +2 -2
  194. package/dist/runtimes/useLocalRuntime.d.ts.map +1 -0
  195. package/dist/runtimes/useRemoteThreadListRuntime.d.ts +2 -2
  196. package/dist/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -0
  197. package/package.json +13 -12
  198. package/src/index.ts +91 -1
  199. package/src/primitives/accessibilityRole.test.tsx +20 -0
  200. package/src/primitives/actionBar/ActionBar.test.tsx +375 -0
  201. package/src/primitives/actionBar/ActionBarCopy.tsx +8 -3
  202. package/src/primitives/actionBar/ActionBarEdit.tsx +16 -4
  203. package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +18 -4
  204. package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +18 -4
  205. package/src/primitives/actionBar/ActionBarReload.tsx +16 -4
  206. package/src/primitives/actionBar/ActionBarSpeak.tsx +42 -0
  207. package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +42 -0
  208. package/src/primitives/actionBar.ts +8 -0
  209. package/src/primitives/branchPicker/BranchPickerNext.test.tsx +52 -3
  210. package/src/primitives/branchPicker/BranchPickerNext.tsx +16 -4
  211. package/src/primitives/branchPicker/BranchPickerPrevious.test.tsx +52 -6
  212. package/src/primitives/branchPicker/BranchPickerPrevious.tsx +16 -4
  213. package/src/primitives/composer/ComposerInput.test.tsx +17 -1
  214. package/src/primitives/composer/ComposerInput.tsx +6 -1
  215. package/src/primitives/message/MessageContent.test.tsx +64 -3
  216. package/src/primitives/message/MessageContent.tsx +5 -3
  217. package/src/primitives/messagePart/MessagePartImage.test.tsx +91 -0
  218. package/src/primitives/messagePart/MessagePartImage.tsx +33 -0
  219. package/src/primitives/messagePart/MessagePartInProgress.ts +1 -0
  220. package/src/primitives/messagePart/MessagePartText.test.tsx +80 -0
  221. package/src/primitives/messagePart/MessagePartText.tsx +36 -0
  222. package/src/primitives/messagePart.ts +10 -0
  223. package/src/primitives/thread/ThreadMessages.test.tsx +190 -6
  224. package/src/primitives/thread/ThreadMessages.tsx +68 -1
  225. package/src/primitives/threadList/ThreadListNew.test.tsx +35 -8
  226. package/src/primitives/threadList/ThreadListNew.tsx +4 -1
  227. package/src/primitives/threadListItem/ThreadListItemTrigger.test.tsx +32 -6
  228. package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +4 -1
@@ -0,0 +1,80 @@
1
+ import { act } from "react";
2
+ import { createRoot, type Root } from "react-dom/client";
3
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
+ import type { ThreadMessageLike } from "@assistant-ui/core";
5
+ import {
6
+ AssistantRuntimeProvider,
7
+ MessageByIndexProvider,
8
+ PartByIndexProvider,
9
+ useExternalStoreRuntime,
10
+ } from "@assistant-ui/core/react";
11
+ import { MessagePartPrimitiveText } from "./MessagePartText";
12
+
13
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
14
+
15
+ const messages: ThreadMessageLike[] = [
16
+ {
17
+ role: "assistant",
18
+ content: [
19
+ { type: "text", text: "Answer" },
20
+ { type: "reasoning", text: "Thinking" },
21
+ { type: "image", image: "https://example.com/image.png" },
22
+ ],
23
+ },
24
+ ];
25
+
26
+ const App = ({ partIndex }: { partIndex: number }) => {
27
+ const runtime = useExternalStoreRuntime({
28
+ messages,
29
+ convertMessage: (message) => message,
30
+ onNew: async () => {
31
+ throw new Error("This thread is read-only");
32
+ },
33
+ });
34
+
35
+ return (
36
+ <AssistantRuntimeProvider runtime={runtime}>
37
+ <MessageByIndexProvider index={0}>
38
+ <PartByIndexProvider index={partIndex}>
39
+ <MessagePartPrimitiveText testID="text" />
40
+ </PartByIndexProvider>
41
+ </MessageByIndexProvider>
42
+ </AssistantRuntimeProvider>
43
+ );
44
+ };
45
+
46
+ describe("MessagePartPrimitiveText", () => {
47
+ let container: HTMLDivElement;
48
+ let root: Root;
49
+
50
+ beforeEach(() => {
51
+ container = document.createElement("div");
52
+ document.body.appendChild(container);
53
+ root = createRoot(container);
54
+ });
55
+
56
+ afterEach(async () => {
57
+ await act(async () => root.unmount());
58
+ container.remove();
59
+ });
60
+
61
+ const mount = async (partIndex: number) => {
62
+ await act(async () => root.render(<App partIndex={partIndex} />));
63
+ return container.querySelector('[data-testid="text"]') as HTMLElement;
64
+ };
65
+
66
+ it("renders text from the current text part", async () => {
67
+ const text = await mount(0);
68
+ expect(text.textContent).toBe("Answer");
69
+ });
70
+
71
+ it("renders text from the current reasoning part", async () => {
72
+ const text = await mount(1);
73
+ expect(text.textContent).toBe("Thinking");
74
+ });
75
+
76
+ it("renders empty text for a non-text part", async () => {
77
+ const text = await mount(2);
78
+ expect(text.textContent).toBe("");
79
+ });
80
+ });
@@ -0,0 +1,36 @@
1
+ import {
2
+ type ComponentRef,
3
+ type ForwardRefExoticComponent,
4
+ forwardRef,
5
+ type RefAttributes,
6
+ } from "react";
7
+ import { Text, type TextProps } from "react-native";
8
+ import { useAuiState } from "@assistant-ui/store";
9
+
10
+ export namespace MessagePartPrimitiveText {
11
+ export type Element = ComponentRef<typeof Text>;
12
+ export type Props = Omit<TextProps, "children">;
13
+ }
14
+
15
+ export const MessagePartPrimitiveText: ForwardRefExoticComponent<
16
+ MessagePartPrimitiveText.Props &
17
+ RefAttributes<MessagePartPrimitiveText.Element>
18
+ > = forwardRef<
19
+ MessagePartPrimitiveText.Element,
20
+ MessagePartPrimitiveText.Props
21
+ >((props, forwardedRef) => {
22
+ const text = useAuiState((s) => {
23
+ const part = s.part;
24
+ return part.type === "text" || part.type === "reasoning" ? part.text : "";
25
+ });
26
+
27
+ return (
28
+ <Text {...props} ref={forwardedRef}>
29
+ {text}
30
+ </Text>
31
+ );
32
+ });
33
+
34
+ MessagePartPrimitiveText.displayName = "MessagePartPrimitive.Text";
35
+
36
+ export type MessagePartTextProps = MessagePartPrimitiveText.Props;
@@ -0,0 +1,10 @@
1
+ export {
2
+ MessagePartPrimitiveText as Text,
3
+ type MessagePartTextProps as TextProps,
4
+ } from "./messagePart/MessagePartText";
5
+ export {
6
+ MessagePartPrimitiveImage as Image,
7
+ type MessagePartImageProps as ImageProps,
8
+ } from "./messagePart/MessagePartImage";
9
+ export { MessagePartPrimitiveInProgress as InProgress } from "./messagePart/MessagePartInProgress";
10
+ export { PartPrimitiveMessages as Messages } from "@assistant-ui/core/react";
@@ -1,8 +1,9 @@
1
1
  import { act, createRef } from "react";
2
2
  import { createRoot, type Root } from "react-dom/client";
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
- import type { FlatList } from "react-native";
4
+ import type { FlatList, FlatListProps } from "react-native";
5
5
  import type { ThreadMessage } from "@assistant-ui/core";
6
+ import type { MessageState } from "@assistant-ui/core/store";
6
7
  import { ThreadMessages, ThreadMessagesFlatList } from "./ThreadMessages";
7
8
 
8
9
  type Msg = { id: string; role: string };
@@ -12,7 +13,7 @@ const h = vi.hoisted(() => ({
12
13
  thread: { messages: [] as Msg[] },
13
14
  message: { role: "user" as string, composer: { isEditing: false } },
14
15
  },
15
- itemState: { role: "user" } as { role: string },
16
+ itemState: { role: "user" } as { role: string; parts?: unknown[] },
16
17
  events: {} as Record<string, Set<() => void>>,
17
18
  flatListProps: null as Record<string, unknown> | null,
18
19
  scrollToOffset: vi.fn(),
@@ -154,6 +155,10 @@ describe("ThreadMessages", () => {
154
155
  onContentSizeChange?: (width: number, height: number) => void;
155
156
  onLayout?: (event: unknown) => void;
156
157
  onScroll?: (event: unknown) => void;
158
+ onStartReached?: NonNullable<
159
+ FlatListProps<ThreadMessage>["onStartReached"]
160
+ >;
161
+ onStartReachedThreshold?: number;
157
162
  scrollEventThrottle?: number;
158
163
  } | null;
159
164
  if (!props) throw new Error("FlatList was not rendered");
@@ -299,13 +304,15 @@ describe("ThreadMessages", () => {
299
304
  describe("children mode", () => {
300
305
  it("renders via the children render prop", async () => {
301
306
  h.state.thread.messages = [{ id: "1", role: "user" }];
302
- h.itemState = { role: "user" };
303
- const children = vi.fn(({ message }: { message: { role: string } }) => (
304
- <span data-testid="child">child:{message.role}</span>
307
+ h.itemState = { role: "user", parts: [] };
308
+ const children = vi.fn(({ message }: { message: MessageState }) => (
309
+ <span data-testid="child">
310
+ child:{message.role}:{message.parts.length}
311
+ </span>
305
312
  ));
306
313
  await mount({ children });
307
314
  const el = container.querySelector('[data-testid="child"]');
308
- expect(el?.textContent).toBe("child:user");
315
+ expect(el?.textContent).toBe("child:user:0");
309
316
  expect(children).toHaveBeenCalled();
310
317
  });
311
318
  });
@@ -392,6 +399,183 @@ describe("ThreadMessages", () => {
392
399
  expect(h.scrollToOffset).not.toHaveBeenCalled();
393
400
  });
394
401
 
402
+ describe("MessagesFlatList history", () => {
403
+ it("wires loadMore only while history can load more", async () => {
404
+ const loadMore = vi.fn();
405
+
406
+ await mountFlatList({
407
+ components: messageComponents,
408
+ history: { hasMore: true, isLoadingMore: false, loadMore },
409
+ });
410
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
411
+ expect(loadMore).toHaveBeenCalledOnce();
412
+
413
+ await mountFlatList({
414
+ components: messageComponents,
415
+ history: { hasMore: false, isLoadingMore: false, loadMore },
416
+ });
417
+ expect(getFlatListProps().onStartReached).toBeUndefined();
418
+
419
+ await mountFlatList({
420
+ components: messageComponents,
421
+ history: { hasMore: true, isLoadingMore: true, loadMore },
422
+ });
423
+ expect(getFlatListProps().onStartReached).toBeUndefined();
424
+ });
425
+
426
+ it("loads one page when start reached fires again before rerender", async () => {
427
+ const loadMore = vi.fn();
428
+ const callerOnStartReached = vi.fn();
429
+
430
+ await mountFlatList({
431
+ components: messageComponents,
432
+ history: { hasMore: true, isLoadingMore: false, loadMore },
433
+ onStartReached: callerOnStartReached,
434
+ });
435
+ const installedOnStartReached = getFlatListProps().onStartReached;
436
+
437
+ installedOnStartReached?.({ distanceFromStart: 0 });
438
+ installedOnStartReached?.({ distanceFromStart: 0 });
439
+
440
+ expect(loadMore).toHaveBeenCalledOnce();
441
+ expect(callerOnStartReached).toHaveBeenCalledTimes(2);
442
+ });
443
+
444
+ it("loads again after a commit without a loading transition", async () => {
445
+ const loadMore = vi.fn();
446
+ const history = { hasMore: true, isLoadingMore: false, loadMore };
447
+
448
+ await mountFlatList({ components: messageComponents, history });
449
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
450
+
451
+ await mountFlatList({ components: messageComponents, history });
452
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
453
+
454
+ expect(loadMore).toHaveBeenCalledTimes(2);
455
+ });
456
+
457
+ it("does not carry a request latch to another history source", async () => {
458
+ const firstLoadMore = vi.fn();
459
+ const secondLoadMore = vi.fn();
460
+
461
+ await mountFlatList({
462
+ components: messageComponents,
463
+ history: {
464
+ hasMore: true,
465
+ isLoadingMore: false,
466
+ loadMore: firstLoadMore,
467
+ },
468
+ });
469
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
470
+
471
+ await mountFlatList({
472
+ components: messageComponents,
473
+ history: {
474
+ hasMore: true,
475
+ isLoadingMore: false,
476
+ loadMore: secondLoadMore,
477
+ },
478
+ });
479
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
480
+
481
+ expect(firstLoadMore).toHaveBeenCalledOnce();
482
+ expect(secondLoadMore).toHaveBeenCalledOnce();
483
+ });
484
+
485
+ it("loads again after the previous history request settles", async () => {
486
+ const loadMore = vi.fn();
487
+
488
+ await mountFlatList({
489
+ components: messageComponents,
490
+ history: { hasMore: true, isLoadingMore: false, loadMore },
491
+ });
492
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
493
+
494
+ await mountFlatList({
495
+ components: messageComponents,
496
+ history: { hasMore: true, isLoadingMore: true, loadMore },
497
+ });
498
+ await mountFlatList({
499
+ components: messageComponents,
500
+ history: { hasMore: true, isLoadingMore: false, loadMore },
501
+ });
502
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
503
+
504
+ expect(loadMore).toHaveBeenCalledTimes(2);
505
+ });
506
+
507
+ it("allows retrying when loadMore throws synchronously", async () => {
508
+ const loadError = new Error("load failed");
509
+ const loadMore = vi
510
+ .fn()
511
+ .mockImplementationOnce(() => {
512
+ throw loadError;
513
+ })
514
+ .mockImplementationOnce(() => undefined);
515
+
516
+ await mountFlatList({
517
+ components: messageComponents,
518
+ history: { hasMore: true, isLoadingMore: false, loadMore },
519
+ });
520
+ const onStartReached = getFlatListProps().onStartReached;
521
+
522
+ expect(() => onStartReached?.({ distanceFromStart: 0 })).toThrow(
523
+ loadError,
524
+ );
525
+ expect(() => onStartReached?.({ distanceFromStart: 0 })).not.toThrow();
526
+ expect(loadMore).toHaveBeenCalledTimes(2);
527
+ });
528
+
529
+ it("defaults the history threshold and preserves a caller override", async () => {
530
+ const history = {
531
+ hasMore: true,
532
+ isLoadingMore: false,
533
+ loadMore: vi.fn(),
534
+ };
535
+
536
+ await mountFlatList({ components: messageComponents, history });
537
+ expect(getFlatListProps().onStartReachedThreshold).toBe(1);
538
+
539
+ await mountFlatList({
540
+ components: messageComponents,
541
+ history,
542
+ onStartReachedThreshold: 0.5,
543
+ });
544
+ expect(getFlatListProps().onStartReachedThreshold).toBe(0.5);
545
+ });
546
+
547
+ it("runs a caller onStartReached before loading more history", async () => {
548
+ const calls: string[] = [];
549
+ const info = { distanceFromStart: 42 };
550
+ const onStartReached = vi.fn(() => calls.push("onStartReached"));
551
+
552
+ await mountFlatList({
553
+ components: messageComponents,
554
+ history: {
555
+ hasMore: true,
556
+ isLoadingMore: false,
557
+ loadMore: () => calls.push("loadMore"),
558
+ },
559
+ onStartReached,
560
+ });
561
+
562
+ getFlatListProps().onStartReached?.(info);
563
+
564
+ expect(onStartReached).toHaveBeenCalledWith(info);
565
+ expect(calls).toEqual(["onStartReached", "loadMore"]);
566
+ });
567
+
568
+ it("leaves start-reached props untouched without history", async () => {
569
+ const onStartReached = vi.fn();
570
+
571
+ await mountFlatList({ components: messageComponents, onStartReached });
572
+
573
+ const props = getFlatListProps();
574
+ expect(props.onStartReached).toBe(onStartReached);
575
+ expect(props).not.toHaveProperty("onStartReachedThreshold");
576
+ });
577
+ });
578
+
395
579
  describe("MessagesFlatList auto-scroll", () => {
396
580
  it("scrolls to the bottom when messages first appear", async () => {
397
581
  h.state.thread.messages = [{ id: "1", role: "user" }];
@@ -17,7 +17,8 @@ import {
17
17
  type NativeScrollEvent,
18
18
  type NativeSyntheticEvent,
19
19
  } from "react-native";
20
- import type { MessageState, ThreadMessage } from "@assistant-ui/core";
20
+ import type { ThreadMessage } from "@assistant-ui/core";
21
+ import type { MessageState } from "@assistant-ui/core/store";
21
22
  import {
22
23
  RenderChildrenWithAccessor,
23
24
  useAuiEvent,
@@ -67,6 +68,13 @@ export type ThreadMessagesFlatListProps = Omit<
67
68
  scrollToBottomOnRunStart?: boolean | undefined;
68
69
  scrollToBottomOnInitialize?: boolean | undefined;
69
70
  scrollToBottomOnThreadSwitch?: boolean | undefined;
71
+ history?:
72
+ | {
73
+ hasMore: boolean;
74
+ isLoadingMore: boolean;
75
+ loadMore: () => void;
76
+ }
77
+ | undefined;
70
78
  };
71
79
 
72
80
  /** @deprecated Use ThreadMessagesFlatListProps instead. */
@@ -75,6 +83,42 @@ export type ThreadMessagesProps = ThreadMessagesFlatListProps;
75
83
  const DEFAULT_SYSTEM_MESSAGE = () => null;
76
84
  const AT_BOTTOM_THRESHOLD = 4;
77
85
 
86
+ const useHistoryLoad = (
87
+ history: ThreadMessagesFlatListProps["history"],
88
+ onStartReached: FlatListProps<ThreadMessage>["onStartReached"],
89
+ ) => {
90
+ const loadRequestedRef = useRef(false);
91
+ const hasMore = history?.hasMore ?? false;
92
+ const isLoadingMore = history?.isLoadingMore ?? false;
93
+
94
+ // The latch is scoped to one commit so a no-op load cannot disable paging.
95
+ useEffect(() => {
96
+ loadRequestedRef.current = isLoadingMore;
97
+ });
98
+
99
+ const handleStartReached = useCallback<
100
+ NonNullable<FlatListProps<ThreadMessage>["onStartReached"]>
101
+ >(
102
+ (info) => {
103
+ onStartReached?.(info);
104
+ if (loadRequestedRef.current) return;
105
+ loadRequestedRef.current = true;
106
+ try {
107
+ history?.loadMore();
108
+ } catch (error) {
109
+ loadRequestedRef.current = false;
110
+ throw error;
111
+ }
112
+ },
113
+ [history, onStartReached],
114
+ );
115
+
116
+ return {
117
+ canLoadMore: hasMore && !isLoadingMore,
118
+ handleStartReached,
119
+ };
120
+ };
121
+
78
122
  const getComponent = (
79
123
  components: MessageComponents,
80
124
  role: ThreadMessage["role"],
@@ -415,10 +459,13 @@ export const ThreadMessagesFlatList = forwardRef<
415
459
  onContentSizeChange,
416
460
  onLayout,
417
461
  onScroll,
462
+ onStartReached,
463
+ onStartReachedThreshold,
418
464
  scrollEventThrottle,
419
465
  scrollToBottomOnInitialize,
420
466
  scrollToBottomOnRunStart,
421
467
  scrollToBottomOnThreadSwitch,
468
+ history,
422
469
  ...flatListProps
423
470
  },
424
471
  forwardedRef,
@@ -485,6 +532,11 @@ export const ThreadMessagesFlatList = forwardRef<
485
532
  [handleAutoScrollContentSizeChange, onContentSizeChange],
486
533
  );
487
534
 
535
+ const { canLoadMore, handleStartReached } = useHistoryLoad(
536
+ history,
537
+ onStartReached,
538
+ );
539
+
488
540
  return (
489
541
  <FlatList
490
542
  ref={setFlatListRef}
@@ -505,6 +557,21 @@ export const ThreadMessagesFlatList = forwardRef<
505
557
  ...(onScroll && { onScroll }),
506
558
  ...(scrollEventThrottle !== undefined && { scrollEventThrottle }),
507
559
  })}
560
+ {...(history
561
+ ? {
562
+ ...(canLoadMore
563
+ ? { onStartReached: handleStartReached }
564
+ : onStartReached
565
+ ? { onStartReached }
566
+ : {}),
567
+ onStartReachedThreshold: onStartReachedThreshold ?? 1,
568
+ }
569
+ : {
570
+ ...(onStartReached && { onStartReached }),
571
+ ...(onStartReachedThreshold !== undefined && {
572
+ onStartReachedThreshold,
573
+ }),
574
+ })}
508
575
  {...flatListProps}
509
576
  />
510
577
  );
@@ -6,6 +6,7 @@ import { ThreadListNew } from "./ThreadListNew";
6
6
  const h = vi.hoisted(() => ({
7
7
  switchToNewThread: vi.fn<() => void>(),
8
8
  state: { threads: { newThreadId: "new", mainThreadId: "other" } },
9
+ platform: { os: "web" },
9
10
  pressableProps: null as Record<string, unknown> | null,
10
11
  }));
11
12
 
@@ -41,7 +42,16 @@ vi.mock("react-native", async (importOriginal) => {
41
42
  );
42
43
  });
43
44
 
44
- return { ...actual, Pressable: PressableMock };
45
+ return {
46
+ ...actual,
47
+ Platform: {
48
+ ...actual.Platform,
49
+ get OS() {
50
+ return h.platform.os;
51
+ },
52
+ },
53
+ Pressable: PressableMock,
54
+ };
45
55
  });
46
56
 
47
57
  (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
@@ -58,6 +68,7 @@ describe("ThreadListNew", () => {
58
68
  beforeEach(() => {
59
69
  h.switchToNewThread.mockReset();
60
70
  h.state.threads = { newThreadId: "new", mainThreadId: "other" };
71
+ h.platform.os = "web";
61
72
  h.pressableProps = null;
62
73
 
63
74
  container = document.createElement("div");
@@ -110,39 +121,55 @@ describe("ThreadListNew", () => {
110
121
  expect(h.switchToNewThread).not.toHaveBeenCalled();
111
122
  });
112
123
 
113
- it("marks itself selected while the new thread is the current one", async () => {
124
+ it("marks itself current on the web while the new thread is current", async () => {
114
125
  h.state.threads = { newThreadId: "new", mainThreadId: "new" };
115
126
 
116
- await mount();
127
+ const el = await mount();
117
128
 
129
+ expect(el.getAttribute("aria-current")).toBe("true");
130
+ expect(el.hasAttribute("aria-selected")).toBe(false);
118
131
  expect(accessibilityState()).toMatchObject({ selected: true });
119
132
  });
120
133
 
121
- it("is not selected while another thread is current", async () => {
122
- await mount();
134
+ it("is not current while another thread is current", async () => {
135
+ const el = await mount();
123
136
 
137
+ expect(el.hasAttribute("aria-current")).toBe(false);
124
138
  expect(accessibilityState()).toMatchObject({ selected: false });
125
139
  });
126
140
 
141
+ it("marks itself selected without aria-current on native", async () => {
142
+ h.platform.os = "ios";
143
+ h.state.threads = { newThreadId: "new", mainThreadId: "new" };
144
+
145
+ const el = await mount();
146
+
147
+ expect(el.hasAttribute("aria-current")).toBe(false);
148
+ expect(accessibilityState()).toMatchObject({ selected: true });
149
+ });
150
+
127
151
  it("keeps other accessibility state the caller passes", async () => {
128
152
  h.state.threads = { newThreadId: "new", mainThreadId: "new" };
129
153
 
130
- await mount({ accessibilityState: { busy: true } });
154
+ const el = await mount({ accessibilityState: { busy: true } });
131
155
 
156
+ expect(el.getAttribute("aria-current")).toBe("true");
132
157
  expect(accessibilityState()).toMatchObject({ selected: true, busy: true });
133
158
  });
134
159
 
135
160
  it("lets the caller override the selected state", async () => {
136
161
  h.state.threads = { newThreadId: "new", mainThreadId: "new" };
137
162
 
138
- await mount({ accessibilityState: { selected: false } });
163
+ const el = await mount({ accessibilityState: { selected: false } });
139
164
 
165
+ expect(el.hasAttribute("aria-current")).toBe(false);
140
166
  expect(accessibilityState()).toMatchObject({ selected: false });
141
167
  });
142
168
 
143
169
  it("lets the caller select a control the state reports inactive", async () => {
144
- await mount({ accessibilityState: { selected: true } });
170
+ const el = await mount({ accessibilityState: { selected: true } });
145
171
 
172
+ expect(el.getAttribute("aria-current")).toBe("true");
146
173
  expect(accessibilityState()).toMatchObject({ selected: true });
147
174
  });
148
175
 
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import {
3
+ Platform,
3
4
  Pressable,
4
5
  type PressableProps,
5
6
  type PressableStateCallbackType,
@@ -27,12 +28,14 @@ export const ThreadListNew = ({
27
28
  (s) => s.threads.newThreadId === s.threads.mainThreadId,
28
29
  );
29
30
  const { switchToNewThread } = useThreadListNew();
31
+ const selected = accessibilityState?.selected ?? isActive;
30
32
 
31
33
  return (
32
34
  <Pressable
33
35
  onPress={switchToNewThread}
34
36
  accessibilityRole="button"
35
- accessibilityState={{ selected: isActive, ...accessibilityState }}
37
+ accessibilityState={{ ...accessibilityState, selected }}
38
+ aria-current={Platform.OS === "web" && selected ? "true" : undefined}
36
39
  {...pressableProps}
37
40
  >
38
41
  {typeof children === "function"
@@ -9,6 +9,7 @@ const h = vi.hoisted(() => ({
9
9
  threads: { mainThreadId: "thread-1" },
10
10
  threadListItem: { id: "thread-1" },
11
11
  },
12
+ platform: { os: "web" },
12
13
  pressableProps: null as Record<string, unknown> | null,
13
14
  }));
14
15
 
@@ -37,7 +38,16 @@ vi.mock("react-native", async (importOriginal) => {
37
38
  { ...props, ref },
38
39
  );
39
40
  });
40
- return { ...actual, Pressable: PressableMock };
41
+ return {
42
+ ...actual,
43
+ Platform: {
44
+ ...actual.Platform,
45
+ get OS() {
46
+ return h.platform.os;
47
+ },
48
+ },
49
+ Pressable: PressableMock,
50
+ };
41
51
  });
42
52
 
43
53
  (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
@@ -55,6 +65,7 @@ describe("ThreadListItemTrigger", () => {
55
65
  h.switchTo.mockReset();
56
66
  h.state.threads.mainThreadId = "thread-1";
57
67
  h.state.threadListItem.id = "thread-1";
68
+ h.platform.os = "web";
58
69
  h.pressableProps = null;
59
70
 
60
71
  container = document.createElement("div");
@@ -107,23 +118,38 @@ describe("ThreadListItemTrigger", () => {
107
118
  expect(h.switchTo).not.toHaveBeenCalled();
108
119
  });
109
120
 
110
- it("marks the current thread selected", async () => {
111
- await mount();
121
+ it("marks the current thread current on the web", async () => {
122
+ const el = await mount();
112
123
 
124
+ expect(el.getAttribute("aria-current")).toBe("true");
125
+ expect(el.hasAttribute("aria-selected")).toBe(false);
113
126
  expect(accessibilityState()).toMatchObject({ selected: true });
114
127
  });
115
128
 
116
- it("does not mark another thread selected", async () => {
129
+ it("does not mark another thread current", async () => {
117
130
  h.state.threads.mainThreadId = "thread-2";
118
131
 
119
- await mount();
132
+ const el = await mount();
120
133
 
134
+ expect(el.hasAttribute("aria-current")).toBe(false);
121
135
  expect(accessibilityState()).toMatchObject({ selected: false });
122
136
  });
123
137
 
138
+ it("marks the current thread selected without aria-current on native", async () => {
139
+ h.platform.os = "ios";
140
+
141
+ const el = await mount();
142
+
143
+ expect(el.hasAttribute("aria-current")).toBe(false);
144
+ expect(accessibilityState()).toMatchObject({ selected: true });
145
+ });
146
+
124
147
  it("keeps caller accessibility state overrides", async () => {
125
- await mount({ accessibilityState: { busy: true, selected: false } });
148
+ const el = await mount({
149
+ accessibilityState: { busy: true, selected: false },
150
+ });
126
151
 
152
+ expect(el.hasAttribute("aria-current")).toBe(false);
127
153
  expect(accessibilityState()).toMatchObject({ busy: true, selected: false });
128
154
  });
129
155
 
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import {
3
+ Platform,
3
4
  Pressable,
4
5
  type PressableProps,
5
6
  type PressableStateCallbackType,
@@ -27,12 +28,14 @@ export const ThreadListItemTrigger = ({
27
28
  (s) => s.threads.mainThreadId === s.threadListItem.id,
28
29
  );
29
30
  const { switchTo } = useThreadListItemTrigger();
31
+ const selected = accessibilityState?.selected ?? isActive;
30
32
 
31
33
  return (
32
34
  <Pressable
33
35
  onPress={switchTo}
34
36
  accessibilityRole="button"
35
- accessibilityState={{ selected: isActive, ...accessibilityState }}
37
+ accessibilityState={{ ...accessibilityState, selected }}
38
+ aria-current={Platform.OS === "web" && selected ? "true" : undefined}
36
39
  {...pressableProps}
37
40
  >
38
41
  {typeof children === "function"