@assistant-ui/vue 0.0.0 → 0.0.2

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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -2
  3. package/dist/AuiIf.d.ts +27 -0
  4. package/dist/AuiIf.d.ts.map +1 -0
  5. package/dist/AuiIf.js +30 -0
  6. package/dist/AuiIf.js.map +1 -0
  7. package/dist/AuiProvider.d.ts +41 -0
  8. package/dist/AuiProvider.d.ts.map +1 -0
  9. package/dist/AuiProvider.js +61 -0
  10. package/dist/AuiProvider.js.map +1 -0
  11. package/dist/context.d.ts +10 -0
  12. package/dist/context.d.ts.map +1 -0
  13. package/dist/context.js +17 -0
  14. package/dist/context.js.map +1 -0
  15. package/dist/index.d.ts +34 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +35 -0
  18. package/dist/primitives/AttachmentByIndexProvider.d.ts +30 -0
  19. package/dist/primitives/AttachmentByIndexProvider.d.ts.map +1 -0
  20. package/dist/primitives/AttachmentByIndexProvider.js +63 -0
  21. package/dist/primitives/AttachmentByIndexProvider.js.map +1 -0
  22. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts +11 -0
  23. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts.map +1 -0
  24. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js +31 -0
  25. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js.map +1 -0
  26. package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts +19 -0
  27. package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts.map +1 -0
  28. package/dist/primitives/ChainOfThoughtPrimitiveParts.js +85 -0
  29. package/dist/primitives/ChainOfThoughtPrimitiveParts.js.map +1 -0
  30. package/dist/primitives/ComposerPrimitiveCancel.d.ts +12 -0
  31. package/dist/primitives/ComposerPrimitiveCancel.d.ts.map +1 -0
  32. package/dist/primitives/ComposerPrimitiveCancel.js +33 -0
  33. package/dist/primitives/ComposerPrimitiveCancel.js.map +1 -0
  34. package/dist/primitives/ComposerPrimitiveInput.d.ts +26 -0
  35. package/dist/primitives/ComposerPrimitiveInput.d.ts.map +1 -0
  36. package/dist/primitives/ComposerPrimitiveInput.js +57 -0
  37. package/dist/primitives/ComposerPrimitiveInput.js.map +1 -0
  38. package/dist/primitives/ComposerPrimitiveSend.d.ts +13 -0
  39. package/dist/primitives/ComposerPrimitiveSend.d.ts.map +1 -0
  40. package/dist/primitives/ComposerPrimitiveSend.js +31 -0
  41. package/dist/primitives/ComposerPrimitiveSend.js.map +1 -0
  42. package/dist/primitives/MessageByIdProvider.d.ts +23 -0
  43. package/dist/primitives/MessageByIdProvider.d.ts.map +1 -0
  44. package/dist/primitives/MessageByIdProvider.js +70 -0
  45. package/dist/primitives/MessageByIdProvider.js.map +1 -0
  46. package/dist/primitives/MessagePrimitiveParts.d.ts +51 -0
  47. package/dist/primitives/MessagePrimitiveParts.d.ts.map +1 -0
  48. package/dist/primitives/MessagePrimitiveParts.js +93 -0
  49. package/dist/primitives/MessagePrimitiveParts.js.map +1 -0
  50. package/dist/primitives/PartByIndexProvider.d.ts +21 -0
  51. package/dist/primitives/PartByIndexProvider.d.ts.map +1 -0
  52. package/dist/primitives/PartByIndexProvider.js +50 -0
  53. package/dist/primitives/PartByIndexProvider.js.map +1 -0
  54. package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts +7 -0
  55. package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts.map +1 -0
  56. package/dist/primitives/ThreadListItemPrimitiveRoot.js +36 -0
  57. package/dist/primitives/ThreadListItemPrimitiveRoot.js.map +1 -0
  58. package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts +14 -0
  59. package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts.map +1 -0
  60. package/dist/primitives/ThreadListItemPrimitiveTrigger.js +58 -0
  61. package/dist/primitives/ThreadListItemPrimitiveTrigger.js.map +1 -0
  62. package/dist/primitives/ThreadListPrimitiveRoot.d.ts +7 -0
  63. package/dist/primitives/ThreadListPrimitiveRoot.d.ts.map +1 -0
  64. package/dist/primitives/ThreadListPrimitiveRoot.js +16 -0
  65. package/dist/primitives/ThreadListPrimitiveRoot.js.map +1 -0
  66. package/dist/primitives/ThreadPrimitiveMessages.d.ts +21 -0
  67. package/dist/primitives/ThreadPrimitiveMessages.d.ts.map +1 -0
  68. package/dist/primitives/ThreadPrimitiveMessages.js +35 -0
  69. package/dist/primitives/ThreadPrimitiveMessages.js.map +1 -0
  70. package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts +25 -0
  71. package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts.map +1 -0
  72. package/dist/primitives/ThreadPrimitiveScrollToBottom.js +43 -0
  73. package/dist/primitives/ThreadPrimitiveScrollToBottom.js.map +1 -0
  74. package/dist/primitives/ThreadPrimitiveViewport.d.ts +57 -0
  75. package/dist/primitives/ThreadPrimitiveViewport.d.ts.map +1 -0
  76. package/dist/primitives/ThreadPrimitiveViewport.js +99 -0
  77. package/dist/primitives/ThreadPrimitiveViewport.js.map +1 -0
  78. package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts +13 -0
  79. package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts.map +1 -0
  80. package/dist/primitives/ThreadPrimitiveViewportFooter.js +46 -0
  81. package/dist/primitives/ThreadPrimitiveViewportFooter.js.map +1 -0
  82. package/dist/primitives/actionBar.d.ts +41 -0
  83. package/dist/primitives/actionBar.d.ts.map +1 -0
  84. package/dist/primitives/actionBar.js +110 -0
  85. package/dist/primitives/actionBar.js.map +1 -0
  86. package/dist/primitives/attachment.d.ts +27 -0
  87. package/dist/primitives/attachment.d.ts.map +1 -0
  88. package/dist/primitives/attachment.js +63 -0
  89. package/dist/primitives/attachment.js.map +1 -0
  90. package/dist/primitives/attrDisabled.d.ts +2 -0
  91. package/dist/primitives/attrDisabled.d.ts.map +1 -0
  92. package/dist/primitives/attrDisabled.js +6 -0
  93. package/dist/primitives/attrDisabled.js.map +1 -0
  94. package/dist/primitives/branchPicker.d.ts +18 -0
  95. package/dist/primitives/branchPicker.d.ts.map +1 -0
  96. package/dist/primitives/branchPicker.js +48 -0
  97. package/dist/primitives/branchPicker.js.map +1 -0
  98. package/dist/primitives/composerAttachments.d.ts +57 -0
  99. package/dist/primitives/composerAttachments.d.ts.map +1 -0
  100. package/dist/primitives/composerAttachments.js +132 -0
  101. package/dist/primitives/composerAttachments.js.map +1 -0
  102. package/dist/primitives/error.d.ts +15 -0
  103. package/dist/primitives/error.d.ts.map +1 -0
  104. package/dist/primitives/error.js +29 -0
  105. package/dist/primitives/error.js.map +1 -0
  106. package/dist/primitives/lastValidCache.d.ts +6 -0
  107. package/dist/primitives/lastValidCache.d.ts.map +1 -0
  108. package/dist/primitives/lastValidCache.js +9 -0
  109. package/dist/primitives/lastValidCache.js.map +1 -0
  110. package/dist/primitives/message.d.ts +12 -0
  111. package/dist/primitives/message.d.ts.map +1 -0
  112. package/dist/primitives/message.js +44 -0
  113. package/dist/primitives/message.js.map +1 -0
  114. package/dist/primitives/messageAttachments.d.ts +12 -0
  115. package/dist/primitives/messageAttachments.d.ts.map +1 -0
  116. package/dist/primitives/messageAttachments.js +27 -0
  117. package/dist/primitives/messageAttachments.js.map +1 -0
  118. package/dist/primitives/stableKeys.d.ts +5 -0
  119. package/dist/primitives/stableKeys.d.ts.map +1 -0
  120. package/dist/primitives/stableKeys.js +14 -0
  121. package/dist/primitives/stableKeys.js.map +1 -0
  122. package/dist/primitives/suggestions.d.ts +66 -0
  123. package/dist/primitives/suggestions.d.ts.map +1 -0
  124. package/dist/primitives/suggestions.js +137 -0
  125. package/dist/primitives/suggestions.js.map +1 -0
  126. package/dist/primitives/thread.d.ts +11 -0
  127. package/dist/primitives/thread.d.ts.map +1 -0
  128. package/dist/primitives/thread.js +37 -0
  129. package/dist/primitives/thread.js.map +1 -0
  130. package/dist/primitives/threadList.d.ts +82 -0
  131. package/dist/primitives/threadList.d.ts.map +1 -0
  132. package/dist/primitives/threadList.js +128 -0
  133. package/dist/primitives/threadList.js.map +1 -0
  134. package/dist/primitives/threadListFocusGroup.d.ts +14 -0
  135. package/dist/primitives/threadListFocusGroup.d.ts.map +1 -0
  136. package/dist/primitives/threadListFocusGroup.js +32 -0
  137. package/dist/primitives/threadListFocusGroup.js.map +1 -0
  138. package/dist/primitives/threadListStructural.d.ts +29 -0
  139. package/dist/primitives/threadListStructural.d.ts.map +1 -0
  140. package/dist/primitives/threadListStructural.js +55 -0
  141. package/dist/primitives/threadListStructural.js.map +1 -0
  142. package/dist/primitives/useComposerSendState.d.ts +6 -0
  143. package/dist/primitives/useComposerSendState.d.ts.map +1 -0
  144. package/dist/primitives/useComposerSendState.js +15 -0
  145. package/dist/primitives/useComposerSendState.js.map +1 -0
  146. package/dist/primitives/viewportContext.d.ts +12 -0
  147. package/dist/primitives/viewportContext.d.ts.map +1 -0
  148. package/dist/primitives/viewportContext.js +6 -0
  149. package/dist/primitives/viewportContext.js.map +1 -0
  150. package/dist/useAui.d.ts +15 -0
  151. package/dist/useAui.d.ts.map +1 -0
  152. package/dist/useAui.js +21 -0
  153. package/dist/useAui.js.map +1 -0
  154. package/dist/useAuiEvent.d.ts +17 -0
  155. package/dist/useAuiEvent.d.ts.map +1 -0
  156. package/dist/useAuiEvent.js +52 -0
  157. package/dist/useAuiEvent.js.map +1 -0
  158. package/dist/useAuiState.d.ts +23 -0
  159. package/dist/useAuiState.d.ts.map +1 -0
  160. package/dist/useAuiState.js +42 -0
  161. package/dist/useAuiState.js.map +1 -0
  162. package/dist/useScrollLock.d.ts +3 -0
  163. package/dist/useScrollLock.d.ts.map +1 -0
  164. package/dist/useScrollLock.js +53 -0
  165. package/dist/useScrollLock.js.map +1 -0
  166. package/package.json +57 -6
  167. package/src/AuiIf.ts +34 -0
  168. package/src/AuiProvider.ts +90 -0
  169. package/src/__tests__/AuiIf.test.ts +54 -0
  170. package/src/__tests__/AuiProvider.test.ts +266 -0
  171. package/src/__tests__/byindex-shrink-race.test.ts +411 -0
  172. package/src/__tests__/dist-graph.test.ts +49 -0
  173. package/src/__tests__/fixtures.ts +51 -0
  174. package/src/__tests__/primitives-actionbar.test.ts +469 -0
  175. package/src/__tests__/primitives-attachment.test.ts +493 -0
  176. package/src/__tests__/primitives-content.test.ts +829 -0
  177. package/src/__tests__/primitives-keyed-items.test.ts +270 -0
  178. package/src/__tests__/primitives-keyed-messages.test.ts +264 -0
  179. package/src/__tests__/primitives-structural.test.ts +573 -0
  180. package/src/__tests__/primitives-suggestions.test.ts +374 -0
  181. package/src/__tests__/primitives-threadlist.test.ts +443 -0
  182. package/src/__tests__/primitives-tool-ui.test.ts +579 -0
  183. package/src/__tests__/primitives.test.ts +408 -0
  184. package/src/__tests__/useAuiEvent.test.ts +159 -0
  185. package/src/__tests__/useAuiState.test.ts +124 -0
  186. package/src/context.ts +31 -0
  187. package/src/index.ts +87 -0
  188. package/src/primitives/AttachmentByIndexProvider.ts +88 -0
  189. package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.test.ts +105 -0
  190. package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.ts +38 -0
  191. package/src/primitives/ChainOfThoughtPrimitiveParts.test.ts +254 -0
  192. package/src/primitives/ChainOfThoughtPrimitiveParts.ts +112 -0
  193. package/src/primitives/ComposerPrimitiveCancel.ts +41 -0
  194. package/src/primitives/ComposerPrimitiveInput.ts +70 -0
  195. package/src/primitives/ComposerPrimitiveSend.ts +39 -0
  196. package/src/primitives/MessageByIdProvider.ts +89 -0
  197. package/src/primitives/MessagePrimitiveParts.tool-interaction.test.ts +88 -0
  198. package/src/primitives/MessagePrimitiveParts.ts +167 -0
  199. package/src/primitives/PartByIndexProvider.ts +62 -0
  200. package/src/primitives/ThreadListItemPrimitiveRoot.ts +57 -0
  201. package/src/primitives/ThreadListItemPrimitiveTrigger.ts +74 -0
  202. package/src/primitives/ThreadListKeyboardNav.test.ts +298 -0
  203. package/src/primitives/ThreadListPrimitiveRoot.ts +18 -0
  204. package/src/primitives/ThreadPrimitiveMessages.ts +38 -0
  205. package/src/primitives/ThreadPrimitiveScrollToBottom.ts +65 -0
  206. package/src/primitives/ThreadPrimitiveViewport.ts +120 -0
  207. package/src/primitives/ThreadPrimitiveViewportFooter.test.ts +400 -0
  208. package/src/primitives/ThreadPrimitiveViewportFooter.ts +60 -0
  209. package/src/primitives/actionBar.ts +159 -0
  210. package/src/primitives/attachment.ts +76 -0
  211. package/src/primitives/attrDisabled.ts +6 -0
  212. package/src/primitives/branchPicker.ts +77 -0
  213. package/src/primitives/composerAttachments.ts +181 -0
  214. package/src/primitives/error.ts +36 -0
  215. package/src/primitives/lastValidCache.ts +12 -0
  216. package/src/primitives/message.ts +54 -0
  217. package/src/primitives/messageAttachments.ts +32 -0
  218. package/src/primitives/stableKeys.test.ts +19 -0
  219. package/src/primitives/stableKeys.ts +21 -0
  220. package/src/primitives/suggestions.ts +179 -0
  221. package/src/primitives/thread.ts +46 -0
  222. package/src/primitives/threadList.ts +163 -0
  223. package/src/primitives/threadListFocusGroup.ts +56 -0
  224. package/src/primitives/threadListStructural.ts +86 -0
  225. package/src/primitives/useComposerSendState.ts +13 -0
  226. package/src/primitives/viewportContext.ts +16 -0
  227. package/src/useAui.ts +18 -0
  228. package/src/useAuiEvent.ts +64 -0
  229. package/src/useAuiState.ts +55 -0
  230. package/src/useScrollLock.test.ts +207 -0
  231. package/src/useScrollLock.ts +79 -0
