@assistant-ui/react-native 0.1.36 → 0.1.38

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 (178) hide show
  1. package/dist/index.d.ts +3 -2
  2. package/dist/index.js +2 -1
  3. package/dist/primitives/actionBar/ActionBarCopy.js +1 -0
  4. package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
  5. package/dist/primitives/actionBar/ActionBarEdit.d.ts +2 -3
  6. package/dist/primitives/actionBar/ActionBarEdit.d.ts.map +1 -1
  7. package/dist/primitives/actionBar/ActionBarEdit.js +1 -0
  8. package/dist/primitives/actionBar/ActionBarEdit.js.map +1 -1
  9. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -0
  10. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
  11. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -0
  12. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
  13. package/dist/primitives/actionBar/ActionBarReload.d.ts +2 -3
  14. package/dist/primitives/actionBar/ActionBarReload.d.ts.map +1 -1
  15. package/dist/primitives/actionBar/ActionBarReload.js +1 -0
  16. package/dist/primitives/actionBar/ActionBarReload.js.map +1 -1
  17. package/dist/primitives/attachment/AttachmentRemove.d.ts +2 -3
  18. package/dist/primitives/attachment/AttachmentRemove.d.ts.map +1 -1
  19. package/dist/primitives/attachment/AttachmentRemove.js +1 -0
  20. package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
  21. package/dist/primitives/branchPicker/BranchPickerNext.d.ts +2 -3
  22. package/dist/primitives/branchPicker/BranchPickerNext.d.ts.map +1 -1
  23. package/dist/primitives/branchPicker/BranchPickerNext.js +1 -0
  24. package/dist/primitives/branchPicker/BranchPickerNext.js.map +1 -1
  25. package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts +2 -3
  26. package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts.map +1 -1
  27. package/dist/primitives/branchPicker/BranchPickerPrevious.js +1 -0
  28. package/dist/primitives/branchPicker/BranchPickerPrevious.js.map +1 -1
  29. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts +2 -3
  30. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts.map +1 -1
  31. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -0
  32. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
  33. package/dist/primitives/composer/ComposerAddAttachment.d.ts +2 -3
  34. package/dist/primitives/composer/ComposerAddAttachment.d.ts.map +1 -1
  35. package/dist/primitives/composer/ComposerAddAttachment.js +1 -0
  36. package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
  37. package/dist/primitives/composer/ComposerCancel.d.ts +2 -3
  38. package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
  39. package/dist/primitives/composer/ComposerCancel.js +1 -0
  40. package/dist/primitives/composer/ComposerCancel.js.map +1 -1
  41. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  42. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  43. package/dist/primitives/composer/ComposerQueue.d.ts +2 -0
  44. package/dist/primitives/composer/ComposerQueue.js +2 -0
  45. package/dist/primitives/composer/ComposerQuote.d.ts +10 -0
  46. package/dist/primitives/composer/ComposerQuote.d.ts.map +1 -0
  47. package/dist/primitives/composer/ComposerQuote.js +38 -0
  48. package/dist/primitives/composer/ComposerQuote.js.map +1 -0
  49. package/dist/primitives/composer/ComposerQuoteDismiss.d.ts +9 -0
  50. package/dist/primitives/composer/ComposerQuoteDismiss.d.ts.map +1 -0
  51. package/dist/primitives/composer/ComposerQuoteDismiss.js +48 -0
  52. package/dist/primitives/composer/ComposerQuoteDismiss.js.map +1 -0
  53. package/dist/primitives/composer/ComposerQuoteText.d.ts +10 -0
  54. package/dist/primitives/composer/ComposerQuoteText.d.ts.map +1 -0
  55. package/dist/primitives/composer/ComposerQuoteText.js +40 -0
  56. package/dist/primitives/composer/ComposerQuoteText.js.map +1 -0
  57. package/dist/primitives/composer/ComposerSend.d.ts +2 -3
  58. package/dist/primitives/composer/ComposerSend.d.ts.map +1 -1
  59. package/dist/primitives/composer/ComposerSend.js +1 -0
  60. package/dist/primitives/composer/ComposerSend.js.map +1 -1
  61. package/dist/primitives/composer.d.ts +6 -2
  62. package/dist/primitives/composer.js +9 -1
  63. package/dist/primitives/composer.js.map +1 -1
  64. package/dist/primitives/message/MessageIf.d.ts +3 -0
  65. package/dist/primitives/message/MessageIf.d.ts.map +1 -1
  66. package/dist/primitives/message/MessageIf.js +3 -0
  67. package/dist/primitives/message/MessageIf.js.map +1 -1
  68. package/dist/primitives/queueItem/QueueItemRemove.d.ts +9 -0
  69. package/dist/primitives/queueItem/QueueItemRemove.d.ts.map +1 -0
  70. package/dist/primitives/queueItem/QueueItemRemove.js +48 -0
  71. package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -0
  72. package/dist/primitives/queueItem/QueueItemSteer.d.ts +9 -0
  73. package/dist/primitives/queueItem/QueueItemSteer.d.ts.map +1 -0
  74. package/dist/primitives/queueItem/QueueItemSteer.js +48 -0
  75. package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -0
  76. package/dist/primitives/queueItem/QueueItemText.d.ts +10 -0
  77. package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -0
  78. package/dist/primitives/queueItem/QueueItemText.js +45 -0
  79. package/dist/primitives/queueItem/QueueItemText.js.map +1 -0
  80. package/dist/primitives/queueItem.d.ts +9 -0
  81. package/dist/primitives/queueItem.d.ts.map +1 -0
  82. package/dist/primitives/queueItem.js +14 -0
  83. package/dist/primitives/queueItem.js.map +1 -0
  84. package/dist/primitives/suggestion/SuggestionTrigger.d.ts +2 -3
  85. package/dist/primitives/suggestion/SuggestionTrigger.d.ts.map +1 -1
  86. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -0
  87. package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
  88. package/dist/primitives/thread/ThreadEmpty.d.ts +4 -1
  89. package/dist/primitives/thread/ThreadEmpty.d.ts.map +1 -1
  90. package/dist/primitives/thread/ThreadEmpty.js +12 -4
  91. package/dist/primitives/thread/ThreadEmpty.js.map +1 -1
  92. package/dist/primitives/thread/ThreadIf.d.ts +3 -0
  93. package/dist/primitives/thread/ThreadIf.d.ts.map +1 -1
  94. package/dist/primitives/thread/ThreadIf.js +5 -6
  95. package/dist/primitives/thread/ThreadIf.js.map +1 -1
  96. package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
  97. package/dist/primitives/thread/ThreadMessages.js +2 -2
  98. package/dist/primitives/thread/ThreadMessages.js.map +1 -1
  99. package/dist/primitives/thread/ThreadSuggestion.d.ts +2 -3
  100. package/dist/primitives/thread/ThreadSuggestion.d.ts.map +1 -1
  101. package/dist/primitives/thread/ThreadSuggestion.js +1 -0
  102. package/dist/primitives/thread/ThreadSuggestion.js.map +1 -1
  103. package/dist/primitives/threadList/ThreadListItems.d.ts.map +1 -1
  104. package/dist/primitives/threadList/ThreadListItems.js +8 -3
  105. package/dist/primitives/threadList/ThreadListItems.js.map +1 -1
  106. package/dist/primitives/threadList/ThreadListNew.d.ts +6 -4
  107. package/dist/primitives/threadList/ThreadListNew.d.ts.map +1 -1
  108. package/dist/primitives/threadList/ThreadListNew.js +46 -15
  109. package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
  110. package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts +2 -3
  111. package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts.map +1 -1
  112. package/dist/primitives/threadListItem/ThreadListItemArchive.js +1 -0
  113. package/dist/primitives/threadListItem/ThreadListItemArchive.js.map +1 -1
  114. package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts +2 -3
  115. package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts.map +1 -1
  116. package/dist/primitives/threadListItem/ThreadListItemDelete.js +1 -0
  117. package/dist/primitives/threadListItem/ThreadListItemDelete.js.map +1 -1
  118. package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts +6 -4
  119. package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts.map +1 -1
  120. package/dist/primitives/threadListItem/ThreadListItemTrigger.js +46 -15
  121. package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
  122. package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts +2 -3
  123. package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts.map +1 -1
  124. package/dist/primitives/threadListItem/ThreadListItemUnarchive.js +1 -0
  125. package/dist/primitives/threadListItem/ThreadListItemUnarchive.js.map +1 -1
  126. package/package.json +9 -8
  127. package/src/index.ts +2 -0
  128. package/src/primitives/accessibilityRole.test.tsx +335 -0
  129. package/src/primitives/actionBar/ActionBarCopy.tsx +1 -0
  130. package/src/primitives/actionBar/ActionBarEdit.tsx +6 -3
  131. package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +1 -1
  132. package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +1 -1
  133. package/src/primitives/actionBar/ActionBarReload.tsx +6 -3
  134. package/src/primitives/attachment/AttachmentRemove.tsx +11 -4
  135. package/src/primitives/branchPicker/BranchPickerNext.tsx +6 -3
  136. package/src/primitives/branchPicker/BranchPickerPrevious.tsx +6 -3
  137. package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.tsx +4 -4
  138. package/src/primitives/composer/ComposerAddAttachment.tsx +10 -4
  139. package/src/primitives/composer/ComposerCancel.tsx +6 -3
  140. package/src/primitives/composer/ComposerInput.tsx +17 -15
  141. package/src/primitives/composer/ComposerQueue.test.tsx +9 -0
  142. package/src/primitives/composer/ComposerQueue.tsx +1 -0
  143. package/src/primitives/composer/ComposerQuote.test.tsx +163 -0
  144. package/src/primitives/composer/ComposerQuote.tsx +17 -0
  145. package/src/primitives/composer/ComposerQuoteDismiss.tsx +31 -0
  146. package/src/primitives/composer/ComposerQuoteText.tsx +17 -0
  147. package/src/primitives/composer/ComposerSend.test.tsx +26 -0
  148. package/src/primitives/composer/ComposerSend.tsx +3 -3
  149. package/src/primitives/composer.ts +13 -0
  150. package/src/primitives/message/MessageIf.tsx +3 -0
  151. package/src/primitives/queueItem/QueueItem.test.tsx +132 -0
  152. package/src/primitives/queueItem/QueueItemRemove.tsx +31 -0
  153. package/src/primitives/queueItem/QueueItemSteer.tsx +31 -0
  154. package/src/primitives/queueItem/QueueItemText.tsx +22 -0
  155. package/src/primitives/queueItem.ts +12 -0
  156. package/src/primitives/suggestion/SuggestionTrigger.tsx +6 -3
  157. package/src/primitives/thread/ThreadEmpty.test.tsx +17 -3
  158. package/src/primitives/thread/ThreadEmpty.tsx +7 -6
  159. package/src/primitives/thread/ThreadIf.test.tsx +20 -20
  160. package/src/primitives/thread/ThreadIf.tsx +5 -8
  161. package/src/primitives/thread/ThreadMessages.test.tsx +18 -4
  162. package/src/primitives/thread/ThreadMessages.tsx +3 -2
  163. package/src/primitives/thread/ThreadSuggestion.tsx +6 -3
  164. package/src/primitives/threadList/ThreadListItems.test.tsx +166 -0
  165. package/src/primitives/threadList/ThreadListItems.tsx +6 -1
  166. package/src/primitives/threadList/ThreadListNew.test.tsx +118 -3
  167. package/src/primitives/threadList/ThreadListNew.tsx +28 -5
  168. package/src/primitives/threadListItem/ThreadListItemArchive.tsx +6 -4
  169. package/src/primitives/threadListItem/ThreadListItemDelete.tsx +10 -4
  170. package/src/primitives/threadListItem/ThreadListItemTrigger.test.tsx +74 -0
  171. package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +28 -5
  172. package/src/primitives/threadListItem/ThreadListItemUnarchive.tsx +10 -4
  173. package/dist/runtimes/RemoteThreadListHookInstanceManager.d.ts +0 -2
  174. package/dist/runtimes/RemoteThreadListHookInstanceManager.js +0 -2
  175. package/dist/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +0 -2
  176. package/dist/runtimes/RemoteThreadListThreadListRuntimeCore.js +0 -2
  177. package/src/runtimes/RemoteThreadListHookInstanceManager.tsx +0 -1
  178. package/src/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +0 -1
