@assistant-ui/vue 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -2
  3. package/dist/AuiIf.d.ts +27 -0
  4. package/dist/AuiIf.d.ts.map +1 -0
  5. package/dist/AuiIf.js +30 -0
  6. package/dist/AuiIf.js.map +1 -0
  7. package/dist/AuiProvider.d.ts +41 -0
  8. package/dist/AuiProvider.d.ts.map +1 -0
  9. package/dist/AuiProvider.js +61 -0
  10. package/dist/AuiProvider.js.map +1 -0
  11. package/dist/context.d.ts +10 -0
  12. package/dist/context.d.ts.map +1 -0
  13. package/dist/context.js +17 -0
  14. package/dist/context.js.map +1 -0
  15. package/dist/index.d.ts +34 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +35 -0
  18. package/dist/primitives/AttachmentByIndexProvider.d.ts +30 -0
  19. package/dist/primitives/AttachmentByIndexProvider.d.ts.map +1 -0
  20. package/dist/primitives/AttachmentByIndexProvider.js +63 -0
  21. package/dist/primitives/AttachmentByIndexProvider.js.map +1 -0
  22. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts +11 -0
  23. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts.map +1 -0
  24. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js +31 -0
  25. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js.map +1 -0
  26. package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts +19 -0
  27. package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts.map +1 -0
  28. package/dist/primitives/ChainOfThoughtPrimitiveParts.js +85 -0
  29. package/dist/primitives/ChainOfThoughtPrimitiveParts.js.map +1 -0
  30. package/dist/primitives/ComposerPrimitiveCancel.d.ts +12 -0
  31. package/dist/primitives/ComposerPrimitiveCancel.d.ts.map +1 -0
  32. package/dist/primitives/ComposerPrimitiveCancel.js +33 -0
  33. package/dist/primitives/ComposerPrimitiveCancel.js.map +1 -0
  34. package/dist/primitives/ComposerPrimitiveInput.d.ts +26 -0
  35. package/dist/primitives/ComposerPrimitiveInput.d.ts.map +1 -0
  36. package/dist/primitives/ComposerPrimitiveInput.js +57 -0
  37. package/dist/primitives/ComposerPrimitiveInput.js.map +1 -0
  38. package/dist/primitives/ComposerPrimitiveSend.d.ts +13 -0
  39. package/dist/primitives/ComposerPrimitiveSend.d.ts.map +1 -0
  40. package/dist/primitives/ComposerPrimitiveSend.js +31 -0
  41. package/dist/primitives/ComposerPrimitiveSend.js.map +1 -0
  42. package/dist/primitives/MessageByIdProvider.d.ts +23 -0
  43. package/dist/primitives/MessageByIdProvider.d.ts.map +1 -0
  44. package/dist/primitives/MessageByIdProvider.js +70 -0
  45. package/dist/primitives/MessageByIdProvider.js.map +1 -0
  46. package/dist/primitives/MessagePrimitiveParts.d.ts +51 -0
  47. package/dist/primitives/MessagePrimitiveParts.d.ts.map +1 -0
  48. package/dist/primitives/MessagePrimitiveParts.js +93 -0
  49. package/dist/primitives/MessagePrimitiveParts.js.map +1 -0
  50. package/dist/primitives/PartByIndexProvider.d.ts +21 -0
  51. package/dist/primitives/PartByIndexProvider.d.ts.map +1 -0
  52. package/dist/primitives/PartByIndexProvider.js +50 -0
  53. package/dist/primitives/PartByIndexProvider.js.map +1 -0
  54. package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts +7 -0
  55. package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts.map +1 -0
  56. package/dist/primitives/ThreadListItemPrimitiveRoot.js +36 -0
  57. package/dist/primitives/ThreadListItemPrimitiveRoot.js.map +1 -0
  58. package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts +14 -0
  59. package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts.map +1 -0
  60. package/dist/primitives/ThreadListItemPrimitiveTrigger.js +58 -0
  61. package/dist/primitives/ThreadListItemPrimitiveTrigger.js.map +1 -0
  62. package/dist/primitives/ThreadListPrimitiveRoot.d.ts +7 -0
  63. package/dist/primitives/ThreadListPrimitiveRoot.d.ts.map +1 -0
  64. package/dist/primitives/ThreadListPrimitiveRoot.js +16 -0
  65. package/dist/primitives/ThreadListPrimitiveRoot.js.map +1 -0
  66. package/dist/primitives/ThreadPrimitiveMessages.d.ts +21 -0
  67. package/dist/primitives/ThreadPrimitiveMessages.d.ts.map +1 -0
  68. package/dist/primitives/ThreadPrimitiveMessages.js +35 -0
  69. package/dist/primitives/ThreadPrimitiveMessages.js.map +1 -0
  70. package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts +25 -0
  71. package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts.map +1 -0
  72. package/dist/primitives/ThreadPrimitiveScrollToBottom.js +43 -0
  73. package/dist/primitives/ThreadPrimitiveScrollToBottom.js.map +1 -0
  74. package/dist/primitives/ThreadPrimitiveViewport.d.ts +57 -0
  75. package/dist/primitives/ThreadPrimitiveViewport.d.ts.map +1 -0
  76. package/dist/primitives/ThreadPrimitiveViewport.js +99 -0
  77. package/dist/primitives/ThreadPrimitiveViewport.js.map +1 -0
  78. package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts +13 -0
  79. package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts.map +1 -0
  80. package/dist/primitives/ThreadPrimitiveViewportFooter.js +46 -0
  81. package/dist/primitives/ThreadPrimitiveViewportFooter.js.map +1 -0
  82. package/dist/primitives/actionBar.d.ts +41 -0
  83. package/dist/primitives/actionBar.d.ts.map +1 -0
  84. package/dist/primitives/actionBar.js +110 -0
  85. package/dist/primitives/actionBar.js.map +1 -0
  86. package/dist/primitives/attachment.d.ts +27 -0
  87. package/dist/primitives/attachment.d.ts.map +1 -0
  88. package/dist/primitives/attachment.js +63 -0
  89. package/dist/primitives/attachment.js.map +1 -0
  90. package/dist/primitives/attrDisabled.d.ts +2 -0
  91. package/dist/primitives/attrDisabled.d.ts.map +1 -0
  92. package/dist/primitives/attrDisabled.js +6 -0
  93. package/dist/primitives/attrDisabled.js.map +1 -0
  94. package/dist/primitives/branchPicker.d.ts +18 -0
  95. package/dist/primitives/branchPicker.d.ts.map +1 -0
  96. package/dist/primitives/branchPicker.js +48 -0
  97. package/dist/primitives/branchPicker.js.map +1 -0
  98. package/dist/primitives/composerAttachments.d.ts +57 -0
  99. package/dist/primitives/composerAttachments.d.ts.map +1 -0
  100. package/dist/primitives/composerAttachments.js +132 -0
  101. package/dist/primitives/composerAttachments.js.map +1 -0
  102. package/dist/primitives/error.d.ts +15 -0
  103. package/dist/primitives/error.d.ts.map +1 -0
  104. package/dist/primitives/error.js +29 -0
  105. package/dist/primitives/error.js.map +1 -0
  106. package/dist/primitives/lastValidCache.d.ts +6 -0
  107. package/dist/primitives/lastValidCache.d.ts.map +1 -0
  108. package/dist/primitives/lastValidCache.js +9 -0
  109. package/dist/primitives/lastValidCache.js.map +1 -0
  110. package/dist/primitives/message.d.ts +12 -0
  111. package/dist/primitives/message.d.ts.map +1 -0
  112. package/dist/primitives/message.js +44 -0
  113. package/dist/primitives/message.js.map +1 -0
  114. package/dist/primitives/messageAttachments.d.ts +12 -0
  115. package/dist/primitives/messageAttachments.d.ts.map +1 -0
  116. package/dist/primitives/messageAttachments.js +27 -0
  117. package/dist/primitives/messageAttachments.js.map +1 -0
  118. package/dist/primitives/stableKeys.d.ts +5 -0
  119. package/dist/primitives/stableKeys.d.ts.map +1 -0
  120. package/dist/primitives/stableKeys.js +14 -0
  121. package/dist/primitives/stableKeys.js.map +1 -0
  122. package/dist/primitives/suggestions.d.ts +66 -0
  123. package/dist/primitives/suggestions.d.ts.map +1 -0
  124. package/dist/primitives/suggestions.js +137 -0
  125. package/dist/primitives/suggestions.js.map +1 -0
  126. package/dist/primitives/thread.d.ts +11 -0
  127. package/dist/primitives/thread.d.ts.map +1 -0
  128. package/dist/primitives/thread.js +37 -0
  129. package/dist/primitives/thread.js.map +1 -0
  130. package/dist/primitives/threadList.d.ts +82 -0
  131. package/dist/primitives/threadList.d.ts.map +1 -0
  132. package/dist/primitives/threadList.js +128 -0
  133. package/dist/primitives/threadList.js.map +1 -0
  134. package/dist/primitives/threadListFocusGroup.d.ts +14 -0
  135. package/dist/primitives/threadListFocusGroup.d.ts.map +1 -0
  136. package/dist/primitives/threadListFocusGroup.js +32 -0
  137. package/dist/primitives/threadListFocusGroup.js.map +1 -0
  138. package/dist/primitives/threadListStructural.d.ts +29 -0
  139. package/dist/primitives/threadListStructural.d.ts.map +1 -0
  140. package/dist/primitives/threadListStructural.js +55 -0
  141. package/dist/primitives/threadListStructural.js.map +1 -0
  142. package/dist/primitives/useComposerSendState.d.ts +6 -0
  143. package/dist/primitives/useComposerSendState.d.ts.map +1 -0
  144. package/dist/primitives/useComposerSendState.js +15 -0
  145. package/dist/primitives/useComposerSendState.js.map +1 -0
  146. package/dist/primitives/viewportContext.d.ts +12 -0
  147. package/dist/primitives/viewportContext.d.ts.map +1 -0
  148. package/dist/primitives/viewportContext.js +6 -0
  149. package/dist/primitives/viewportContext.js.map +1 -0
  150. package/dist/useAui.d.ts +15 -0
  151. package/dist/useAui.d.ts.map +1 -0
  152. package/dist/useAui.js +21 -0
  153. package/dist/useAui.js.map +1 -0
  154. package/dist/useAuiEvent.d.ts +17 -0
  155. package/dist/useAuiEvent.d.ts.map +1 -0
  156. package/dist/useAuiEvent.js +52 -0
  157. package/dist/useAuiEvent.js.map +1 -0
  158. package/dist/useAuiState.d.ts +23 -0
  159. package/dist/useAuiState.d.ts.map +1 -0
  160. package/dist/useAuiState.js +42 -0
  161. package/dist/useAuiState.js.map +1 -0
  162. package/dist/useScrollLock.d.ts +3 -0
  163. package/dist/useScrollLock.d.ts.map +1 -0
  164. package/dist/useScrollLock.js +53 -0
  165. package/dist/useScrollLock.js.map +1 -0
  166. package/package.json +57 -6
  167. package/src/AuiIf.ts +34 -0
  168. package/src/AuiProvider.ts +90 -0
  169. package/src/__tests__/AuiIf.test.ts +54 -0
  170. package/src/__tests__/AuiProvider.test.ts +266 -0
  171. package/src/__tests__/byindex-shrink-race.test.ts +411 -0
  172. package/src/__tests__/dist-graph.test.ts +49 -0
  173. package/src/__tests__/fixtures.ts +51 -0
  174. package/src/__tests__/primitives-actionbar.test.ts +469 -0
  175. package/src/__tests__/primitives-attachment.test.ts +493 -0
  176. package/src/__tests__/primitives-content.test.ts +829 -0
  177. package/src/__tests__/primitives-keyed-items.test.ts +270 -0
  178. package/src/__tests__/primitives-keyed-messages.test.ts +264 -0
  179. package/src/__tests__/primitives-structural.test.ts +573 -0
  180. package/src/__tests__/primitives-suggestions.test.ts +374 -0
  181. package/src/__tests__/primitives-threadlist.test.ts +443 -0
  182. package/src/__tests__/primitives-tool-ui.test.ts +579 -0
  183. package/src/__tests__/primitives.test.ts +408 -0
  184. package/src/__tests__/useAuiEvent.test.ts +159 -0
  185. package/src/__tests__/useAuiState.test.ts +124 -0
  186. package/src/context.ts +31 -0
  187. package/src/index.ts +87 -0
  188. package/src/primitives/AttachmentByIndexProvider.ts +88 -0
  189. package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.test.ts +105 -0
  190. package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.ts +38 -0
  191. package/src/primitives/ChainOfThoughtPrimitiveParts.test.ts +254 -0
  192. package/src/primitives/ChainOfThoughtPrimitiveParts.ts +112 -0
  193. package/src/primitives/ComposerPrimitiveCancel.ts +41 -0
  194. package/src/primitives/ComposerPrimitiveInput.ts +70 -0
  195. package/src/primitives/ComposerPrimitiveSend.ts +39 -0
  196. package/src/primitives/MessageByIdProvider.ts +89 -0
  197. package/src/primitives/MessagePrimitiveParts.tool-interaction.test.ts +88 -0
  198. package/src/primitives/MessagePrimitiveParts.ts +167 -0
  199. package/src/primitives/PartByIndexProvider.ts +62 -0
  200. package/src/primitives/ThreadListItemPrimitiveRoot.ts +57 -0
  201. package/src/primitives/ThreadListItemPrimitiveTrigger.ts +74 -0
  202. package/src/primitives/ThreadListKeyboardNav.test.ts +298 -0
  203. package/src/primitives/ThreadListPrimitiveRoot.ts +18 -0
  204. package/src/primitives/ThreadPrimitiveMessages.ts +38 -0
  205. package/src/primitives/ThreadPrimitiveScrollToBottom.ts +65 -0
  206. package/src/primitives/ThreadPrimitiveViewport.ts +120 -0
  207. package/src/primitives/ThreadPrimitiveViewportFooter.test.ts +400 -0
  208. package/src/primitives/ThreadPrimitiveViewportFooter.ts +60 -0
  209. package/src/primitives/actionBar.ts +159 -0
  210. package/src/primitives/attachment.ts +76 -0
  211. package/src/primitives/attrDisabled.ts +6 -0
  212. package/src/primitives/branchPicker.ts +77 -0
  213. package/src/primitives/composerAttachments.ts +181 -0
  214. package/src/primitives/error.ts +36 -0
  215. package/src/primitives/lastValidCache.ts +12 -0
  216. package/src/primitives/message.ts +54 -0
  217. package/src/primitives/messageAttachments.ts +32 -0
  218. package/src/primitives/stableKeys.test.ts +19 -0
  219. package/src/primitives/stableKeys.ts +21 -0
  220. package/src/primitives/suggestions.ts +179 -0
  221. package/src/primitives/thread.ts +46 -0
  222. package/src/primitives/threadList.ts +163 -0
  223. package/src/primitives/threadListFocusGroup.ts +56 -0
  224. package/src/primitives/threadListStructural.ts +86 -0
  225. package/src/primitives/useComposerSendState.ts +13 -0
  226. package/src/primitives/viewportContext.ts +16 -0
  227. package/src/useAui.ts +18 -0
  228. package/src/useAuiEvent.ts +64 -0
  229. package/src/useAuiState.ts +55 -0
  230. package/src/useScrollLock.test.ts +207 -0
  231. package/src/useScrollLock.ts +79 -0
