@assistant-ui/vue 0.0.0 → 0.0.1

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 +50 -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 +23 -0
  67. package/dist/primitives/ThreadPrimitiveMessages.d.ts.map +1 -0
  68. package/dist/primitives/ThreadPrimitiveMessages.js +37 -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 +174 -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 +749 -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 +361 -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 +64 -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 +166 -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 +40 -0
  205. package/src/primitives/ThreadPrimitiveScrollToBottom.ts +65 -0
  206. package/src/primitives/ThreadPrimitiveViewport.ts +233 -0
  207. package/src/primitives/ThreadPrimitiveViewportFooter.test.ts +305 -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,298 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ createApp,
4
+ defineComponent,
5
+ h,
6
+ nextTick,
7
+ ref,
8
+ type Component,
9
+ } from "vue";
10
+ import { AuiConfig } from "@assistant-ui/store/client";
11
+ import { RuntimeAdapter } from "@assistant-ui/core/store";
12
+ import type { ExternalStoreAdapter } from "@assistant-ui/core";
13
+ import {
14
+ AssistantRuntimeImpl,
15
+ ExternalStoreRuntimeCore,
16
+ } from "@assistant-ui/core/internal";
17
+ import { AuiProvider } from "../AuiProvider";
18
+ import { ThreadListItemByIndexProvider } from "./threadList";
19
+ import { ThreadListItemPrimitiveRoot } from "./ThreadListItemPrimitiveRoot";
20
+ import { ThreadListItemPrimitiveTrigger } from "./ThreadListItemPrimitiveTrigger";
21
+ import { ThreadListPrimitiveRoot } from "./ThreadListPrimitiveRoot";
22
+
23
+ type Message = { id: string; role: "user"; text: string };
24
+
25
+ const createRuntime = () => {
26
+ let mainThreadId = "first";
27
+ let core!: ExternalStoreRuntimeCore;
28
+ const onSwitchToThread = vi.fn((threadId: string) => {
29
+ mainThreadId = threadId;
30
+ core.setAdapter(createAdapter());
31
+ });
32
+ const createAdapter = (): ExternalStoreAdapter<Message> => ({
33
+ messages: [],
34
+ convertMessage: (message) => ({
35
+ id: message.id,
36
+ role: message.role,
37
+ content: [{ type: "text", text: message.text }],
38
+ }),
39
+ onNew: async () => {},
40
+ adapters: {
41
+ threadList: {
42
+ threadId: mainThreadId,
43
+ threads: ["first", "second", "third"].map((id) => ({
44
+ status: "regular" as const,
45
+ id,
46
+ title: id,
47
+ })),
48
+ onSwitchToThread,
49
+ },
50
+ },
51
+ });
52
+ core = new ExternalStoreRuntimeCore(createAdapter());
53
+ return {
54
+ runtime: new AssistantRuntimeImpl(core),
55
+ onSwitchToThread,
56
+ };
57
+ };
58
+
59
+ const mount = (runtime: AssistantRuntimeImpl, view: Component) => {
60
+ const app = createApp(
61
+ defineComponent({
62
+ setup: () => () =>
63
+ h(
64
+ AuiProvider,
65
+ { config: AuiConfig({ threads: RuntimeAdapter(runtime) }) },
66
+ { default: () => h(view) },
67
+ ),
68
+ }),
69
+ );
70
+ const el = document.createElement("div");
71
+ document.body.append(el);
72
+ app.mount(el);
73
+ return {
74
+ el,
75
+ unmount: () => {
76
+ app.unmount();
77
+ el.remove();
78
+ },
79
+ };
80
+ };
81
+
82
+ const keydown = (element: HTMLElement, key: string) =>
83
+ element.dispatchEvent(
84
+ new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key }),
85
+ );
86
+
87
+ const buttons = (el: HTMLElement) => [
88
+ ...el.querySelectorAll<HTMLButtonElement>("button"),
89
+ ];
90
+
91
+ describe("thread list keyboard navigation", () => {
92
+ it("moves through triggers in DOM order without wrapping", async () => {
93
+ const { runtime } = createRuntime();
94
+ const View = defineComponent({
95
+ setup: () => () =>
96
+ h(
97
+ ThreadListPrimitiveRoot,
98
+ { class: "list" },
99
+ {
100
+ default: () =>
101
+ [0, 1, 2].map((index) =>
102
+ h(
103
+ ThreadListItemByIndexProvider,
104
+ { index, key: index },
105
+ {
106
+ default: () =>
107
+ h(
108
+ ThreadListItemPrimitiveRoot,
109
+ { class: "item" },
110
+ {
111
+ default: () =>
112
+ h(ThreadListItemPrimitiveTrigger, null, {
113
+ default: () => `item ${index}`,
114
+ }),
115
+ },
116
+ ),
117
+ },
118
+ ),
119
+ ),
120
+ },
121
+ ),
122
+ });
123
+ const { el, unmount } = mount(runtime, View);
124
+
125
+ await nextTick();
126
+ const [first, second, third] = buttons(el);
127
+ const list = el.querySelector<HTMLElement>(".list")!;
128
+ const [firstItem, , thirdItem] = el.querySelectorAll<HTMLElement>(".item");
129
+ list.insertBefore(thirdItem!, firstItem!);
130
+
131
+ third!.focus();
132
+ expect(keydown(third!, "ArrowDown")).toBe(false);
133
+ expect(document.activeElement).toBe(first);
134
+ expect(keydown(first!, "ArrowDown")).toBe(false);
135
+ expect(document.activeElement).toBe(second);
136
+ expect(keydown(second!, "ArrowDown")).toBe(true);
137
+ expect(document.activeElement).toBe(second);
138
+ expect(keydown(first!, "ArrowUp")).toBe(false);
139
+ expect(document.activeElement).toBe(third);
140
+ expect(keydown(third!, "ArrowUp")).toBe(true);
141
+ expect(document.activeElement).toBe(third);
142
+
143
+ unmount();
144
+ });
145
+
146
+ it("lets a caller veto collection navigation", async () => {
147
+ const { runtime } = createRuntime();
148
+ const View = defineComponent({
149
+ setup: () => () =>
150
+ h(ThreadListPrimitiveRoot, null, {
151
+ default: () =>
152
+ [0, 1].map((index) =>
153
+ h(
154
+ ThreadListItemByIndexProvider,
155
+ { index, key: index },
156
+ {
157
+ default: () =>
158
+ h(
159
+ ThreadListItemPrimitiveRoot,
160
+ {
161
+ onKeydown: (event: KeyboardEvent) =>
162
+ event.preventDefault(),
163
+ },
164
+ {
165
+ default: () =>
166
+ h(ThreadListItemPrimitiveTrigger, null, {
167
+ default: () => `item ${index}`,
168
+ }),
169
+ },
170
+ ),
171
+ },
172
+ ),
173
+ ),
174
+ }),
175
+ });
176
+ const { el, unmount } = mount(runtime, View);
177
+
178
+ await nextTick();
179
+ const [first] = buttons(el);
180
+ first!.focus();
181
+ expect(keydown(first!, "ArrowDown")).toBe(false);
182
+ expect(document.activeElement).toBe(first);
183
+
184
+ unmount();
185
+ });
186
+
187
+ it("tracks the main thread on the item root", async () => {
188
+ const { runtime, onSwitchToThread } = createRuntime();
189
+ const View = defineComponent({
190
+ setup: () => () =>
191
+ h(ThreadListPrimitiveRoot, null, {
192
+ default: () =>
193
+ [0, 1].map((index) =>
194
+ h(
195
+ ThreadListItemByIndexProvider,
196
+ { index, key: index },
197
+ {
198
+ default: () =>
199
+ h(
200
+ ThreadListItemPrimitiveRoot,
201
+ { class: "item" },
202
+ {
203
+ default: () =>
204
+ h(ThreadListItemPrimitiveTrigger, null, {
205
+ default: () => `item ${index}`,
206
+ }),
207
+ },
208
+ ),
209
+ },
210
+ ),
211
+ ),
212
+ }),
213
+ });
214
+ const { el, unmount } = mount(runtime, View);
215
+
216
+ await nextTick();
217
+ const items = el.querySelectorAll<HTMLElement>(".item");
218
+ expect(items[0]!.getAttribute("data-active")).toBe("true");
219
+ expect(items[0]!.getAttribute("aria-current")).toBe("true");
220
+ buttons(el)[1]!.click();
221
+ await vi.waitFor(() => {
222
+ expect(onSwitchToThread).toHaveBeenCalledWith("second");
223
+ expect(items[1]!.getAttribute("data-active")).toBe("true");
224
+ expect(items[1]!.getAttribute("aria-current")).toBe("true");
225
+ });
226
+ expect(items[0]!.hasAttribute("data-active")).toBe(false);
227
+ expect(items[0]!.hasAttribute("aria-current")).toBe(false);
228
+
229
+ unmount();
230
+ });
231
+
232
+ it("keeps the trigger working without either root", async () => {
233
+ const { runtime, onSwitchToThread } = createRuntime();
234
+ const View = defineComponent({
235
+ setup: () => () =>
236
+ [0, 1].map((index) =>
237
+ h(
238
+ ThreadListItemByIndexProvider,
239
+ { index, key: index },
240
+ {
241
+ default: () =>
242
+ h(ThreadListItemPrimitiveTrigger, null, {
243
+ default: () => `item ${index}`,
244
+ }),
245
+ },
246
+ ),
247
+ ),
248
+ });
249
+ const { el, unmount } = mount(runtime, View);
250
+
251
+ await nextTick();
252
+ buttons(el)[1]!.click();
253
+ await vi.waitFor(() => {
254
+ expect(onSwitchToThread).toHaveBeenCalledWith("second");
255
+ });
256
+
257
+ unmount();
258
+ });
259
+
260
+ it("unregisters a trigger when its item unmounts", async () => {
261
+ const { runtime } = createRuntime();
262
+ const showSecond = ref(true);
263
+ const View = defineComponent({
264
+ setup: () => () =>
265
+ h(ThreadListPrimitiveRoot, null, {
266
+ default: () =>
267
+ [0, 1, 2]
268
+ .filter((index) => index !== 1 || showSecond.value)
269
+ .map((index) =>
270
+ h(
271
+ ThreadListItemByIndexProvider,
272
+ { index, key: index },
273
+ {
274
+ default: () =>
275
+ h(ThreadListItemPrimitiveRoot, null, {
276
+ default: () =>
277
+ h(ThreadListItemPrimitiveTrigger, null, {
278
+ default: () => `item ${index}`,
279
+ }),
280
+ }),
281
+ },
282
+ ),
283
+ ),
284
+ }),
285
+ });
286
+ const { el, unmount } = mount(runtime, View);
287
+
288
+ await nextTick();
289
+ showSecond.value = false;
290
+ await nextTick();
291
+ const [first, third] = buttons(el);
292
+ first!.focus();
293
+ expect(keydown(first!, "ArrowDown")).toBe(false);
294
+ expect(document.activeElement).toBe(third);
295
+
296
+ unmount();
297
+ });
298
+ });
@@ -0,0 +1,18 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ mergeProps,
5
+ type SlotsType,
6
+ type VNodeChild,
7
+ } from "vue";
8
+ import { provideThreadListCollection } from "./threadListFocusGroup";
9
+
10
+ export const ThreadListPrimitiveRoot = defineComponent({
11
+ name: "ThreadListPrimitiveRoot",
12
+ inheritAttrs: false,
13
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
14
+ setup(_, { attrs, slots }) {
15
+ provideThreadListCollection();
16
+ return () => h("div", mergeProps(attrs), slots.default?.());
17
+ },
18
+ });
@@ -0,0 +1,40 @@
1
+ import { defineComponent, h, type SlotsType, type VNodeChild } from "vue";
2
+ import type {} from "@assistant-ui/core/store";
3
+ import { useAuiState } from "../useAuiState";
4
+ import { MessageByIdProvider } from "./MessageByIdProvider";
5
+ import { useStableKeys } from "./stableKeys";
6
+
7
+ /**
8
+ * Renders the default slot once per message in the current thread, each
9
+ * instance scoped to its message through {@link MessageByIdProvider} and
10
+ * keyed by the message id: an edit or reload that replaces the occupant of a
11
+ * slot remounts that row, so `<TransitionGroup>` and per-row component state
12
+ * follow message identity. The empty optimistic placeholder that precedes a
13
+ * response is its own identity, so the arrival of the real assistant message
14
+ * remounts that one row (a leave/enter pair under `<TransitionGroup>`).
15
+ *
16
+ * @example
17
+ * ```html
18
+ * <ThreadPrimitiveMessages>
19
+ * <ChatMessage />
20
+ * </ThreadPrimitiveMessages>
21
+ * ```
22
+ */
23
+ export const ThreadPrimitiveMessages = defineComponent({
24
+ name: "ThreadPrimitiveMessages",
25
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
26
+ setup(_, { slots }) {
27
+ const messages = useAuiState((s) => s.thread.messages);
28
+ const ids = useStableKeys(() =>
29
+ messages.value.map((message) => message.id),
30
+ );
31
+ return () =>
32
+ ids.value.map((id) =>
33
+ h(
34
+ MessageByIdProvider,
35
+ { id, key: id },
36
+ { default: () => slots.default?.() },
37
+ ),
38
+ );
39
+ },
40
+ });
@@ -0,0 +1,65 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ inject,
5
+ mergeProps,
6
+ type PropType,
7
+ type SlotsType,
8
+ type VNodeChild,
9
+ } from "vue";
10
+ import { isDevelopment } from "@assistant-ui/core/store/internal";
11
+ import { isAttrDisabled } from "./attrDisabled";
12
+ import { viewportInjectionKey } from "./viewportContext";
13
+
14
+ let warnedOutsideViewport = false;
15
+
16
+ export const clearScrollToBottomWarningForTesting = () => {
17
+ warnedOutsideViewport = false;
18
+ };
19
+
20
+ /**
21
+ * A button that scrolls the surrounding {@link ThreadPrimitiveViewport} to
22
+ * the bottom. Disabled while the viewport is already at the bottom, or when
23
+ * no viewport provides the channel.
24
+ */
25
+ export const ThreadPrimitiveScrollToBottom = defineComponent({
26
+ name: "ThreadPrimitiveScrollToBottom",
27
+ inheritAttrs: false,
28
+ props: {
29
+ behavior: {
30
+ type: String as PropType<ScrollBehavior>,
31
+ default: "auto",
32
+ },
33
+ },
34
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
35
+ setup(props, { attrs, slots }) {
36
+ const viewport = inject(viewportInjectionKey, null);
37
+ if (isDevelopment && !viewport && !warnedOutsideViewport) {
38
+ warnedOutsideViewport = true;
39
+ console.warn(
40
+ "ThreadPrimitiveScrollToBottom: no surrounding ThreadPrimitiveViewport provides the scroll channel; the button stays disabled. Place it inside the viewport.",
41
+ );
42
+ }
43
+ const onClick = (event: MouseEvent) => {
44
+ if (
45
+ event.defaultPrevented ||
46
+ !viewport ||
47
+ viewport.isAtBottom.value ||
48
+ isAttrDisabled(attrs)
49
+ )
50
+ return;
51
+ viewport.scrollToBottom(props.behavior);
52
+ };
53
+ return () =>
54
+ h(
55
+ "button",
56
+ mergeProps(attrs, {
57
+ type: "button",
58
+ disabled:
59
+ !viewport || viewport.isAtBottom.value || isAttrDisabled(attrs),
60
+ onClick,
61
+ }),
62
+ slots.default?.(),
63
+ );
64
+ },
65
+ });
@@ -0,0 +1,233 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ onMounted,
5
+ onScopeDispose,
6
+ provide,
7
+ shallowRef,
8
+ watch,
9
+ type SlotsType,
10
+ type VNodeChild,
11
+ } from "vue";
12
+ import type {} from "@assistant-ui/core/store";
13
+ import { useAuiEvent } from "../useAuiEvent";
14
+ import { useAuiState } from "../useAuiState";
15
+ import {
16
+ isUserScrollUp,
17
+ isViewportAtBottom,
18
+ observeContentResize,
19
+ viewportOverflows,
20
+ } from "@assistant-ui/store/client";
21
+ import { viewportInjectionKey } from "./viewportContext";
22
+
23
+ /**
24
+ * A scrollable container that keeps the thread pinned to the bottom: content
25
+ * growth scrolls back down while the user sits at the bottom, a run start
26
+ * scrolls down, and scrolling up unpins until the user returns to the bottom.
27
+ * The four options mirror the React hook and are independent: `autoScroll`
28
+ * covers follow-on-content-growth, and the other three gate the
29
+ * first-messages, run-start, and thread-switch scrolls. Provides the
30
+ * scroll-to-bottom channel that {@link ThreadPrimitiveScrollToBottom} drives;
31
+ * the React viewport's top-anchor system stays in the React viewport store
32
+ * and is not ported.
33
+ */
34
+ export const ThreadPrimitiveViewport = defineComponent({
35
+ name: "ThreadPrimitiveViewport",
36
+ props: {
37
+ autoScroll: {
38
+ type: Boolean,
39
+ default: true,
40
+ },
41
+ scrollToBottomOnInitialize: {
42
+ type: Boolean,
43
+ default: true,
44
+ },
45
+ scrollToBottomOnRunStart: {
46
+ type: Boolean,
47
+ default: true,
48
+ },
49
+ scrollToBottomOnThreadSwitch: {
50
+ type: Boolean,
51
+ default: true,
52
+ },
53
+ },
54
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
55
+ setup(props, { slots }) {
56
+ const divRef = shallowRef<HTMLElement | null>(null);
57
+ const contentInset = shallowRef(0);
58
+ const contentInsetEntries = new Map<symbol, number>();
59
+ let intent: ScrollBehavior | null = null;
60
+ const isAtBottom = shallowRef(true);
61
+ let lastScrollTop = 0;
62
+ let lastScrollHeight = 0;
63
+ let lastObservedScrollHeight = 0;
64
+ let lastObservedClientHeight = 0;
65
+ let frame: number | null = null;
66
+
67
+ const scrollToBottom = (behavior: ScrollBehavior) => {
68
+ const div = divRef.value;
69
+ if (!div) return;
70
+ intent = behavior;
71
+ div.scrollTo?.({ top: div.scrollHeight, behavior });
72
+ };
73
+
74
+ const scheduleScrollToBottom = (behavior: ScrollBehavior) => {
75
+ intent = behavior;
76
+ // The immediate watch below runs synchronously during SSR, where no
77
+ // frame scheduler exists.
78
+ if (typeof requestAnimationFrame === "undefined") return;
79
+ if (frame !== null) cancelAnimationFrame(frame);
80
+ frame = requestAnimationFrame(() => {
81
+ frame = null;
82
+ scrollToBottom(behavior);
83
+ });
84
+ };
85
+
86
+ const handleScroll = () => {
87
+ const div = divRef.value;
88
+ if (!div) return;
89
+
90
+ const newIsAtBottom = isViewportAtBottom(div, contentInset.value);
91
+ const inFlightDownward = !newIsAtBottom && lastScrollTop < div.scrollTop;
92
+ if (!inFlightDownward) {
93
+ if (newIsAtBottom) {
94
+ // At-bottom is ambiguous while the viewport does not overflow; keep
95
+ // the intent alive until content can actually scroll.
96
+ if (viewportOverflows(div, contentInset.value)) intent = null;
97
+ } else if (
98
+ isUserScrollUp(
99
+ { scrollTop: lastScrollTop, scrollHeight: lastScrollHeight },
100
+ div,
101
+ )
102
+ ) {
103
+ intent = null;
104
+ }
105
+ if (newIsAtBottom || intent === null) isAtBottom.value = newIsAtBottom;
106
+ }
107
+
108
+ lastScrollTop = div.scrollTop;
109
+ lastScrollHeight = div.scrollHeight;
110
+ };
111
+
112
+ const updateContentInset = () => {
113
+ let total = 0;
114
+ for (const height of contentInsetEntries.values()) total += height;
115
+ if (contentInset.value === total) return;
116
+ const grew = total > contentInset.value;
117
+ contentInset.value = total;
118
+ // A growing inset obscures content a pinned viewport was showing, so it
119
+ // follows like a content resize; a shrinking inset reveals content and
120
+ // must not move the viewport.
121
+ if (grew) {
122
+ if (intent) {
123
+ scrollToBottom(intent);
124
+ } else if (props.autoScroll && isAtBottom.value) {
125
+ scrollToBottom("instant");
126
+ }
127
+ }
128
+ handleScroll();
129
+ };
130
+
131
+ const registerContentInset = () => {
132
+ const id = Symbol();
133
+ contentInsetEntries.set(id, 0);
134
+
135
+ return {
136
+ setHeight: (height: number) => {
137
+ if (contentInsetEntries.get(id) === height) return;
138
+ contentInsetEntries.set(id, height);
139
+ updateContentInset();
140
+ },
141
+ unregister: () => {
142
+ if (!contentInsetEntries.delete(id)) return;
143
+ updateContentInset();
144
+ },
145
+ };
146
+ };
147
+
148
+ const onContentResize = () => {
149
+ const div = divRef.value;
150
+ if (!div) return;
151
+ const { scrollHeight, clientHeight } = div;
152
+ if (
153
+ scrollHeight === lastObservedScrollHeight &&
154
+ clientHeight === lastObservedClientHeight
155
+ ) {
156
+ return;
157
+ }
158
+ lastObservedScrollHeight = scrollHeight;
159
+ lastObservedClientHeight = clientHeight;
160
+
161
+ if (intent) {
162
+ scrollToBottom(intent);
163
+ } else if (props.autoScroll && isAtBottom.value) {
164
+ scrollToBottom("instant");
165
+ }
166
+ handleScroll();
167
+ };
168
+
169
+ // A pointer gesture invalidates pending bottom-scroll intent; otherwise an
170
+ // intent kept alive by a non-overflowing thread hijacks the next content
171
+ // growth. Unlike the React hook, an already scheduled frame is cancelled
172
+ // too, so the gesture also wins the race against a just-planted intent.
173
+ const onPointerdown = () => {
174
+ intent = null;
175
+ if (frame !== null) {
176
+ cancelAnimationFrame(frame);
177
+ frame = null;
178
+ }
179
+ };
180
+
181
+ let disconnect: (() => void) | undefined;
182
+ onMounted(() => {
183
+ const div = divRef.value;
184
+ if (!div) return;
185
+ disconnect = observeContentResize(div, onContentResize);
186
+ });
187
+ onScopeDispose(() => {
188
+ disconnect?.();
189
+ if (frame !== null) cancelAnimationFrame(frame);
190
+ });
191
+
192
+ const hasMessages = useAuiState((s) => s.thread.messages.length > 0);
193
+ let initialized = false;
194
+ watch(
195
+ [hasMessages, () => props.scrollToBottomOnInitialize],
196
+ ([has, enabled]) => {
197
+ if (!has) {
198
+ initialized = false;
199
+ return;
200
+ }
201
+ if (!enabled || initialized) return;
202
+ initialized = true;
203
+ if (intent !== null) return;
204
+ scheduleScrollToBottom("instant");
205
+ },
206
+ { immediate: true },
207
+ );
208
+
209
+ useAuiEvent("thread.runStart", () => {
210
+ if (!props.scrollToBottomOnRunStart) return;
211
+ scheduleScrollToBottom("auto");
212
+ });
213
+
214
+ useAuiEvent("threads.selectionChanged", () => {
215
+ if (!props.scrollToBottomOnThreadSwitch) return;
216
+ scheduleScrollToBottom("instant");
217
+ });
218
+
219
+ provide(viewportInjectionKey, {
220
+ isAtBottom,
221
+ scrollToBottom: (behavior: ScrollBehavior = "auto") =>
222
+ scrollToBottom(behavior),
223
+ registerContentInset,
224
+ });
225
+
226
+ return () =>
227
+ h(
228
+ "div",
229
+ { ref: divRef, onScroll: handleScroll, onPointerdown },
230
+ slots.default?.(),
231
+ );
232
+ },
233
+ });