@@ -0,0 +1,31 @@
1
+ import { useCallback } from "react";
2
+ import { Pressable, type PressableProps } from "react-native";
3
+ import { useAui } from "@assistant-ui/store";
4
+
5
+ export type QueueItemSteerProps = Omit<
6
+ PressableProps,
7
+ "onPress" | "children"
8
+ > & {
9
+ children: PressableProps["children"];
10
+ };
11
+
12
+ export const QueueItemSteer = ({
13
+ children,
14
+ ...pressableProps
15
+ }: QueueItemSteerProps) => {
16
+ const aui = useAui();
17
+
18
+ const handleSteer = useCallback(() => {
19
+ aui.queueItem.steer();
20
+ }, [aui]);
21
+
22
+ return (
23
+ <Pressable
24
+ onPress={handleSteer}
25
+ accessibilityRole="button"
26
+ {...pressableProps}
27
+ >
28
+ {children}
29
+ </Pressable>
30
+ );
31
+ };
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from "react";
2
+ import { Text, type TextProps } from "react-native";
3
+ import { useAuiState } from "@assistant-ui/store";
4
+
5
+ export type QueueItemTextProps = TextProps & {
6
+ children?: ReactNode;
7
+ };
8
+
9
+ export const QueueItemText = ({
10
+ children,
11
+ ...textProps
12
+ }: QueueItemTextProps) => {
13
+ const text = useAuiState((s) =>
14
+ // hosts on the pre-parts adapter shape may omit the field at runtime
15
+ (s.queueItem.parts ?? [])
16
+ .filter((part) => part.type === "text")
17
+ .map((part) => part.text)
18
+ .join("\n\n"),
19
+ );
20
+
21
+ return <Text {...textProps}>{children ?? text}</Text>;
22
+ };
@@ -0,0 +1,12 @@
1
+ export {
2
+ QueueItemText as Text,
3
+ type QueueItemTextProps as TextProps,
4
+ } from "./queueItem/QueueItemText";
5
+ export {
6
+ QueueItemRemove as Remove,
7
+ type QueueItemRemoveProps as RemoveProps,
8
+ } from "./queueItem/QueueItemRemove";
9
+ export {
10
+ QueueItemSteer as Steer,
11
+ type QueueItemSteerProps as SteerProps,
12
+ } from "./queueItem/QueueItemSteer";
@@ -1,10 +1,12 @@
1
- import type { ReactNode } from "react";
2
1
  import { Pressable, type PressableProps } from "react-native";