@@ -0,0 +1,829 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ afterEach(() => {
4
+ vi.restoreAllMocks();
5
+ clearPartWarningsForTesting();
6
+ clearScrollToBottomWarningForTesting();
7
+ });
8
+ import { createApp, defineComponent, h, nextTick, type Component } from "vue";
9
+ import { flushTapSync } from "@assistant-ui/tap";
10
+ import { AuiConfig } from "@assistant-ui/store/client";
11
+ import { RuntimeAdapter } from "@assistant-ui/core/store";
12
+ import type {
13
+ ExternalStoreAdapter,
14
+ ThreadMessageLike,
15
+ } from "@assistant-ui/core";
16
+ import {
17
+ AssistantRuntimeImpl,
18
+ ExternalStoreRuntimeCore,
19
+ } from "@assistant-ui/core/internal";
20
+ import { AuiProvider } from "../AuiProvider";
21
+ import { useAuiState } from "../useAuiState";
22
+ import { ThreadPrimitiveMessages } from "../primitives/ThreadPrimitiveMessages";
23
+ import { ThreadPrimitiveViewport } from "../primitives/ThreadPrimitiveViewport";
24
+ import { ThreadPrimitiveViewportFooter } from "../primitives/ThreadPrimitiveViewportFooter";
25
+ import { MessagePrimitiveRoot } from "../primitives/message";
26
+ import {
27
+ ThreadPrimitiveScrollToBottom,
28
+ clearScrollToBottomWarningForTesting,
29
+ } from "../primitives/ThreadPrimitiveScrollToBottom";
30
+ import {
31
+ MessagePrimitiveParts,
32
+ clearPartWarningsForTesting,
33
+ } from "../primitives/MessagePrimitiveParts";
34
+ import {
35
+ BranchPickerPrimitiveCount,
36
+ BranchPickerPrimitiveNext,
37
+ BranchPickerPrimitiveNumber,
38
+ BranchPickerPrimitivePrevious,
39
+ } from "../primitives/branchPicker";
40
+
41
+ type DemoMessage = {
42
+ role: "user" | "assistant";
43
+ content: ThreadMessageLike["content"];
44
+ };
45
+
46
+ const createTestRuntime = () => {
47
+ let messages: DemoMessage[] = [];
48
+ let isRunning = false;
49
+ const makeAdapter = (): ExternalStoreAdapter<DemoMessage> => ({
50
+ messages,
51
+ isRunning,
52
+ convertMessage: (message) => ({
53
+ role: message.role,
54
+ content: message.content,
55
+ }),
56
+ onNew: async () => {},
57
+ });
58
+ const core = new ExternalStoreRuntimeCore(makeAdapter());
59
+ const runtime = new AssistantRuntimeImpl(core);
60
+ const append = (message: DemoMessage) => {
61
+ messages = [...messages, message];
62
+ core.setAdapter(makeAdapter());
63
+ };
64
+ const setRunning = (value: boolean) => {
65
+ isRunning = value;
66
+ core.setAdapter(makeAdapter());
67
+ };
68
+ return { runtime, append, setRunning };
69
+ };
70
+
71
+ const mockViewportGeometry = (div: HTMLElement) => {
72
+ Object.defineProperty(div, "scrollHeight", {
73
+ get: () => 500,
74
+ configurable: true,
75
+ });
76
+ Object.defineProperty(div, "clientHeight", {
77
+ get: () => 100,
78
+ configurable: true,
79
+ });
80
+ const scrollTo = vi.fn();
81
+ Object.defineProperty(div, "scrollTo", {
82
+ value: scrollTo,
83
+ configurable: true,
84
+ });
85
+ return scrollTo;
86
+ };
87
+
88
+ const mountChat = (runtime: AssistantRuntimeImpl, view: Component) => {
89
+ const app = createApp(
90
+ defineComponent({
91
+ setup: () => () =>
92
+ h(
93
+ AuiProvider,
94
+ { config: AuiConfig({ threads: RuntimeAdapter(runtime) }) },
95
+ { default: () => h(view) },
96
+ ),
97
+ }),
98
+ );
99
+ const el = document.createElement("div");
100
+ app.mount(el);
101
+ return { el, unmount: () => app.unmount() };
102
+ };
103
+
104
+ describe("MessagePrimitiveParts", () => {
105
+ it("renders text parts by default and routes typed slots in order", async () => {
106
+ const { runtime, append } = createTestRuntime();
107
+ const View = defineComponent({
108
+ setup: () => () =>
109
+ h(ThreadPrimitiveMessages, null, {
110
+ default: () =>
111
+ h("li", null, [
112
+ h(MessagePrimitiveParts, null, {
113
+ "tool-call": () => {
114
+ const ToolProbe = defineComponent({
115
+ setup() {
116
+ const name = useAuiState((s) =>
117
+ s.part.type === "tool-call" ? s.part.toolName : "",
118
+ );
119
+ return () => h("span", { class: "tool" }, name.value);
120
+ },
121
+ });
122
+ return h(ToolProbe);
123
+ },
124
+ }),
125
+ ]),
126
+ }),
127
+ });
128
+ const { el, unmount } = mountChat(runtime, View);
129
+
130
+ flushTapSync(() =>
131
+ append({
132
+ role: "assistant",
133
+ content: [
134
+ { type: "text", text: "before " },
135
+ {
136
+ type: "tool-call",
137
+ toolCallId: "call-1",
138
+ toolName: "search",
139
+ args: {},
140
+ },
141
+ { type: "text", text: " after" },
142
+ ],
143
+ }),
144
+ );
145
+
146
+ await vi.waitFor(async () => {
147
+ await nextTick();
148
+ expect(el.querySelector("li")).not.toBeNull();
149
+ expect(el.querySelector("span.tool")?.textContent).toBe("search");
150
+ });
151
+ expect(el.querySelector("li")!.textContent).toBe("before search after");
152
+
153
+ unmount();
154
+ });
155
+
156
+ it("keeps text rendering when only a default slot is provided", async () => {
157
+ const { runtime, append } = createTestRuntime();
158
+ const View = defineComponent({
159
+ setup: () => () =>
160
+ h("li", null, [
161
+ h(ThreadPrimitiveMessages, null, {
162
+ default: () =>
163
+ h(MessagePrimitiveParts, null, {
164
+ default: () => h("span", { class: "fallback" }, "[tool]"),
165
+ }),
166
+ }),
167
+ ]),
168
+ });
169
+ const { el, unmount } = mountChat(runtime, View);
170
+
171
+ flushTapSync(() =>
172
+ append({
173
+ role: "assistant",
174
+ content: [
175
+ { type: "text", text: "hello" },
176
+ {
177
+ type: "tool-call",
178
+ toolCallId: "call-1",
179
+ toolName: "search",
180
+ args: {},
181
+ },
182
+ ],
183
+ }),
184
+ );
185
+
186
+ await vi.waitFor(async () => {
187
+ await nextTick();
188
+ expect(el.querySelector("span.fallback")).not.toBeNull();
189
+ });
190
+ expect(el.querySelector("li")!.textContent).toBe("hello[tool]");
191
+
192
+ unmount();
193
+ });
194
+ });
195
+
196
+ describe("MessagePrimitiveParts dev warning", () => {
197
+ it("warns once in dev for a part type without a slot", async () => {
198
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
199
+ const { runtime, append } = createTestRuntime();
200
+ const View = defineComponent({
201
+ setup: () => () =>
202
+ h(ThreadPrimitiveMessages, null, {
203
+ default: () => h(MessagePrimitiveParts),
204
+ }),
205
+ });
206
+ const { unmount } = mountChat(runtime, View);
207
+
208
+ flushTapSync(() =>
209
+ append({
210
+ role: "assistant",
211
+ content: [
212
+ {
213
+ type: "tool-call",
214
+ toolCallId: "call-1",
215
+ toolName: "search",
216
+ args: {},
217
+ },
218
+ ],
219
+ }),
220
+ );
221
+ await vi.waitFor(async () => {
222
+ await nextTick();
223
+ expect(warn).toHaveBeenCalledTimes(1);
224
+ });
225
+ expect(warn.mock.calls[0]![0]).toContain(
226
+ 'no slot for part type "tool-call"',
227
+ );
228
+
229
+ unmount();
230
+ });
231
+ });
232
+
233
+ describe("BranchPickerPrimitive", () => {
234
+ it("disables both directions and renders 1/1 on a single branch", async () => {
235
+ const { runtime, append } = createTestRuntime();
236
+ const View = defineComponent({
237
+ setup: () => () =>
238
+ h(ThreadPrimitiveMessages, null, {
239
+ default: () => [
240
+ h(
241
+ BranchPickerPrimitivePrevious,
242
+ { class: "prev" },
243
+ { default: () => "<" },
244
+ ),
245
+ h("span", { class: "pos" }, [
246
+ h(BranchPickerPrimitiveNumber),
247
+ " / ",
248
+ h(BranchPickerPrimitiveCount),
249
+ ]),
250
+ h(
251
+ BranchPickerPrimitiveNext,
252
+ { class: "next" },
253
+ { default: () => ">" },
254
+ ),
255
+ ],
256
+ }),
257
+ });
258
+ const { el, unmount } = mountChat(runtime, View);
259
+
260
+ flushTapSync(() =>
261
+ append({ role: "user", content: [{ type: "text", text: "hi" }] }),
262
+ );
263
+ await vi.waitFor(async () => {
264
+ await nextTick();
265
+ expect(el.querySelector(".pos")).not.toBeNull();
266
+ });
267
+
268
+ expect(el.querySelector(".pos")!.textContent).toBe("1 / 1");
269
+ expect(el.querySelector<HTMLButtonElement>(".prev")!.disabled).toBe(true);
270
+ expect(el.querySelector<HTMLButtonElement>(".next")!.disabled).toBe(true);
271
+
272
+ unmount();
273
+ });
274
+ });
275
+
276
+ describe("ThreadPrimitiveViewport", () => {
277
+ it("mounts as a scroll container and renders its slot", async () => {
278
+ const { runtime, append } = createTestRuntime();
279
+ const View = defineComponent({
280
+ setup: () => () =>
281
+ h(
282
+ ThreadPrimitiveViewport,
283
+ { class: "viewport" },
284
+ { default: () => h("p", { class: "content" }, "hello") },
285
+ ),
286
+ });
287
+ const { el, unmount } = mountChat(runtime, View);
288
+
289
+ expect(el.querySelector("div.viewport p.content")?.textContent).toBe(
290
+ "hello",
291
+ );
292
+
293
+ flushTapSync(() =>
294
+ append({ role: "user", content: [{ type: "text", text: "hi" }] }),
295
+ );
296
+ await nextTick();
297
+
298
+ unmount();
299
+ });
300
+
301
+ it("follows content growth while pinned and stops after a user scrolls up", async () => {
302
+ const { runtime, append } = createTestRuntime();
303
+ const View = defineComponent({
304
+ setup: () => () =>
305
+ h(
306
+ ThreadPrimitiveViewport,
307
+ { class: "viewport" },
308
+ {
309
+ default: () =>
310
+ h(ThreadPrimitiveMessages, null, {
311
+ default: () => h("p", "row"),
312
+ }),
313
+ },
314
+ ),
315
+ });
316
+ const { el, unmount } = mountChat(runtime, View);
317
+
318
+ const div = el.querySelector<HTMLElement>("div.viewport")!;
319
+ let scrollHeight = 500;
320
+ let scrollTop = 0;
321
+ Object.defineProperty(div, "scrollHeight", {
322
+ get: () => scrollHeight,
323
+ configurable: true,
324
+ });
325
+ Object.defineProperty(div, "clientHeight", {
326
+ get: () => 100,
327
+ configurable: true,
328
+ });
329
+ Object.defineProperty(div, "scrollTop", {
330
+ get: () => scrollTop,
331
+ set: (value: number) => {
332
+ scrollTop = value;
333
+ },
334
+ configurable: true,
335
+ });
336
+ const scrollTo = vi.fn(({ top }: { top: number }) => {
337
+ scrollTop = Math.max(0, Math.min(top, scrollHeight - 100));
338
+ });
339
+ Object.defineProperty(div, "scrollTo", {
340
+ value: scrollTo,
341
+ configurable: true,
342
+ });
343
+
344
+ flushTapSync(() =>
345
+ append({ role: "user", content: [{ type: "text", text: "one" }] }),
346
+ );
347
+ await vi.waitFor(async () => {
348
+ await nextTick();
349
+ expect(scrollTo).toHaveBeenCalled();
350
+ });
351
+ div.dispatchEvent(new Event("scroll"));
352
+ const callsWhilePinned = scrollTo.mock.calls.length;
353
+
354
+ scrollHeight = 600;
355
+ flushTapSync(() =>
356
+ append({ role: "assistant", content: [{ type: "text", text: "two" }] }),
357
+ );
358
+ await vi.waitFor(async () => {
359
+ await nextTick();
360
+ expect(scrollTo.mock.calls.length).toBeGreaterThan(callsWhilePinned);
361
+ });
362
+ div.dispatchEvent(new Event("scroll"));
363
+ await new Promise((resolve) => setTimeout(resolve, 30));
364
+
365
+ div.dispatchEvent(new Event("pointerdown"));
366
+ scrollTop = 50;
367
+ div.dispatchEvent(new Event("scroll"));
368
+ const callsAfterUnpin = scrollTo.mock.calls.length;
369
+
370
+ scrollHeight = 700;
371
+ flushTapSync(() =>
372
+ append({ role: "user", content: [{ type: "text", text: "three" }] }),
373
+ );
374
+ await nextTick();
375
+ await new Promise((resolve) => setTimeout(resolve, 50));
376
+ expect(scrollTo.mock.calls.length).toBe(callsAfterUnpin);
377
+
378
+ unmount();
379
+ });
380
+
381
+ it("pauses following when a message disclosure expands", async () => {
382
+ const { runtime, append } = createTestRuntime();
383
+ const View = defineComponent({
384
+ setup: () => () =>
385
+ h(
386
+ ThreadPrimitiveViewport,
387
+ {
388
+ class: "viewport",
389
+ scrollToBottomOnInitialize: false,
390
+ },
391
+ {
392
+ default: () =>
393
+ h(ThreadPrimitiveMessages, null, {
394
+ default: () =>
395
+ h(MessagePrimitiveRoot, null, {
396
+ default: () =>
397
+ h(
398
+ "button",
399
+ { class: "disclosure", "aria-expanded": "false" },
400
+ "Expand",
401
+ ),
402
+ }),
403
+ }),
404
+ },
405
+ ),
406
+ });
407
+ const { el, unmount } = mountChat(runtime, View);
408
+ const div = el.querySelector<HTMLElement>("div.viewport")!;
409
+ let scrollHeight = 500;
410
+ let scrollTop = 400;
411
+ Object.defineProperties(div, {
412
+ scrollHeight: {
413
+ get: () => scrollHeight,
414
+ configurable: true,
415
+ },
416
+ clientHeight: { value: 100, configurable: true },
417
+ scrollTop: {
418
+ get: () => scrollTop,
419
+ set: (value: number) => {
420
+ scrollTop = value;
421
+ },
422
+ configurable: true,
423
+ },
424
+ });
425
+ const scrollTo = vi.fn(({ top }: { top: number }) => {
426
+ scrollTop = Math.max(0, Math.min(top, scrollHeight - 100));
427
+ div.dispatchEvent(new Event("scroll"));
428
+ });
429
+ Object.defineProperty(div, "scrollTo", {
430
+ value: scrollTo,
431
+ configurable: true,
432
+ });
433
+
434
+ flushTapSync(() =>
435
+ append({ role: "assistant", content: [{ type: "text", text: "one" }] }),
436
+ );
437
+ await vi.waitFor(async () => {
438
+ await nextTick();
439
+ expect(el.querySelector("[data-message-id]")).not.toBeNull();
440
+ expect(scrollTo).toHaveBeenCalled();
441
+ });
442
+ scrollTo.mockClear();
443
+
444
+ const disclosure =
445
+ el.querySelector<HTMLButtonElement>("button.disclosure")!;
446
+ disclosure.addEventListener("click", (event) => {
447
+ disclosure.setAttribute("aria-expanded", "true");
448
+ event.stopPropagation();
449
+ });
450
+ disclosure.click();
451
+ scrollHeight = 600;
452
+ el.querySelector("[data-message-id]")!.append(document.createElement("p"));
453
+ await new Promise((resolve) => setTimeout(resolve, 30));
454
+
455
+ expect(scrollTo).not.toHaveBeenCalled();
456
+ expect(scrollTop).toBe(400);
457
+ unmount();
458
+ });
459
+
460
+ it("stays put when every scroll option is disabled", async () => {
461
+ const { runtime, append } = createTestRuntime();
462
+ const View = defineComponent({
463
+ setup: () => () =>
464
+ h(
465
+ ThreadPrimitiveViewport,
466
+ {
467
+ class: "viewport",
468
+ autoScroll: false,
469
+ scrollToBottomOnInitialize: false,
470
+ scrollToBottomOnRunStart: false,
471
+ },
472
+ {
473
+ default: () =>
474
+ h(ThreadPrimitiveMessages, null, {
475
+ default: () => h("p", "row"),
476
+ }),
477
+ },
478
+ ),
479
+ });
480
+ const { el, unmount } = mountChat(runtime, View);
481
+
482
+ const div = el.querySelector<HTMLElement>("div.viewport")!;
483
+ Object.defineProperty(div, "scrollHeight", {
484
+ get: () => 500,
485
+ configurable: true,
486
+ });
487
+ Object.defineProperty(div, "clientHeight", {
488
+ get: () => 100,
489
+ configurable: true,
490
+ });
491
+ const scrollTo = vi.fn();
492
+ Object.defineProperty(div, "scrollTo", {
493
+ value: scrollTo,
494
+ configurable: true,
495
+ });
496
+
497
+ flushTapSync(() =>
498
+ append({ role: "user", content: [{ type: "text", text: "one" }] }),
499
+ );
500
+ await nextTick();
501
+ await new Promise((resolve) => setTimeout(resolve, 50));
502
+ expect(scrollTo).not.toHaveBeenCalled();
503
+
504
+ unmount();
505
+ });
506
+
507
+ it("scrolls on run start and stays put when that option is disabled", async () => {
508
+ const active = createTestRuntime();
509
+ const ActiveView = defineComponent({
510
+ setup: () => () =>
511
+ h(
512
+ ThreadPrimitiveViewport,
513
+ { class: "viewport" },
514
+ { default: () => h("p", "row") },
515
+ ),
516
+ });
517
+ const mountedActive = mountChat(active.runtime, ActiveView);
518
+ const activeScrollTo = mockViewportGeometry(
519
+ mountedActive.el.querySelector<HTMLElement>("div.viewport")!,
520
+ );
521
+
522
+ flushTapSync(() => active.setRunning(true));
523
+ await vi.waitFor(() => {
524
+ expect(activeScrollTo).toHaveBeenCalled();
525
+ });
526
+ mountedActive.unmount();
527
+
528
+ const disabled = createTestRuntime();
529
+ const DisabledView = defineComponent({
530
+ setup: () => () =>
531
+ h(
532
+ ThreadPrimitiveViewport,
533
+ {
534
+ class: "viewport",
535
+ autoScroll: false,
536
+ scrollToBottomOnInitialize: false,
537
+ scrollToBottomOnRunStart: false,
538
+ },
539
+ { default: () => h("p", "row") },
540
+ ),
541
+ });
542
+ const mountedDisabled = mountChat(disabled.runtime, DisabledView);
543
+ const disabledScrollTo = mockViewportGeometry(
544
+ mountedDisabled.el.querySelector<HTMLElement>("div.viewport")!,
545
+ );
546
+
547
+ flushTapSync(() => disabled.setRunning(true));
548
+ await new Promise((resolve) => setTimeout(resolve, 50));
549
+ expect(disabledScrollTo).not.toHaveBeenCalled();
550
+
551
+ mountedDisabled.unmount();
552
+ });
553
+
554
+ it("enables scroll-to-bottom only while scrolled up and scrolls on click", async () => {
555
+ const { runtime } = createTestRuntime();
556
+ const View = defineComponent({
557
+ setup: () => () =>
558
+ h(
559
+ ThreadPrimitiveViewport,
560
+ { class: "viewport" },
561
+ {
562
+ default: () => [
563
+ h(ThreadPrimitiveMessages, null, {
564
+ default: () => h("p", "row"),
565
+ }),
566
+ h(
567
+ ThreadPrimitiveScrollToBottom,
568
+ { class: "jump" },
569
+ { default: () => "Jump" },
570
+ ),
571
+ ],
572
+ },
573
+ ),
574
+ });
575
+ const { el, unmount } = mountChat(runtime, View);
576
+
577
+ const div = el.querySelector<HTMLElement>("div.viewport")!;
578
+ let scrollTop = 400;
579
+ Object.defineProperty(div, "scrollHeight", {
580
+ get: () => 500,
581
+ configurable: true,
582
+ });
583
+ Object.defineProperty(div, "clientHeight", {
584
+ get: () => 100,
585
+ configurable: true,
586
+ });
587
+ Object.defineProperty(div, "scrollTop", {
588
+ get: () => scrollTop,
589
+ set: (value: number) => {
590
+ scrollTop = value;
591
+ },
592
+ configurable: true,
593
+ });
594
+ const scrollTo = vi.fn(({ top }: { top: number }) => {
595
+ scrollTop = Math.max(0, Math.min(top, 400));
596
+ });
597
+ Object.defineProperty(div, "scrollTo", {
598
+ value: scrollTo,
599
+ configurable: true,
600
+ });
601
+
602
+ div.dispatchEvent(new Event("scroll"));
603
+ const button = el.querySelector<HTMLButtonElement>("button.jump")!;
604
+ expect(button.disabled).toBe(true);
605
+
606
+ scrollTop = 50;
607
+ div.dispatchEvent(new Event("scroll"));
608
+ await vi.waitFor(async () => {
609
+ await nextTick();
610
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
611
+ false,
612
+ );
613
+ });
614
+
615
+ el.querySelector<HTMLButtonElement>("button.jump")!.click();
616
+ expect(scrollTo).toHaveBeenCalledWith({ top: 500, behavior: "auto" });
617
+ div.dispatchEvent(new Event("scroll"));
618
+ await vi.waitFor(async () => {
619
+ await nextTick();
620
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
621
+ true,
622
+ );
623
+ });
624
+
625
+ unmount();
626
+ });
627
+
628
+ it("updates at-bottom state when a footer height changes", async () => {
629
+ const observers = new Set<ResizeObserverMock>();
630
+ class ResizeObserverMock {
631
+ element: Element | null = null;
632
+ readonly callback: ResizeObserverCallback;
633
+
634
+ constructor(callback: ResizeObserverCallback) {
635
+ this.callback = callback;
636
+ observers.add(this);
637
+ }
638
+
639
+ observe(element: Element) {
640
+ this.element = element;
641
+ }
642
+
643
+ unobserve() {}
644
+
645
+ disconnect() {
646
+ observers.delete(this);
647
+ }
648
+
649
+ takeRecords() {
650
+ return [];
651
+ }
652
+
653
+ trigger() {
654
+ this.callback([], this);
655
+ }
656
+ }
657
+ vi.stubGlobal("ResizeObserver", ResizeObserverMock);
658
+
659
+ const { runtime } = createTestRuntime();
660
+ const View = defineComponent({
661
+ setup: () => () =>
662
+ h(
663
+ ThreadPrimitiveViewport,
664
+ { class: "viewport" },
665
+ {
666
+ default: () => [
667
+ h(ThreadPrimitiveViewportFooter, { class: "footer" }),
668
+ h(
669
+ ThreadPrimitiveScrollToBottom,
670
+ { class: "jump" },
671
+ { default: () => "Jump" },
672
+ ),
673
+ ],
674
+ },
675
+ ),
676
+ });
677
+ const { el, unmount } = mountChat(runtime, View);
678
+ const div = el.querySelector<HTMLElement>("div.viewport")!;
679
+ const footer = el.querySelector<HTMLElement>("div.footer")!;
680
+ let scrollTop = 350;
681
+ let footerHeight = 0;
682
+ Object.defineProperty(div, "scrollHeight", {
683
+ get: () => 500,
684
+ configurable: true,
685
+ });
686
+ Object.defineProperty(div, "clientHeight", {
687
+ get: () => 100,
688
+ configurable: true,
689
+ });
690
+ Object.defineProperty(div, "scrollTop", {
691
+ get: () => scrollTop,
692
+ set: (value: number) => {
693
+ scrollTop = value;
694
+ },
695
+ configurable: true,
696
+ });
697
+ Object.defineProperty(footer, "offsetHeight", {
698
+ get: () => footerHeight,
699
+ configurable: true,
700
+ });
701
+
702
+ try {
703
+ await nextTick();
704
+ expect(
705
+ [...observers].some((observer) => observer.element === footer),
706
+ ).toBe(true);
707
+ scrollTop = 0;
708
+ div.dispatchEvent(new Event("scroll"));
709
+ scrollTop = 350;
710
+ div.dispatchEvent(new Event("scroll"));
711
+ await nextTick();
712
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
713
+ false,
714
+ );
715
+
716
+ footerHeight = 50;
717
+ expect(footer.offsetHeight).toBe(50);
718
+ for (const observer of observers) {
719
+ if (observer.element === footer) observer.trigger();
720
+ }
721
+ await nextTick();
722
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
723
+ true,
724
+ );
725
+
726
+ footerHeight = 0;
727
+ for (const observer of observers) {
728
+ if (observer.element === footer) observer.trigger();
729
+ }
730
+ await nextTick();
731
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
732
+ false,
733
+ );
734
+ } finally {
735
+ Reflect.deleteProperty(div, "scrollHeight");
736
+ Reflect.deleteProperty(div, "clientHeight");
737
+ Reflect.deleteProperty(div, "scrollTop");
738
+ Reflect.deleteProperty(footer, "offsetHeight");
739
+ unmount();
740
+ vi.unstubAllGlobals();
741
+ }
742
+ });
743
+
744
+ it("keeps MutationObserver content observation when ResizeObserver is unavailable", async () => {
745
+ vi.stubGlobal("ResizeObserver", undefined);
746
+ const { runtime } = createTestRuntime();
747
+ const View = defineComponent({
748
+ setup: () => () =>
749
+ h(
750
+ ThreadPrimitiveViewport,
751
+ { class: "viewport" },
752
+ {
753
+ default: () => [
754
+ h(ThreadPrimitiveViewportFooter, { class: "footer" }),
755
+ h("p", "content"),
756
+ ],
757
+ },
758
+ ),
759
+ });
760
+ const { el, unmount } = mountChat(runtime, View);
761
+ const div = el.querySelector<HTMLElement>("div.viewport")!;
762
+ let scrollHeight = 500;
763
+ let scrollTop = 400;
764
+ Object.defineProperty(div, "scrollHeight", {
765
+ get: () => scrollHeight,
766
+ configurable: true,
767
+ });
768
+ Object.defineProperty(div, "clientHeight", {
769
+ get: () => 100,
770
+ configurable: true,
771
+ });
772
+ Object.defineProperty(div, "scrollTop", {
773
+ get: () => scrollTop,
774
+ set: (value: number) => {
775
+ scrollTop = value;
776
+ },
777
+ configurable: true,
778
+ });
779
+ const scrollTo = vi.fn(({ top }: ScrollToOptions) => {
780
+ scrollTop = Math.max(0, Math.min(top ?? 0, scrollHeight - 100));
781
+ div.dispatchEvent(new Event("scroll"));
782
+ });
783
+ Object.defineProperty(div, "scrollTo", {
784
+ value: scrollTo,
785
+ configurable: true,
786
+ });
787
+
788
+ try {
789
+ await nextTick();
790
+ expect(el.querySelector("div.footer")).not.toBeNull();
791
+ div.dispatchEvent(new Event("scroll"));
792
+ scrollHeight = 600;
793
+ div.append(document.createElement("span"));
794
+ await vi.waitFor(() => {
795
+ expect(scrollTo).toHaveBeenCalledWith({
796
+ top: 600,
797
+ behavior: "instant",
798
+ });
799
+ });
800
+ } finally {
801
+ Reflect.deleteProperty(div, "scrollHeight");
802
+ Reflect.deleteProperty(div, "clientHeight");
803
+ Reflect.deleteProperty(div, "scrollTop");
804
+ Reflect.deleteProperty(div, "scrollTo");
805
+ unmount();
806
+ vi.unstubAllGlobals();
807
+ }
808
+ });
809
+
810
+ it("renders scroll-to-bottom disabled outside a viewport and warns in dev", () => {
811
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
812
+ const { runtime } = createTestRuntime();
813
+ const View = defineComponent({
814
+ setup: () => () =>
815
+ h(
816
+ ThreadPrimitiveScrollToBottom,
817
+ { class: "jump" },
818
+ { default: () => "Jump" },
819
+ ),
820
+ });
821
+ const { el, unmount } = mountChat(runtime, View);
822
+ expect(el.querySelector<HTMLButtonElement>("button.jump")!.disabled).toBe(
823
+ true,
824
+ );
825
+ expect(warn).toHaveBeenCalledTimes(1);
826
+ expect(warn.mock.calls[0]![0]).toContain("no surrounding");
827
+ unmount();
828
+ });
829
+ });