@assistant-ui/vue 0.0.0 → 0.0.1

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