3
2
  import { useAuiState } from "@assistant-ui/store";
4
3
  import { useSuggestionTrigger } from "@assistant-ui/core/react";
5
4
 
6
- export type SuggestionTriggerProps = Omit<PressableProps, "onPress"> & {
7
- children: ReactNode;
5
+ export type SuggestionTriggerProps = Omit<
6
+ PressableProps,
7
+ "onPress" | "children"
8
+ > & {
9
+ children: PressableProps["children"];
8
10
  /**
9
11
  * When true, automatically sends the message.
10
12
  * When false, replaces or appends the composer text with the suggestion.
@@ -36,6 +38,7 @@ export const SuggestionTrigger = ({
36
38
  <Pressable
37
39
  onPress={trigger}
38
40
  disabled={disabledProp ?? disabled}
41
+ accessibilityRole="button"
39
42
  {...pressableProps}
40
43
  >
41
44
  {children}
@@ -1,16 +1,30 @@
1
+ import type { ReactNode } from "react";
1
2
  import { act } from "react";
2
3
  import { createRoot, type Root } from "react-dom/client";
3
4
  import { Text } from "react-native";
4
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import type * as Store from "@assistant-ui/store";
5
7
  import { ThreadEmpty } from "./ThreadEmpty";
6
8
 
7
9
  const h = vi.hoisted(() => ({
8
10
  isEmpty: true,
9
11
  }));
10
12
 
11
- vi.mock("@assistant-ui/core/react", () => ({
12
- useThreadIsEmpty: () => h.isEmpty,
13
- }));
13
+ type ThreadSlice = { thread: { isEmpty: boolean } };
14
+
15
+ vi.mock("@assistant-ui/store", async (importOriginal) => {
16
+ const actual = await importOriginal<typeof Store>();
17
+ return {
18
+ ...actual,
19
+ AuiIf: ({
20
+ condition,
21
+ children,
22
+ }: {
23
+ condition: (s: ThreadSlice) => boolean;
24
+ children: ReactNode;
25
+ }) => (condition({ thread: { isEmpty: h.isEmpty } }) ? children : null),
26
+ };
27
+ });
14
28
 
15
29
  (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
16
30
 
@@ -1,12 +1,13 @@
1
1
  import type { ReactNode } from "react";
2
- import { useThreadIsEmpty } from "@assistant-ui/core/react";
2
+ import { AuiIf } from "@assistant-ui/store";
3
3
 
4
4
  export type ThreadEmptyProps = {
5
5
  children: ReactNode;
6
6
  };
7
7
 
8
- export const ThreadEmpty = ({ children }: ThreadEmptyProps) => {
9
- const isEmpty = useThreadIsEmpty();
10
- if (!isEmpty) return null;
11
- return <>{children}</>;
12
- };
8
+ /**
9
+ * @deprecated Use `<AuiIf condition={(s) => s.thread.isEmpty} />` instead.
10
+ */
11
+ export const ThreadEmpty = ({ children }: ThreadEmptyProps) => (
12
+ <AuiIf condition={(s) => s.thread.isEmpty}>{children}</AuiIf>
13
+ );
@@ -2,25 +2,29 @@ import { act } from "react";
2
2
  import { createRoot, type Root } from "react-dom/client";
3
3
  import { Text } from "react-native";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import type * as Store from "@assistant-ui/store";
5
6
  import { ThreadIf } from "./ThreadIf";
6
7
 
7
8
  const h = vi.hoisted(() => ({
8
- thread: { messages: [] as unknown[], isRunning: false },
9
+ thread: { isEmpty: true, isRunning: false },
9
10
  }));
10
11
 
11
- vi.mock("@assistant-ui/store", () => ({
12
- useAuiState: <T,>(selector: (s: { thread: typeof h.thread }) => T) =>
13
- selector({ thread: h.thread }),
14
- }));
12
+ vi.mock("@assistant-ui/store", async (importOriginal) => {
13
+ const actual = await importOriginal<typeof Store>();
14
+ return {
15
+ ...actual,
16
+ useAuiState: <T,>(selector: (s: { thread: typeof h.thread }) => T) =>
17
+ selector({ thread: h.thread }),
18
+ };
19
+ });
15
20
 
16
21
  (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
17
22
 
18
23
  describe("ThreadIf", () => {
19
24
  let container: HTMLDivElement;
20
25
  let root: Root;
21
-
22
26
  beforeEach(() => {
23
- h.thread.messages = [];
27
+ h.thread.isEmpty = true;
24
28
  h.thread.isRunning = false;
25
29
 
26
30
  container = document.createElement("div");
@@ -46,33 +50,29 @@ describe("ThreadIf", () => {
46
50
  return container.querySelector('[data-testid="child"]');
47
51
  };
48
52
 
49
- const setMessages = (count: number) => {
50
- h.thread.messages = Array.from({ length: count }, (_, i) => i);
51
- };
52
-
53
53
  it("renders children when no guard is set", async () => {
54
- setMessages(2);
54
+ h.thread.isEmpty = false;
55
55
  expect(await mount()).not.toBeNull();
56
56
  });
57
57
 
58
58
  describe("empty guard", () => {
59
59
  it("renders children when empty:true matches an empty thread", async () => {
60
- setMessages(0);
60
+ h.thread.isEmpty = true;
61
61
  expect(await mount({ empty: true })).not.toBeNull();
62
62
  });
63
63
 
64
- it("hides children when empty:true but the thread has messages", async () => {
65
- setMessages(3);
64
+ it("hides children when empty:true but the thread is not empty", async () => {
65
+ h.thread.isEmpty = false;
66
66
  expect(await mount({ empty: true })).toBeNull();
67
67
  });
68
68
 
69
69
  it("renders children when empty:false matches a non-empty thread", async () => {
70
- setMessages(3);
70
+ h.thread.isEmpty = false;
71
71
  expect(await mount({ empty: false })).not.toBeNull();
72
72
  });
73
73
 
74
74
  it("hides children when empty:false but the thread is empty", async () => {
75
- setMessages(0);
75
+ h.thread.isEmpty = true;
76
76
  expect(await mount({ empty: false })).toBeNull();
77
77
  });
78
78
  });
@@ -101,19 +101,19 @@ describe("ThreadIf", () => {
101
101
 
102
102
  describe("combined guards", () => {
103
103
  it("renders children only when both empty and running match", async () => {
104
- setMessages(0);
104
+ h.thread.isEmpty = true;
105
105
  h.thread.isRunning = true;
106
106
  expect(await mount({ empty: true, running: true })).not.toBeNull();
107
107
  });
108
108
 
109
109
  it("hides children when empty matches but running does not", async () => {
110
- setMessages(0);
110
+ h.thread.isEmpty = true;
111
111
  h.thread.isRunning = false;
112
112
  expect(await mount({ empty: true, running: true })).toBeNull();
113
113
  });
114
114
 
115
115
  it("hides children when running matches but empty does not", async () => {
116
- setMessages(2);
116
+ h.thread.isEmpty = false;
117
117
  h.thread.isRunning = true;
118
118
  expect(await mount({ empty: true, running: true })).toBeNull();
119
119
  });
@@ -7,17 +7,14 @@ export type ThreadIfProps = {
7
7
  running?: boolean | undefined;
8
8
  };
9
9
 
10
+ /**
11
+ * @deprecated Use `<AuiIf condition={(s) => s.thread...} />` instead.
12
+ */
10
13
  export const ThreadIf = ({ children, empty, running }: ThreadIfProps) => {
11
14
  const thread = useAuiState((s) => s.thread);
12
15
 
13
- if (empty !== undefined) {
14
- const isEmpty = thread.messages.length === 0;
15
- if (empty !== isEmpty) return null;
16
- }
17
-
18
- if (running !== undefined) {
19
- if (running !== thread.isRunning) return null;
20
- }
16
+ if (empty !== undefined && empty !== thread.isEmpty) return null;
17
+ if (running !== undefined && running !== thread.isRunning) return null;
21
18
 
22
19
  return <>{children}</>;
23
20
  };
@@ -273,6 +273,20 @@ describe("ThreadMessages", () => {
273
273
  expect(container.querySelector('[data-testid="c-message"]')).toBeNull();
274
274
  });
275
275
 
276
+ it("renders nothing for an editing system role with no system or Message component", async () => {
277
+ h.state.thread.messages = [{ id: "1", role: "system" }];
278
+ h.state.message.role = "system";
279
+ h.state.message.composer.isEditing = true;
280
+ const components = makeComponents() as Record<string, unknown>;
281
+ delete components.SystemEditComposer;
282
+ delete components.EditComposer;
283
+ delete components.SystemMessage;
284
+ delete components.Message;
285
+ await mount({ components: components as never });
286
+ expect(container.querySelector('[data-testid="c-system"]')).toBeNull();
287
+ expect(container.querySelector('[data-testid="c-message"]')).toBeNull();
288
+ });
289
+
276
290
  it("throws for an unknown role", async () => {
277
291
  h.state.thread.messages = [{ id: "1", role: "ghost" }];
278
292
  h.state.message.role = "ghost";
@@ -354,7 +368,7 @@ describe("ThreadMessages", () => {
354
368
  props.onContentSizeChange?.(0, 140);
355
369
  });
356
370
  await emit("thread.runStart");
357
- await emit("threadListItem.switchedTo");
371
+ await emit("threads.selectionChanged");
358
372
 
359
373
  expect(h.scrollToEnd).not.toHaveBeenCalled();
360
374
  });
@@ -442,7 +456,7 @@ describe("ThreadMessages", () => {
442
456
  const props = getFlatListProps();
443
457
  h.scrollToEnd.mockClear();
444
458
 
445
- await emit("threadListItem.switchedTo");
459
+ await emit("threads.selectionChanged");
446
460
  expect(h.scrollToEnd).toHaveBeenCalledTimes(1);
447
461
 
448
462
  await act(async () => {
@@ -560,7 +574,7 @@ describe("ThreadMessages", () => {
560
574
  await mountFlatList({ components: messageComponents });
561
575
  h.scrollToEnd.mockClear();
562
576
 
563
- await emit("threadListItem.switchedTo");
577
+ await emit("threads.selectionChanged");
564
578
 
565
579
  expect(h.scrollToEnd).toHaveBeenCalledWith({ animated: false });
566
580
  });
@@ -573,7 +587,7 @@ describe("ThreadMessages", () => {
573
587
  });
574
588
  h.scrollToEnd.mockClear();
575
589
 
576
- await emit("threadListItem.switchedTo");
590
+ await emit("threads.selectionChanged");
577
591
 
578
592
  h.state.thread.messages = [
579
593
  { id: "2", role: "user" },
@@ -111,7 +111,8 @@ const getComponent = (
111
111
  components.SystemEditComposer ??
112
112
  components.EditComposer ??
113
113
  components.SystemMessage ??
114
- (components.Message as ComponentType)
114
+ (components.Message as ComponentType) ??
115
+ DEFAULT_SYSTEM_MESSAGE
115
116
  );
116
117
  } else {
117
118
  return (
@@ -348,7 +349,7 @@ const useThreadMessagesFlatListAutoScroll = ({
348
349
  scrollToBottom(true);
349
350
  });
350
351
 
351
- useAuiEvent("threadListItem.switchedTo", () => {
352
+ useAuiEvent("threads.selectionChanged", () => {
352
353
  if (!scrollToBottomOnThreadSwitch) return;
353
354
  initializeScrollRequestedRef.current = false;
354
355
  lastScrollEventYRef.current = 0;
@@ -1,9 +1,11 @@
1
- import type { ReactNode } from "react";
2
1
  import { Pressable, type PressableProps } from "react-native";
3
2
  import { useSuggestionTrigger } from "@assistant-ui/core/react";
4
3
 
5
- export type ThreadSuggestionProps = Omit<PressableProps, "onPress"> & {
6
- children: ReactNode;
4
+ export type ThreadSuggestionProps = Omit<
5
+ PressableProps,
6
+ "onPress" | "children"
7
+ > & {
8
+ children: PressableProps["children"];
7
9
  /** The suggestion prompt. */
8
10
  prompt: string;
9
11
  /**
@@ -37,6 +39,7 @@ export const ThreadSuggestion = ({
37
39
  <Pressable
38
40
  onPress={trigger}
39
41
  disabled={disabledProp ?? disabled}
42
+ accessibilityRole="button"
40
43
  {...pressableProps}
41
44
  >
42
45
  {children}
@@ -0,0 +1,166 @@
1
+ import { act } from "react";
2
+ import { createRoot, type Root } from "react-dom/client";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { Text } from "react-native";
5
+ import { ThreadListItems } from "./ThreadListItems";
6
+
7
+ const h = vi.hoisted(() => ({
8
+ state: { threads: { threadIds: [] as string[] } },
9
+ flatListProps: null as Record<string, unknown> | null,
10
+ providerProps: vi.fn(),
11
+ }));
12
+
13
+ vi.mock("react-native", async (importOriginal) => {
14
+ const actual = await importOriginal<typeof import("react-native")>();
15
+ const React = await import("react");
16
+
17
+ const FlatListMock = React.forwardRef(function FlatListMock(
18
+ props: Record<string, unknown>,
19
+ _ref,
20
+ ) {
21
+ h.flatListProps = props;
22
+
23
+ const data = (props.data as unknown[]) ?? [];
24
+ const renderItem = props.renderItem as
25
+ | ((value: { item: unknown; index: number }) => React.ReactNode)
26
+ | undefined;
27
+ const keyExtractor = props.keyExtractor as
28
+ | ((item: unknown, index: number) => string)
29
+ | undefined;
30
+
31
+ return React.createElement(
32
+ "div",
33
+ { "data-testid": "flatlist" },
34
+ data.map((item, index) =>
35
+ React.createElement(
36
+ "div",
37
+ { key: keyExtractor?.(item, index) ?? index },
38
+ renderItem?.({ item, index }),
39
+ ),
40
+ ),
41
+ );
42
+ });
43
+
44
+ return {
45
+ ...actual,
46
+ FlatList: FlatListMock,
47
+ };
48
+ });
49
+
50
+ vi.mock("@assistant-ui/store", async (importOriginal) => {
51
+ const actual = await importOriginal<typeof import("@assistant-ui/store")>();
52
+ return {
53
+ ...actual,
54
+ useAuiState: <T,>(selector: (s: typeof h.state) => T) => selector(h.state),
55
+ };
56
+ });
57
+
58
+ vi.mock("@assistant-ui/core/react", async (importOriginal) => {
59
+ const actual =
60
+ await importOriginal<typeof import("@assistant-ui/core/react")>();
61
+ return {
62
+ ...actual,
63
+ ThreadListItemByIndexProvider: ({
64
+ index,
65
+ archived,
66
+ children,
67
+ }: {
68
+ index: number;
69
+ archived: boolean;
70
+ children?: React.ReactNode;
71
+ }) => {
72
+ h.providerProps({ index, archived });
73
+ return <>{children}</>;
74
+ },
75
+ };
76
+ });
77
+
78
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
79
+
80
+ describe("ThreadListItems", () => {
81
+ let container: HTMLDivElement;
82
+ let root: Root;
83
+
84
+ beforeEach(() => {
85
+ h.state.threads.threadIds = [];
86
+ h.flatListProps = null;
87
+ h.providerProps.mockReset();
88
+
89
+ container = document.createElement("div");
90
+ document.body.appendChild(container);
91
+ root = createRoot(container);
92
+ });
93
+
94
+ afterEach(async () => {
95
+ await act(async () => {
96
+ root.unmount();
97
+ });
98
+ container.remove();
99
+ });
100
+
101
+ const mount = async (
102
+ props: Partial<Parameters<typeof ThreadListItems>[0]> = {},
103
+ ) => {
104
+ await act(async () => {
105
+ root.render(
106
+ <ThreadListItems
107
+ renderItem={({ threadId }) => <Text>{threadId}</Text>}
108
+ {...props}
109
+ />,
110
+ );
111
+ });
112
+ };
113
+
114
+ it("wraps each row in ThreadListItemByIndexProvider with its index", async () => {
115
+ h.state.threads.threadIds = ["t-1", "t-2", "t-3"];
116
+
117
+ await mount();
118
+
119
+ expect(h.providerProps.mock.calls.map((call) => call[0])).toEqual([
120
+ { index: 0, archived: false },
121
+ { index: 1, archived: false },
122
+ { index: 2, archived: false },
123
+ ]);
124
+ expect(container.textContent).toContain("t-1");
125
+ expect(container.textContent).toContain("t-3");
126
+ });
127
+
128
+ it("passes threadId and index to renderItem", async () => {
129
+ const renderItem = vi.fn(
130
+ ({ threadId }: { threadId: string; index: number }) => (
131
+ <Text>{threadId}</Text>
132
+ ),
133
+ );
134
+ h.state.threads.threadIds = ["a", "b"];
135
+
136
+ await mount({ renderItem });
137
+
138
+ expect(renderItem.mock.calls.map((call) => call[0])).toEqual([
139
+ { threadId: "a", index: 0 },
140
+ { threadId: "b", index: 1 },
141
+ ]);
142
+ });
143
+
144
+ it("keys rows by threadId via keyExtractor", async () => {
145
+ h.state.threads.threadIds = ["t-1", "t-2"];
146
+
147
+ await mount();
148
+
149
+ const keyExtractor = h.flatListProps?.keyExtractor as (
150
+ item: string,
151
+ index: number,
152
+ ) => string;
153
+ expect(keyExtractor("t-1", 0)).toBe("t-1");
154
+ expect(keyExtractor("t-2", 1)).toBe("t-2");
155
+ expect(h.flatListProps?.data).toEqual(["t-1", "t-2"]);
156
+ });
157
+
158
+ it("forwards extra FlatList props", async () => {
159
+ h.state.threads.threadIds = ["t-1"];
160
+
161
+ await mount({ horizontal: true, testID: "thread-list" });
162
+
163
+ expect(h.flatListProps?.horizontal).toBe(true);
164
+ expect(h.flatListProps?.testID).toBe("thread-list");
165
+ });
166
+ });
@@ -1,6 +1,7 @@
1
1
  import { type ReactElement, useCallback } from "react";
2
2
  import { FlatList, type FlatListProps } from "react-native";
3
3
  import { useAuiState } from "@assistant-ui/store";
4
+ import { ThreadListItemByIndexProvider } from "@assistant-ui/core/react";
4
5
 
5
6
  export type ThreadListItemsProps = Omit<
6
7
  FlatListProps<string>,
@@ -17,7 +18,11 @@ export const ThreadListItems = ({
17
18
 
18
19
  const renderFlatListItem = useCallback(
19
20
  ({ item, index }: { item: string; index: number }) => {
20
- return renderItem({ threadId: item, index });
21
+ return (
22
+ <ThreadListItemByIndexProvider index={index} archived={false}>
23
+ {renderItem({ threadId: item, index })}
24
+ </ThreadListItemByIndexProvider>
25
+ );
21
26
  },
22
27
  [renderItem],
23
28
  );