@@ -0,0 +1,374 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createApp, defineComponent, h, nextTick, type Component } from "vue";
3
+ import { flushTapSync } from "@assistant-ui/tap";
4
+ import { AuiConfig } from "@assistant-ui/store/client";
5
+ import { RuntimeAdapter, Suggestions } from "@assistant-ui/core/store";
6
+ import type {
7
+ AppendMessage,
8
+ ExternalStoreAdapter,
9
+ RealtimeVoiceAdapter,
10
+ } from "@assistant-ui/core";
11
+ import {
12
+ AssistantRuntimeImpl,
13
+ ExternalStoreRuntimeCore,
14
+ } from "@assistant-ui/core/internal";
15
+ import { AuiProvider } from "../AuiProvider";
16
+ import {
17
+ SuggestionPrimitiveDescription,
18
+ SuggestionPrimitiveTitle,
19
+ SuggestionPrimitiveTrigger,
20
+ ThreadPrimitiveSuggestions,
21
+ } from "../primitives/suggestions";
22
+
23
+ type DemoMessage = { id: string; role: "user" | "assistant"; text: string };
24
+
25
+ const createSuggestingRuntime = () => {
26
+ let isRunning = false;
27
+ const onNew = vi.fn<(message: AppendMessage) => Promise<void>>(
28
+ async () => {},
29
+ );
30
+ const makeAdapter = (): ExternalStoreAdapter<DemoMessage> => ({
31
+ messages: [],
32
+ isRunning,
33
+ convertMessage: (message) => ({
34
+ id: message.id,
35
+ role: message.role,
36
+ content: [{ type: "text", text: message.text }],
37
+ }),
38
+ onNew,
39
+ });
40
+ const core = new ExternalStoreRuntimeCore(makeAdapter());
41
+ const runtime = new AssistantRuntimeImpl(core);
42
+ const sync = () => core.setAdapter(makeAdapter());
43
+ const setRunning = (value: boolean) => {
44
+ isRunning = value;
45
+ sync();
46
+ };
47
+ return { runtime, onNew, setRunning };
48
+ };
49
+
50
+ const createVoiceSuggestingRuntime = (
51
+ sendText?: RealtimeVoiceAdapter.Session["sendText"],
52
+ ) => {
53
+ let isRunning = false;
54
+ const onNew = vi.fn<(message: AppendMessage) => Promise<void>>(
55
+ async () => {},
56
+ );
57
+ const session: RealtimeVoiceAdapter.Session = {
58
+ status: { type: "running" },
59
+ isMuted: false,
60
+ disconnect: () => {},
61
+ mute: () => {},
62
+ unmute: () => {},
63
+ ...(sendText && { sendText }),
64
+ onStatusChange: () => () => {},
65
+ onTranscript: () => () => {},
66
+ onModeChange: () => () => {},
67
+ onVolumeChange: () => () => {},
68
+ };
69
+ const makeAdapter = (): ExternalStoreAdapter<DemoMessage> => ({
70
+ messages: [],
71
+ isRunning,
72
+ convertMessage: (message) => ({
73
+ id: message.id,
74
+ role: message.role,
75
+ content: [{ type: "text", text: message.text }],
76
+ }),
77
+ onNew,
78
+ adapters: { voice: { connect: () => session } },
79
+ });
80
+ const core = new ExternalStoreRuntimeCore(makeAdapter());
81
+ const runtime = new AssistantRuntimeImpl(core);
82
+ const setRunning = (value: boolean) => {
83
+ isRunning = value;
84
+ core.setAdapter(makeAdapter());
85
+ };
86
+ return { runtime, onNew, setRunning };
87
+ };
88
+
89
+ const mountSuggestions = (
90
+ runtime: AssistantRuntimeImpl,
91
+ triggerProps?: Record<string, unknown>,
92
+ ) => {
93
+ const View = defineComponent({
94
+ setup: () => () =>
95
+ h(ThreadPrimitiveSuggestions, null, {
96
+ default: () =>
97
+ h(
98
+ SuggestionPrimitiveTrigger,
99
+ { class: "chip", ...triggerProps },
100
+ {
101
+ default: () => [
102
+ h("b", null, [h(SuggestionPrimitiveTitle)]),
103
+ h("i", null, [h(SuggestionPrimitiveDescription)]),
104
+ ],
105
+ },
106
+ ),
107
+ }),
108
+ }) as Component;
109
+ const app = createApp(
110
+ defineComponent({
111
+ setup: () => () =>
112
+ h(
113
+ AuiProvider,
114
+ {
115
+ config: AuiConfig({
116
+ threads: RuntimeAdapter(runtime),
117
+ suggestions: Suggestions([
118
+ {
119
+ title: "Say hello",
120
+ label: "a friendly opener",
121
+ prompt: "Hello there!",
122
+ },
123
+ {
124
+ title: "Ask the time",
125
+ label: "small talk",
126
+ prompt: "What time is it?",
127
+ },
128
+ {
129
+ title: "Empty",
130
+ label: "no prompt",
131
+ prompt: "",
132
+ },
133
+ ]),
134
+ }),
135
+ },
136
+ { default: () => h(View) },
137
+ ),
138
+ }),
139
+ );
140
+ const el = document.createElement("div");
141
+ app.mount(el);
142
+ return { el, unmount: () => app.unmount() };
143
+ };
144
+
145
+ describe("suggestions primitives", () => {
146
+ it("renders one scoped chip per suggestion with title and label", async () => {
147
+ const { runtime } = createSuggestingRuntime();
148
+ const { el, unmount } = mountSuggestions(runtime);
149
+
150
+ await vi.waitFor(async () => {
151
+ await nextTick();
152
+ expect(el.querySelectorAll("button.chip")).toHaveLength(3);
153
+ });
154
+ const chips = [...el.querySelectorAll("button.chip")];
155
+ expect(chips.map((chip) => chip.querySelector("b")!.textContent)).toEqual([
156
+ "Say hello",
157
+ "Ask the time",
158
+ "Empty",
159
+ ]);
160
+ expect(chips.map((chip) => chip.querySelector("i")!.textContent)).toEqual([
161
+ "a friendly opener",
162
+ "small talk",
163
+ "no prompt",
164
+ ]);
165
+
166
+ unmount();
167
+ });
168
+
169
+ it("sends the prompt as a message when send is set", async () => {
170
+ const { runtime, onNew } = createSuggestingRuntime();
171
+ const { el, unmount } = mountSuggestions(runtime, { send: true });
172
+
173
+ await vi.waitFor(async () => {
174
+ await nextTick();
175
+ expect(el.querySelectorAll("button.chip")).toHaveLength(3);
176
+ });
177
+ el.querySelectorAll<HTMLButtonElement>("button.chip")[1]!.click();
178
+
179
+ await vi.waitFor(() => {
180
+ expect(onNew).toHaveBeenCalledTimes(1);
181
+ });
182
+ expect(onNew.mock.calls[0]![0]).toMatchObject({
183
+ content: [{ type: "text", text: "What time is it?" }],
184
+ });
185
+
186
+ unmount();
187
+ });
188
+
189
+ it("replaces the composer text without send, and appends when clearComposer is false", async () => {
190
+ const { runtime } = createSuggestingRuntime();
191
+ const { el, unmount } = mountSuggestions(runtime);
192
+
193
+ await vi.waitFor(async () => {
194
+ await nextTick();
195
+ expect(el.querySelectorAll("button.chip")).toHaveLength(3);
196
+ });
197
+ el.querySelectorAll<HTMLButtonElement>("button.chip")[0]!.click();
198
+ await vi.waitFor(() => {
199
+ expect(runtime.thread.composer.getState().text).toBe("Hello there!");
200
+ });
201
+
202
+ unmount();
203
+
204
+ const appending = createSuggestingRuntime();
205
+ const mounted = mountSuggestions(appending.runtime, {
206
+ clearComposer: false,
207
+ });
208
+ await vi.waitFor(async () => {
209
+ await nextTick();
210
+ expect(mounted.el.querySelectorAll("button.chip")).toHaveLength(3);
211
+ });
212
+ flushTapSync(() =>
213
+ appending.runtime.thread.composer.setText("Existing draft"),
214
+ );
215
+ mounted.el.querySelectorAll<HTMLButtonElement>("button.chip")[0]!.click();
216
+ await vi.waitFor(() => {
217
+ expect(appending.runtime.thread.composer.getState().text).toBe(
218
+ "Existing draft Hello there!",
219
+ );
220
+ });
221
+
222
+ flushTapSync(() =>
223
+ appending.runtime.thread.composer.setText("Existing draft"),
224
+ );
225
+ mounted.el.querySelectorAll<HTMLButtonElement>("button.chip")[2]!.click();
226
+ await vi.waitFor(() => {
227
+ expect(appending.runtime.thread.composer.getState().text).toBe(
228
+ "Existing draft",
229
+ );
230
+ });
231
+
232
+ mounted.unmount();
233
+ });
234
+
235
+ it("queues a send during a run without clearing the draft and forwards runConfig", async () => {
236
+ let isRunning = false;
237
+ const onNew = vi.fn<(message: AppendMessage) => Promise<void>>(
238
+ async () => {},
239
+ );
240
+ const steer = vi.fn<(message: AppendMessage) => void>();
241
+ const makeAdapter = (): ExternalStoreAdapter<DemoMessage> => ({
242
+ messages: [],
243
+ isRunning,
244
+ convertMessage: (message) => ({
245
+ id: message.id,
246
+ role: message.role,
247
+ content: [{ type: "text", text: message.text }],
248
+ }),
249
+ onNew,
250
+ queue: {
251
+ items: [],
252
+ steerItems: [],
253
+ enqueue: () => {},
254
+ steer,
255
+ move: () => {},
256
+ edit: () => {},
257
+ remove: () => {},
258
+ },
259
+ });
260
+ const core = new ExternalStoreRuntimeCore(makeAdapter());
261
+ const runtime = new AssistantRuntimeImpl(core);
262
+ const setRunning = (value: boolean) => {
263
+ isRunning = value;
264
+ core.setAdapter(makeAdapter());
265
+ };
266
+
267
+ const { el, unmount } = mountSuggestions(runtime, { send: true });
268
+ await vi.waitFor(async () => {
269
+ await nextTick();
270
+ expect(el.querySelectorAll("button.chip")).toHaveLength(3);
271
+ });
272
+
273
+ flushTapSync(() => {
274
+ runtime.thread.composer.setRunConfig({ custom: { mode: "echo" } });
275
+ runtime.thread.composer.setText("half-typed draft");
276
+ setRunning(true);
277
+ });
278
+ await vi.waitFor(async () => {
279
+ await nextTick();
280
+ expect(
281
+ el.querySelectorAll<HTMLButtonElement>("button.chip")[0]!.disabled,
282
+ ).toBe(false);
283
+ });
284
+
285
+ el.querySelectorAll<HTMLButtonElement>("button.chip")[0]!.click();
286
+ await vi.waitFor(() => {
287
+ expect(steer).toHaveBeenCalledTimes(1);
288
+ });
289
+ expect(steer.mock.calls[0]![0]).toMatchObject({
290
+ content: [{ type: "text", text: "Hello there!" }],
291
+ runConfig: { custom: { mode: "echo" } },
292
+ });
293
+ expect(onNew).not.toHaveBeenCalled();
294
+ expect(runtime.thread.composer.getState().text).toBe("half-typed draft");
295
+
296
+ unmount();
297
+ });
298
+
299
+ it("disables sending chips while a run is in flight", async () => {
300
+ const { runtime, setRunning } = createSuggestingRuntime();
301
+ const { el, unmount } = mountSuggestions(runtime, { send: true });
302
+
303
+ await vi.waitFor(async () => {
304
+ await nextTick();
305
+ expect(el.querySelectorAll("button.chip")).toHaveLength(3);
306
+ });
307
+ expect(
308
+ el.querySelectorAll<HTMLButtonElement>("button.chip")[0]!.disabled,
309
+ ).toBe(false);
310
+
311
+ flushTapSync(() => setRunning(true));
312
+ await vi.waitFor(async () => {
313
+ await nextTick();
314
+ expect(
315
+ el.querySelectorAll<HTMLButtonElement>("button.chip")[0]!.disabled,
316
+ ).toBe(true);
317
+ });
318
+
319
+ unmount();
320
+ });
321
+
322
+ it("sends into a voice session that takes typed text while a spoken reply is running", async () => {
323
+ const sendText = vi.fn<(text: string) => void>();
324
+ const { runtime, onNew, setRunning } =
325
+ createVoiceSuggestingRuntime(sendText);
326
+ const { el, unmount } = mountSuggestions(runtime, { send: true });
327
+ await vi.waitFor(async () => {
328
+ await nextTick();
329
+ expect(el.querySelectorAll("button.chip")).toHaveLength(3);
330
+ });
331
+
332
+ flushTapSync(() => {
333
+ runtime.thread.connectVoice();
334
+ runtime.thread.composer.setText("half-typed draft");
335
+ setRunning(true);
336
+ });
337
+ await vi.waitFor(async () => {
338
+ await nextTick();
339
+ expect(
340
+ el.querySelectorAll<HTMLButtonElement>("button.chip")[0]!.disabled,
341
+ ).toBe(false);
342
+ });
343
+
344
+ el.querySelectorAll<HTMLButtonElement>("button.chip")[0]!.click();
345
+ await vi.waitFor(() => {
346
+ expect(sendText).toHaveBeenCalledExactlyOnceWith("Hello there!");
347
+ });
348
+ expect(onNew).not.toHaveBeenCalled();
349
+ expect(runtime.thread.composer.getState().text).toBe("");
350
+
351
+ runtime.thread.disconnectVoice();
352
+ unmount();
353
+ });
354
+
355
+ it("disables sending chips while a voice session cannot take typed text", async () => {
356
+ const { runtime } = createVoiceSuggestingRuntime();
357
+ const { el, unmount } = mountSuggestions(runtime, { send: true });
358
+ await vi.waitFor(async () => {
359
+ await nextTick();
360
+ expect(el.querySelectorAll("button.chip")).toHaveLength(3);
361
+ });
362
+
363
+ flushTapSync(() => runtime.thread.connectVoice());
364
+ await vi.waitFor(async () => {
365
+ await nextTick();
366
+ expect(
367
+ el.querySelectorAll<HTMLButtonElement>("button.chip")[0]!.disabled,
368
+ ).toBe(true);
369
+ });
370
+
371
+ runtime.thread.disconnectVoice();
372
+ unmount();
373
+ });
374
+ });