@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,493 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ afterEach(() => {
4
+ vi.restoreAllMocks();
5
+ });
6
+ import {
7
+ createApp,
8
+ defineComponent,
9
+ h,
10
+ nextTick,
11
+ ref,
12
+ type Component,
13
+ } from "vue";
14
+ import { flushTapSync } from "@assistant-ui/tap";
15
+ import { AuiConfig } from "@assistant-ui/store/client";
16
+ import { RuntimeAdapter } from "@assistant-ui/core/store";
17
+ import type {
18
+ AttachmentAdapter,
19
+ ExternalStoreAdapter,
20
+ PendingAttachment,
21
+ ThreadMessageLike,
22
+ } from "@assistant-ui/core";
23
+ import {
24
+ AssistantRuntimeImpl,
25
+ ExternalStoreRuntimeCore,
26
+ } from "@assistant-ui/core/internal";
27
+ import { AuiProvider } from "../AuiProvider";
28
+ import { useAui } from "../useAui";
29
+ import { useAuiState } from "../useAuiState";
30
+ import { ThreadPrimitiveMessages } from "../primitives/ThreadPrimitiveMessages";
31
+ import {
32
+ AttachmentPrimitiveName,
33
+ AttachmentPrimitiveRemove,
34
+ AttachmentPrimitiveThumb,
35
+ } from "../primitives/attachment";
36
+ import {
37
+ ComposerPrimitiveAddAttachment,
38
+ ComposerPrimitiveAttachmentDropzone,
39
+ ComposerPrimitiveAttachments,
40
+ } from "../primitives/composerAttachments";
41
+ import { MessagePrimitiveAttachments } from "../primitives/messageAttachments";
42
+
43
+ type DemoMessage = {
44
+ role: "user" | "assistant";
45
+ content: ThreadMessageLike["content"];
46
+ attachments?: ThreadMessageLike["attachments"];
47
+ };
48
+
49
+ const createTestAttachmentAdapter = (
50
+ accept = "*",
51
+ ): AttachmentAdapter & {
52
+ added: File[];
53
+ removed: string[];
54
+ } => {
55
+ const added: File[] = [];
56
+ const removed: string[] = [];
57
+ let nextId = 0;
58
+ return {
59
+ accept,
60
+ added,
61
+ removed,
62
+ async add({ file }): Promise<PendingAttachment> {
63
+ added.push(file);
64
+ return {
65
+ id: `att-${nextId++}`,
66
+ type: "file",
67
+ name: file.name,
68
+ contentType: file.type,
69
+ file,
70
+ status: { type: "requires-action", reason: "composer-send" },
71
+ };
72
+ },
73
+ async send(attachment) {
74
+ return {
75
+ ...attachment,
76
+ status: { type: "complete" },
77
+ content: [],
78
+ };
79
+ },
80
+ async remove(attachment) {
81
+ removed.push(attachment.id);
82
+ },
83
+ };
84
+ };
85
+
86
+ const createTestRuntime = ({
87
+ withAttachments = true,
88
+ accept = "*",
89
+ }: { withAttachments?: boolean; accept?: string } = {}) => {
90
+ let messages: DemoMessage[] = [];
91
+ const attachmentAdapter = createTestAttachmentAdapter(accept);
92
+ const makeAdapter = (): ExternalStoreAdapter<DemoMessage> => ({
93
+ messages,
94
+ isRunning: false,
95
+ convertMessage: (message) => ({
96
+ role: message.role,
97
+ content: message.content,
98
+ attachments: message.attachments,
99
+ }),
100
+ onNew: async () => {},
101
+ ...(withAttachments && { adapters: { attachments: attachmentAdapter } }),
102
+ });
103
+ const core = new ExternalStoreRuntimeCore(makeAdapter());
104
+ const runtime = new AssistantRuntimeImpl(core);
105
+ const append = (message: DemoMessage) => {
106
+ messages = [...messages, message];
107
+ core.setAdapter(makeAdapter());
108
+ };
109
+ return { runtime, append, attachmentAdapter };
110
+ };
111
+
112
+ const mountChat = (runtime: AssistantRuntimeImpl, view: Component) => {
113
+ let client: any;
114
+ const CaptureClient = defineComponent({
115
+ setup() {
116
+ client = useAui();
117
+ return () => null;
118
+ },
119
+ });
120
+ const app = createApp(
121
+ defineComponent({
122
+ setup: () => () =>
123
+ h(
124
+ AuiProvider,
125
+ { config: AuiConfig({ threads: RuntimeAdapter(runtime) }) },
126
+ { default: () => [h(CaptureClient), h(view)] },
127
+ ),
128
+ }),
129
+ );
130
+ const el = document.createElement("div");
131
+ app.mount(el);
132
+ return { el, client: () => client, unmount: () => app.unmount() };
133
+ };
134
+
135
+ const makeFile = (name: string, type = "text/plain") =>
136
+ new File(["data"], name, { type });
137
+
138
+ const ComposerAttachmentsView = defineComponent({
139
+ setup: () => () =>
140
+ h("div", null, [
141
+ h(ComposerPrimitiveAttachments, null, {
142
+ default: () =>
143
+ h("span", { class: "att" }, [
144
+ h(AttachmentPrimitiveName),
145
+ h(AttachmentPrimitiveThumb, { class: "thumb" }),
146
+ h(
147
+ AttachmentPrimitiveRemove,
148
+ { class: "remove" },
149
+ {
150
+ default: () => "x",
151
+ },
152
+ ),
153
+ ]),
154
+ }),
155
+ ]),
156
+ });
157
+
158
+ describe("composer attachment primitives", () => {
159
+ it("iterates composer attachments with name and thumb labels", async () => {
160
+ const { runtime } = createTestRuntime();
161
+ const { el, client, unmount } = mountChat(runtime, ComposerAttachmentsView);
162
+
163
+ await client().composer.addAttachment(makeFile("report.pdf"));
164
+ await client().composer.addAttachment(makeFile("noext", "text/plain"));
165
+ flushTapSync(() => {});
166
+
167
+ await vi.waitFor(async () => {
168
+ await nextTick();
169
+ expect(el.querySelectorAll("span.att")).toHaveLength(2);
170
+ });
171
+ const thumbs = [...el.querySelectorAll(".thumb")].map((n) => n.textContent);
172
+ expect(el.textContent).toContain("report.pdf");
173
+ expect(thumbs[0]).toBe(".pdf");
174
+ expect(thumbs[1]).toBe("file");
175
+
176
+ unmount();
177
+ });
178
+
179
+ it("removes an attachment through the remove button", async () => {
180
+ const { runtime, attachmentAdapter } = createTestRuntime();
181
+ const { el, client, unmount } = mountChat(runtime, ComposerAttachmentsView);
182
+
183
+ await client().composer.addAttachment(makeFile("a.txt"));
184
+ flushTapSync(() => {});
185
+ await vi.waitFor(async () => {
186
+ await nextTick();
187
+ expect(el.querySelector("button.remove")).not.toBeNull();
188
+ });
189
+
190
+ (el.querySelector("button.remove") as HTMLButtonElement).click();
191
+ await vi.waitFor(async () => {
192
+ await nextTick();
193
+ expect(el.querySelectorAll("span.att")).toHaveLength(0);
194
+ });
195
+ expect(attachmentAdapter.removed).toHaveLength(1);
196
+
197
+ unmount();
198
+ });
199
+
200
+ it("does not reuse component state for a different attachment", async () => {
201
+ const { runtime } = createTestRuntime();
202
+ const StatefulAttachment = defineComponent({
203
+ setup() {
204
+ const initialName = useAuiState((s) => s.attachment.name).value;
205
+ return () => h("span", { class: "stateful-att" }, initialName);
206
+ },
207
+ });
208
+ const view = defineComponent({
209
+ setup: () => () =>
210
+ h(ComposerPrimitiveAttachments, null, {
211
+ default: () => h(StatefulAttachment),
212
+ }),
213
+ });
214
+ const { el, client, unmount } = mountChat(runtime, view);
215
+
216
+ await client().composer.addAttachment(makeFile("first.txt"));
217
+ await client().composer.addAttachment(makeFile("second.txt"));
218
+ flushTapSync(() => {});
219
+ await vi.waitFor(async () => {
220
+ await nextTick();
221
+ expect(el.querySelectorAll(".stateful-att")).toHaveLength(2);
222
+ });
223
+
224
+ await client().composer.attachment({ index: 0 }).remove();
225
+ flushTapSync(() => {});
226
+ await vi.waitFor(async () => {
227
+ await nextTick();
228
+ const names = [...el.querySelectorAll(".stateful-att")].map(
229
+ (node) => node.textContent,
230
+ );
231
+ expect(names).toEqual(["second.txt"]);
232
+ });
233
+
234
+ unmount();
235
+ });
236
+
237
+ it("opens a file picker with the composer's accept filter and cleans it up", async () => {
238
+ const { runtime } = createTestRuntime();
239
+ const view = defineComponent({
240
+ setup: () => () =>
241
+ h(
242
+ ComposerPrimitiveAddAttachment,
243
+ { class: "add" },
244
+ {
245
+ default: () => "+",
246
+ },
247
+ ),
248
+ });
249
+ const { el, unmount } = mountChat(runtime, view);
250
+ await nextTick();
251
+
252
+ const button = el.querySelector("button.add") as HTMLButtonElement;
253
+ expect(button.disabled).toBe(false);
254
+ button.click();
255
+
256
+ const input = document.body.querySelector(
257
+ 'input[type="file"]',
258
+ ) as HTMLInputElement;
259
+ expect(input).not.toBeNull();
260
+ expect(input.multiple).toBe(true);
261
+ expect(input.accept).toBe("");
262
+
263
+ input.dispatchEvent(new Event("change"));
264
+ expect(document.body.querySelector('input[type="file"]')).toBeNull();
265
+
266
+ unmount();
267
+ });
268
+
269
+ it("applies a non-wildcard adapter accept to the file picker", async () => {
270
+ const { runtime } = createTestRuntime({ accept: "image/*" });
271
+ const view = defineComponent({
272
+ setup: () => () =>
273
+ h(
274
+ ComposerPrimitiveAddAttachment,
275
+ { class: "add" },
276
+ {
277
+ default: () => "+",
278
+ },
279
+ ),
280
+ });
281
+ const { el, unmount } = mountChat(runtime, view);
282
+ await nextTick();
283
+
284
+ (el.querySelector("button.add") as HTMLButtonElement).click();
285
+ const input = document.body.querySelector(
286
+ 'input[type="file"]',
287
+ ) as HTMLInputElement;
288
+ expect(input.accept).toBe("image/*");
289
+ input.remove();
290
+
291
+ unmount();
292
+ });
293
+ });
294
+
295
+ const dragEvent = (type: string, files: File[] = [], kinds = ["Files"]) => {
296
+ const event = new Event(type, { bubbles: true, cancelable: true });
297
+ Object.defineProperty(event, "dataTransfer", {
298
+ value: { types: kinds, files, dropEffect: "" },
299
+ configurable: true,
300
+ });
301
+ return event as DragEvent;
302
+ };
303
+
304
+ describe("ComposerPrimitiveAttachmentDropzone", () => {
305
+ const DropzoneView = defineComponent({
306
+ setup: () => () =>
307
+ h(
308
+ ComposerPrimitiveAttachmentDropzone,
309
+ { class: "dz" },
310
+ {
311
+ default: () => "drop here",
312
+ },
313
+ ),
314
+ });
315
+
316
+ it("flags data-dragging on file drag and adds dropped files", async () => {
317
+ const { runtime, attachmentAdapter } = createTestRuntime();
318
+ const { el, client, unmount } = mountChat(runtime, DropzoneView);
319
+ await nextTick();
320
+ const dz = el.querySelector(".dz") as HTMLElement;
321
+
322
+ dz.dispatchEvent(dragEvent("dragenter"));
323
+ await nextTick();
324
+ expect(dz.dataset["dragging"]).toBe("true");
325
+
326
+ dz.dispatchEvent(dragEvent("drop", [makeFile("dropped.txt")]));
327
+ await vi.waitFor(async () => {
328
+ await nextTick();
329
+ expect(attachmentAdapter.added).toHaveLength(1);
330
+ });
331
+ expect(dz.dataset["dragging"]).toBeUndefined();
332
+ await vi.waitFor(() => {
333
+ flushTapSync(() => {});
334
+ expect(client().composer.getState().attachments).toHaveLength(1);
335
+ });
336
+
337
+ unmount();
338
+ });
339
+
340
+ it("keeps data-dragging across descendant transitions and restores it on dragover", async () => {
341
+ const { runtime } = createTestRuntime();
342
+ const inner = defineComponent({
343
+ setup: () => () => h("span", { class: "inner" }, "target"),
344
+ });
345
+ const view = defineComponent({
346
+ setup: () => () =>
347
+ h(
348
+ ComposerPrimitiveAttachmentDropzone,
349
+ { class: "dz" },
350
+ {
351
+ default: () => h(inner),
352
+ },
353
+ ),
354
+ });
355
+ const { el, unmount } = mountChat(runtime, view);
356
+ await nextTick();
357
+ const dz = el.querySelector(".dz") as HTMLElement;
358
+ const child = el.querySelector(".inner") as HTMLElement;
359
+
360
+ dz.dispatchEvent(dragEvent("dragenter"));
361
+ await nextTick();
362
+ expect(dz.dataset["dragging"]).toBe("true");
363
+
364
+ const leaveToChild = dragEvent("dragleave");
365
+ Object.defineProperty(leaveToChild, "relatedTarget", {
366
+ value: child,
367
+ configurable: true,
368
+ });
369
+ dz.dispatchEvent(leaveToChild);
370
+ await nextTick();
371
+ expect(dz.dataset["dragging"]).toBe("true");
372
+
373
+ const leaveOutside = dragEvent("dragleave");
374
+ Object.defineProperty(leaveOutside, "relatedTarget", {
375
+ value: document.body,
376
+ configurable: true,
377
+ });
378
+ dz.dispatchEvent(leaveOutside);
379
+ await nextTick();
380
+ expect(dz.dataset["dragging"]).toBeUndefined();
381
+
382
+ dz.dispatchEvent(dragEvent("dragover"));
383
+ await nextTick();
384
+ expect(dz.dataset["dragging"]).toBe("true");
385
+
386
+ unmount();
387
+ });
388
+
389
+ it("clears dragging state when disabled during a drag", async () => {
390
+ const { runtime, attachmentAdapter } = createTestRuntime();
391
+ const disabled = ref(false);
392
+ const view = defineComponent({
393
+ setup: () => () =>
394
+ h(
395
+ ComposerPrimitiveAttachmentDropzone,
396
+ { class: "dz", disabled: disabled.value },
397
+ { default: () => "drop here" },
398
+ ),
399
+ });
400
+ const { el, unmount } = mountChat(runtime, view);
401
+ await nextTick();
402
+ const dz = el.querySelector(".dz") as HTMLElement;
403
+
404
+ dz.dispatchEvent(dragEvent("dragenter"));
405
+ await nextTick();
406
+ expect(dz.dataset["dragging"]).toBe("true");
407
+
408
+ disabled.value = true;
409
+ await nextTick();
410
+ expect(dz.dataset["dragging"]).toBeUndefined();
411
+
412
+ disabled.value = false;
413
+ await nextTick();
414
+ expect(dz.dataset["dragging"]).toBeUndefined();
415
+
416
+ disabled.value = true;
417
+ await nextTick();
418
+ dz.dispatchEvent(dragEvent("drop", [makeFile("ignored.txt")]));
419
+ await nextTick();
420
+ expect(attachmentAdapter.added).toHaveLength(0);
421
+
422
+ unmount();
423
+ });
424
+
425
+ it("ignores non-file drags and rejects drops without attachment support", async () => {
426
+ const { runtime, attachmentAdapter } = createTestRuntime({
427
+ withAttachments: false,
428
+ });
429
+ const { el, unmount } = mountChat(runtime, DropzoneView);
430
+ await nextTick();
431
+ const dz = el.querySelector(".dz") as HTMLElement;
432
+
433
+ dz.dispatchEvent(dragEvent("dragenter", [], ["text/plain"]));
434
+ await nextTick();
435
+ expect(dz.dataset["dragging"]).toBeUndefined();
436
+
437
+ const enter = dragEvent("dragenter");
438
+ dz.dispatchEvent(enter);
439
+ await nextTick();
440
+ expect(dz.dataset["dragging"]).toBeUndefined();
441
+ expect((enter as any).dataTransfer.dropEffect).toBe("none");
442
+
443
+ dz.dispatchEvent(dragEvent("drop", [makeFile("a.txt")]));
444
+ await nextTick();
445
+ expect(attachmentAdapter.added).toHaveLength(0);
446
+
447
+ unmount();
448
+ });
449
+ });
450
+
451
+ describe("MessagePrimitiveAttachments", () => {
452
+ it("iterates the current message's attachments", async () => {
453
+ const { runtime, append } = createTestRuntime();
454
+ const view = defineComponent({
455
+ setup: () => () =>
456
+ h("li", null, [
457
+ h(ThreadPrimitiveMessages, null, {
458
+ default: () =>
459
+ h(MessagePrimitiveAttachments, null, {
460
+ default: () =>
461
+ h("span", { class: "matt" }, [h(AttachmentPrimitiveName)]),
462
+ }),
463
+ }),
464
+ ]),
465
+ });
466
+ const { el, unmount } = mountChat(runtime, view);
467
+
468
+ flushTapSync(() =>
469
+ append({
470
+ role: "user",
471
+ content: [{ type: "text", text: "see attached" }],
472
+ attachments: [
473
+ {
474
+ id: "att-a",
475
+ type: "file",
476
+ name: "notes.md",
477
+ contentType: "text/markdown",
478
+ status: { type: "complete" },
479
+ content: [],
480
+ },
481
+ ],
482
+ }),
483
+ );
484
+
485
+ await vi.waitFor(async () => {
486
+ await nextTick();
487
+ expect(el.querySelector("span.matt")).not.toBeNull();
488
+ });
489
+ expect(el.textContent).toContain("notes.md");
490
+
491
+ unmount();
492
+ });
493
+ });