@assistant-ui/vue 0.0.0-stage → 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 +64 -3
  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,305 @@
1
+ import { afterEach, 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 {
13
+ AssistantRuntimeImpl,
14
+ ExternalStoreRuntimeCore,
15
+ } from "@assistant-ui/core/internal";
16
+ import { AuiProvider } from "../AuiProvider";
17
+ import { ThreadPrimitiveScrollToBottom } from "./ThreadPrimitiveScrollToBottom";
18
+ import { ThreadPrimitiveViewport } from "./ThreadPrimitiveViewport";
19
+ import { ThreadPrimitiveViewportFooter } from "./ThreadPrimitiveViewportFooter";
20
+
21
+ const geometryElements = new Set<HTMLElement>();
22
+
23
+ const createTestRuntime = () =>
24
+ new AssistantRuntimeImpl(
25
+ new ExternalStoreRuntimeCore({
26
+ messages: [],
27
+ convertMessage: () => ({ role: "user", content: [] }),
28
+ onNew: async () => {},
29
+ }),
30
+ );
31
+
32
+ const mountChat = (runtime: AssistantRuntimeImpl, view: Component) => {
33
+ const app = createApp(
34
+ defineComponent({
35
+ setup: () => () =>
36
+ h(
37
+ AuiProvider,
38
+ { config: AuiConfig({ threads: RuntimeAdapter(runtime) }) },
39
+ { default: () => h(view) },
40
+ ),
41
+ }),
42
+ );
43
+ const el = document.createElement("div");
44
+ app.mount(el);
45
+ return { el, unmount: () => app.unmount() };
46
+ };
47
+
48
+ const installViewportGeometry = (div: HTMLElement) => {
49
+ let scrollHeight = 500;
50
+ let scrollTop = 0;
51
+ const clientHeight = 100;
52
+ geometryElements.add(div);
53
+ Object.defineProperty(div, "scrollHeight", {
54
+ get: () => scrollHeight,
55
+ configurable: true,
56
+ });
57
+ Object.defineProperty(div, "clientHeight", {
58
+ get: () => clientHeight,
59
+ configurable: true,
60
+ });
61
+ Object.defineProperty(div, "scrollTop", {
62
+ get: () => scrollTop,
63
+ set: (value: number) => {
64
+ scrollTop = value;
65
+ },
66
+ configurable: true,
67
+ });
68
+ const scrollTo = vi.fn(({ top }: ScrollToOptions) => {
69
+ scrollTop = Math.max(0, Math.min(top ?? 0, scrollHeight - clientHeight));
70
+ div.dispatchEvent(new Event("scroll"));
71
+ });
72
+ Object.defineProperty(div, "scrollTo", {
73
+ value: scrollTo,
74
+ configurable: true,
75
+ });
76
+ return {
77
+ scrollTo,
78
+ setScrollHeight: (value: number) => {
79
+ scrollHeight = value;
80
+ },
81
+ setScrollTop: (value: number) => {
82
+ scrollTop = value;
83
+ },
84
+ };
85
+ };
86
+
87
+ const installResizeObserver = () => {
88
+ const observers = new Set<ResizeObserverMock>();
89
+ class ResizeObserverMock {
90
+ element: Element | null = null;
91
+ readonly callback: ResizeObserverCallback;
92
+
93
+ constructor(callback: ResizeObserverCallback) {
94
+ this.callback = callback;
95
+ observers.add(this);
96
+ }
97
+
98
+ observe(element: Element) {
99
+ this.element = element;
100
+ }
101
+
102
+ unobserve() {}
103
+
104
+ disconnect() {
105
+ observers.delete(this);
106
+ }
107
+
108
+ takeRecords() {
109
+ return [];
110
+ }
111
+
112
+ trigger() {
113
+ this.callback([], this);
114
+ }
115
+ }
116
+ vi.stubGlobal("ResizeObserver", ResizeObserverMock);
117
+ return {
118
+ trigger: (element: Element) => {
119
+ for (const observer of observers) {
120
+ if (observer.element === element) observer.trigger();
121
+ }
122
+ },
123
+ };
124
+ };
125
+
126
+ afterEach(() => {
127
+ vi.restoreAllMocks();
128
+ vi.unstubAllGlobals();
129
+ for (const element of geometryElements) {
130
+ Reflect.deleteProperty(element, "scrollHeight");
131
+ Reflect.deleteProperty(element, "clientHeight");
132
+ Reflect.deleteProperty(element, "scrollTop");
133
+ Reflect.deleteProperty(element, "scrollTo");
134
+ }
135
+ geometryElements.clear();
136
+ });
137
+
138
+ describe("ThreadPrimitiveViewportFooter", () => {
139
+ it("registers its height and unregisters it when removed", async () => {
140
+ const observers = installResizeObserver();
141
+ const showFooter = ref(true);
142
+ const View = defineComponent({
143
+ setup: () => () =>
144
+ h(
145
+ ThreadPrimitiveViewport,
146
+ { class: "viewport" },
147
+ {
148
+ default: () => [
149
+ showFooter.value
150
+ ? h(ThreadPrimitiveViewportFooter, { class: "footer" })
151
+ : null,
152
+ h(
153
+ ThreadPrimitiveScrollToBottom,
154
+ { class: "jump" },
155
+ { default: () => "Jump" },
156
+ ),
157
+ ],
158
+ },
159
+ ),
160
+ });
161
+ const { el, unmount } = mountChat(createTestRuntime(), View);
162
+ const div = el.querySelector<HTMLElement>("div.viewport")!;
163
+ const geometry = installViewportGeometry(div);
164
+ const footer = el.querySelector<HTMLElement>("div.footer")!;
165
+ Object.defineProperty(footer, "offsetHeight", {
166
+ get: () => 50,
167
+ configurable: true,
168
+ });
169
+ div.dispatchEvent(new Event("scroll"));
170
+ observers.trigger(footer);
171
+ await nextTick();
172
+
173
+ geometry.setScrollTop(355);
174
+ div.dispatchEvent(new Event("scroll"));
175
+ await nextTick();
176
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
177
+ true,
178
+ );
179
+
180
+ showFooter.value = false;
181
+ await nextTick();
182
+ div.dispatchEvent(new Event("scroll"));
183
+ await nextTick();
184
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
185
+ false,
186
+ );
187
+
188
+ Reflect.deleteProperty(footer, "offsetHeight");
189
+ unmount();
190
+ });
191
+
192
+ it("sums the heights of multiple footers", async () => {
193
+ const observers = installResizeObserver();
194
+ const showSecond = ref(true);
195
+ const View = defineComponent({
196
+ setup: () => () =>
197
+ h(
198
+ ThreadPrimitiveViewport,
199
+ { class: "viewport" },
200
+ {
201
+ default: () => [
202
+ h(ThreadPrimitiveViewportFooter, { class: "first-footer" }),
203
+ showSecond.value
204
+ ? h(ThreadPrimitiveViewportFooter, { class: "second-footer" })
205
+ : null,
206
+ h(
207
+ ThreadPrimitiveScrollToBottom,
208
+ { class: "jump" },
209
+ { default: () => "Jump" },
210
+ ),
211
+ ],
212
+ },
213
+ ),
214
+ });
215
+ const { el, unmount } = mountChat(createTestRuntime(), View);
216
+ const div = el.querySelector<HTMLElement>("div.viewport")!;
217
+ const geometry = installViewportGeometry(div);
218
+ const firstFooter = el.querySelector<HTMLElement>("div.first-footer")!;
219
+ const secondFooter = el.querySelector<HTMLElement>("div.second-footer")!;
220
+ Object.defineProperty(firstFooter, "offsetHeight", {
221
+ get: () => 20,
222
+ configurable: true,
223
+ });
224
+ Object.defineProperty(secondFooter, "offsetHeight", {
225
+ get: () => 30,
226
+ configurable: true,
227
+ });
228
+ div.dispatchEvent(new Event("scroll"));
229
+ observers.trigger(firstFooter);
230
+ observers.trigger(secondFooter);
231
+ await nextTick();
232
+
233
+ geometry.setScrollTop(355);
234
+ div.dispatchEvent(new Event("scroll"));
235
+ await nextTick();
236
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
237
+ true,
238
+ );
239
+
240
+ showSecond.value = false;
241
+ await nextTick();
242
+ div.dispatchEvent(new Event("scroll"));
243
+ await nextTick();
244
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
245
+ false,
246
+ );
247
+
248
+ Reflect.deleteProperty(firstFooter, "offsetHeight");
249
+ Reflect.deleteProperty(secondFooter, "offsetHeight");
250
+ unmount();
251
+ });
252
+
253
+ it("follows a footer growth while pinned at the bottom", async () => {
254
+ const observers = installResizeObserver();
255
+ let footerHeight = 50;
256
+ const View = defineComponent({
257
+ setup: () => () =>
258
+ h(
259
+ ThreadPrimitiveViewport,
260
+ { class: "viewport" },
261
+ {
262
+ default: () => [
263
+ h(ThreadPrimitiveViewportFooter, { class: "footer" }),
264
+ h(
265
+ ThreadPrimitiveScrollToBottom,
266
+ { class: "jump" },
267
+ { default: () => "Jump" },
268
+ ),
269
+ ],
270
+ },
271
+ ),
272
+ });
273
+ const { el, unmount } = mountChat(createTestRuntime(), View);
274
+ const div = el.querySelector<HTMLElement>("div.viewport")!;
275
+ const geometry = installViewportGeometry(div);
276
+ const footer = el.querySelector<HTMLElement>("div.footer")!;
277
+ Object.defineProperty(footer, "offsetHeight", {
278
+ get: () => footerHeight,
279
+ configurable: true,
280
+ });
281
+ geometry.setScrollTop(400);
282
+ div.dispatchEvent(new Event("scroll"));
283
+ observers.trigger(footer);
284
+ await nextTick();
285
+ expect(geometry.scrollTo).toHaveBeenCalledWith({
286
+ top: 500,
287
+ behavior: "instant",
288
+ });
289
+
290
+ geometry.scrollTo.mockClear();
291
+ footerHeight = 80;
292
+ observers.trigger(footer);
293
+ await nextTick();
294
+ expect(geometry.scrollTo).toHaveBeenCalledWith({
295
+ top: 500,
296
+ behavior: "instant",
297
+ });
298
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
299
+ true,
300
+ );
301
+
302
+ Reflect.deleteProperty(footer, "offsetHeight");
303
+ unmount();
304
+ });
305
+ });
@@ -0,0 +1,60 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ inject,
5
+ mergeProps,
6
+ onMounted,
7
+ onScopeDispose,
8
+ shallowRef,
9
+ type SlotsType,
10
+ type VNodeChild,
11
+ } from "vue";
12
+ import { viewportInjectionKey } from "./viewportContext";
13
+
14
+ /**
15
+ * A footer container that measures its height into the viewport's content
16
+ * inset. Positions within the summed inset of the native bottom count as at
17
+ * bottom, and a growing inset re-follows a pinned viewport. Multiple footers
18
+ * sum. Typically used with `class="sticky bottom-0"`.
19
+ */
20
+ export const ThreadPrimitiveViewportFooter = defineComponent({
21
+ name: "ThreadPrimitiveViewportFooter",
22
+ inheritAttrs: false,
23
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
24
+ setup(_, { attrs, slots }) {
25
+ const viewport = inject(viewportInjectionKey, null);
26
+ const divRef = shallowRef<HTMLElement | null>(null);
27
+ let dispose: (() => void) | undefined;
28
+
29
+ onMounted(() => {
30
+ const div = divRef.value;
31
+ if (!div || !viewport) return;
32
+
33
+ const contentInset = viewport.registerContentInset();
34
+ const updateHeight = () => {
35
+ const marginTop =
36
+ Number.parseFloat(getComputedStyle(div).marginTop) || 0;
37
+ contentInset.setHeight(div.offsetHeight + marginTop);
38
+ };
39
+ updateHeight();
40
+
41
+ let resizeObserver: ResizeObserver | undefined;
42
+ if (typeof ResizeObserver !== "undefined") {
43
+ resizeObserver = new ResizeObserver(updateHeight);
44
+ resizeObserver.observe(div);
45
+ }
46
+
47
+ dispose = () => {
48
+ resizeObserver?.disconnect();
49
+ contentInset.unregister();
50
+ };
51
+ });
52
+
53
+ onScopeDispose(() => {
54
+ dispose?.();
55
+ });
56
+
57
+ return () =>
58
+ h("div", mergeProps(attrs, { ref: divRef }), slots.default?.());
59
+ },
60
+ });
@@ -0,0 +1,159 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ mergeProps,
5
+ onScopeDispose,
6
+ type SlotsType,
7
+ type VNodeChild,
8
+ } from "vue";
9
+ import { flushTapSync } from "@assistant-ui/tap";
10
+ import {
11
+ actionBarCopyDisabled,
12
+ actionBarEditDisabled,
13
+ actionBarReloadDisabled,
14
+ } from "@assistant-ui/core/store/internal";
15
+ import { isAttrDisabled } from "./attrDisabled";
16
+ import { useAui } from "../useAui";
17
+ import { useAuiState } from "../useAuiState";
18
+
19
+ /** A button that begins editing the current message. Disabled while editing. */
20
+ export const ActionBarPrimitiveEdit = defineComponent({
21
+ name: "ActionBarPrimitiveEdit",
22
+ inheritAttrs: false,
23
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
24
+ setup(_, { attrs, slots }) {
25
+ const aui = useAui();
26
+ const disabled = useAuiState(actionBarEditDisabled);
27
+ const onClick = (event: MouseEvent) => {
28
+ if (event.defaultPrevented || disabled.value || isAttrDisabled(attrs))
29
+ return;
30
+ aui.composer.beginEdit();
31
+ };
32
+ return () =>
33
+ h(
34
+ "button",
35
+ mergeProps(attrs, {
36
+ type: "button",
37
+ disabled: disabled.value || isAttrDisabled(attrs),
38
+ onClick,
39
+ }),
40
+ slots.default?.(),
41
+ );
42
+ },
43
+ });
44
+
45
+ /**
46
+ * A button that regenerates the current assistant message. Disabled while a
47
+ * run is in flight, the thread is disabled, or the message is not an
48
+ * assistant message.
49
+ */
50
+ export const ActionBarPrimitiveReload = defineComponent({
51
+ name: "ActionBarPrimitiveReload",
52
+ inheritAttrs: false,
53
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
54
+ setup(_, { attrs, slots }) {
55
+ const aui = useAui();
56
+ const disabled = useAuiState(actionBarReloadDisabled);
57
+ const onClick = (event: MouseEvent) => {
58
+ if (event.defaultPrevented || disabled.value || isAttrDisabled(attrs))
59
+ return;
60
+ aui.message.reload();
61
+ };
62
+ return () =>
63
+ h(
64
+ "button",
65
+ mergeProps(attrs, {
66
+ type: "button",
67
+ disabled: disabled.value || isAttrDisabled(attrs),
68
+ onClick,
69
+ }),
70
+ slots.default?.(),
71
+ );
72
+ },
73
+ });
74
+
75
+ const defaultCopyToClipboard = (text: string) => {
76
+ if (typeof navigator === "undefined" || !navigator.clipboard) {
77
+ return Promise.reject(new Error("Clipboard API is not available."));
78
+ }
79
+ return navigator.clipboard.writeText(text);
80
+ };
81
+
82
+ /**
83
+ * A button that copies the current message (or, while editing, the edit
84
+ * composer text). `data-copied` is present for `copiedDuration` milliseconds
85
+ * after a successful copy. Disabled while an assistant message is still
86
+ * running or the message has no text.
87
+ */
88
+ export const ActionBarPrimitiveCopy = defineComponent({
89
+ name: "ActionBarPrimitiveCopy",
90
+ inheritAttrs: false,
91
+ props: {
92
+ copiedDuration: {
93
+ type: Number,
94
+ default: 3000,
95
+ },
96
+ },
97
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
98
+ setup(props, { attrs, slots }) {
99
+ const aui = useAui();
100
+ const disabled = useAuiState(actionBarCopyDisabled);
101
+ const isCopied = useAuiState((s) => s.message.isCopied);
102
+ const isEditing = useAuiState((s) => s.composer.isEditing);
103
+ const composerText = useAuiState((s) => s.composer.text);
104
+
105
+ let copiedTimer: ReturnType<typeof setTimeout> | undefined;
106
+ let disposed = false;
107
+ onScopeDispose(() => {
108
+ disposed = true;
109
+ if (copiedTimer === undefined) return;
110
+ clearTimeout(copiedTimer);
111
+ copiedTimer = undefined;
112
+ aui.message.setIsCopied(false);
113
+ });
114
+
115
+ const onClick = (event: MouseEvent) => {
116
+ if (event.defaultPrevented || disabled.value || isAttrDisabled(attrs))
117
+ return;
118
+ const value = isEditing.value
119
+ ? composerText.value
120
+ : aui.message.getCopyText();
121
+ if (!value) return;
122
+ // A by-index scope follows whatever message occupies the slot, so late
123
+ // completions and timers re-check the copied message's id before
124
+ // touching state.
125
+ const copiedMessageId = aui.message.getState().id;
126
+ const stillCopiedMessage = () =>
127
+ !disposed && aui.message.getState().id === copiedMessageId;
128
+ // The rejection handler swallows clipboard write failures (permission
129
+ // denied, API unavailable) so they don't surface as unhandled promise
130
+ // rejections.
131
+ Promise.resolve(defaultCopyToClipboard(value)).then(
132
+ () => {
133
+ if (!stillCopiedMessage()) return;
134
+ if (copiedTimer !== undefined) clearTimeout(copiedTimer);
135
+ flushTapSync(() => aui.message.setIsCopied(true));
136
+ copiedTimer = setTimeout(() => {
137
+ copiedTimer = undefined;
138
+ if (stillCopiedMessage()) {
139
+ flushTapSync(() => aui.message.setIsCopied(false));
140
+ }
141
+ }, props.copiedDuration);
142
+ },
143
+ () => {},
144
+ );
145
+ };
146
+
147
+ return () =>
148
+ h(
149
+ "button",
150
+ mergeProps(attrs, {
151
+ type: "button",
152
+ disabled: disabled.value || isAttrDisabled(attrs),
153
+ ...(isCopied.value && { "data-copied": "" }),
154
+ onClick,
155
+ }),
156
+ slots.default?.(),
157
+ );
158
+ },
159
+ });
@@ -0,0 +1,76 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ mergeProps,
5
+ type SlotsType,
6
+ type VNodeChild,
7
+ } from "vue";
8
+ import { isAttrDisabled } from "./attrDisabled";
9
+ import { useAui } from "../useAui";
10
+ import { useAuiState } from "../useAuiState";
11
+
12
+ /** A wrapper element scoped to the current attachment. */
13
+ export const AttachmentPrimitiveRoot = defineComponent({
14
+ name: "AttachmentPrimitiveRoot",
15
+ inheritAttrs: false,
16
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
17
+ setup(_, { attrs, slots }) {
18
+ return () => h("div", mergeProps(attrs, {}), slots.default?.());
19
+ },
20
+ });
21
+
22
+ /** Renders the current attachment's file name as raw text. */
23
+ export const AttachmentPrimitiveName = defineComponent({
24
+ name: "AttachmentPrimitiveName",
25
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
26
+ setup(_, { slots }) {
27
+ const name = useAuiState((s) => s.attachment.name);
28
+ return () => slots.default?.() ?? name.value;
29
+ },
30
+ });
31
+
32
+ /**
33
+ * Renders a short label for the current attachment: the file extension when
34
+ * the name has one, otherwise the attachment type.
35
+ */
36
+ export const AttachmentPrimitiveThumb = defineComponent({
37
+ name: "AttachmentPrimitiveThumb",
38
+ inheritAttrs: false,
39
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
40
+ setup(_, { attrs, slots }) {
41
+ const label = useAuiState((s) => {
42
+ const name = s.attachment.name;
43
+ const dot = name.lastIndexOf(".");
44
+ if (dot > 0 && dot < name.length - 1) {
45
+ return `.${name.slice(dot + 1)}`;
46
+ }
47
+ return s.attachment.type;
48
+ });
49
+ return () =>
50
+ h("div", mergeProps(attrs, {}), slots.default?.() ?? label.value);
51
+ },
52
+ });
53
+
54
+ /** A button that removes the current attachment. */
55
+ export const AttachmentPrimitiveRemove = defineComponent({
56
+ name: "AttachmentPrimitiveRemove",
57
+ inheritAttrs: false,
58
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
59
+ setup(_, { attrs, slots }) {
60
+ const aui = useAui();
61
+ const onClick = (event: MouseEvent) => {
62
+ if (event.defaultPrevented || isAttrDisabled(attrs)) return;
63
+ void aui.attachment.remove();
64
+ };
65
+ return () =>
66
+ h(
67
+ "button",
68
+ mergeProps(attrs, {
69
+ type: "button",
70
+ disabled: isAttrDisabled(attrs),
71
+ onClick,
72
+ }),
73
+ slots.default?.(),
74
+ );
75
+ },
76
+ });
@@ -0,0 +1,6 @@
1
+ // A bare `disabled` template attribute arrives as "", which Boolean() would
2
+ // drop; mirror Vue's boolean-attribute semantics instead.
3
+ export const isAttrDisabled = (attrs: Record<string, unknown>): boolean =>
4
+ attrs.disabled != null &&
5
+ attrs.disabled !== false &&
6
+ attrs.disabled !== "false";
@@ -0,0 +1,77 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ mergeProps,
5
+ type ComputedRef,
6
+ type SlotsType,
7
+ type VNodeChild,
8
+ } from "vue";
9
+ import {
10
+ branchPickerNextDisabled,
11
+ branchPickerPreviousDisabled,
12
+ } from "@assistant-ui/core/store/internal";
13
+ import { isAttrDisabled } from "./attrDisabled";
14
+ import { useAui } from "../useAui";
15
+ import { useAuiState } from "../useAuiState";
16
+
17
+ const branchButton = (
18
+ name: string,
19
+ useDisabled: () => ComputedRef<boolean>,
20
+ position: "previous" | "next",
21
+ ) =>
22
+ defineComponent({
23
+ name,
24
+ inheritAttrs: false,
25
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
26
+ setup(_, { attrs, slots }) {
27
+ const aui = useAui();
28
+ const disabled = useDisabled();
29
+ const onClick = (event: MouseEvent) => {
30
+ if (event.defaultPrevented || disabled.value || isAttrDisabled(attrs))
31
+ return;
32
+ aui.message.switchToBranch({ position });
33
+ };
34
+ return () =>
35
+ h(
36
+ "button",
37
+ mergeProps(attrs, {
38
+ type: "button",
39
+ disabled: disabled.value || isAttrDisabled(attrs),
40
+ onClick,
41
+ }),
42
+ slots.default?.(),
43
+ );
44
+ },
45
+ });
46
+
47
+ /** A button that switches the current message to its previous branch. */
48
+ export const BranchPickerPrimitivePrevious = branchButton(
49
+ "BranchPickerPrimitivePrevious",
50
+ () => useAuiState(branchPickerPreviousDisabled),
51
+ "previous",
52
+ );
53
+
54
+ /** A button that switches the current message to its next branch. */
55
+ export const BranchPickerPrimitiveNext = branchButton(
56
+ "BranchPickerPrimitiveNext",
57
+ () => useAuiState(branchPickerNextDisabled),
58
+ "next",
59
+ );
60
+
61
+ /** Renders the current message's 1-based branch number as text. */
62
+ export const BranchPickerPrimitiveNumber = defineComponent({
63
+ name: "BranchPickerPrimitiveNumber",
64
+ setup() {
65
+ const branchNumber = useAuiState((s) => s.message.branchNumber);
66
+ return () => String(branchNumber.value);
67
+ },
68
+ });
69
+
70
+ /** Renders the current message's branch count as text. */
71
+ export const BranchPickerPrimitiveCount = defineComponent({
72
+ name: "BranchPickerPrimitiveCount",
73
+ setup() {
74
+ const branchCount = useAuiState((s) => s.message.branchCount);
75
+ return () => String(branchCount.value);
76
+ },
77
+ });