@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,579 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ afterEach(() => {
4
+ vi.restoreAllMocks();
5
+ clearPartWarningsForTesting();
6
+ });
7
+ import { createApp, defineComponent, h, nextTick, type Component } from "vue";
8
+ import { flushTapSync } from "@assistant-ui/tap";
9
+ import { AuiConfig } from "@assistant-ui/store/client";
10
+ import { RuntimeAdapter } from "@assistant-ui/core/store";
11
+ import { resource } from "@assistant-ui/tap";
12
+ import { Tools, type Toolkit } from "@assistant-ui/core/react";
13
+ import type {
14
+ ExternalStoreAdapter,
15
+ ThreadMessageLike,
16
+ } from "@assistant-ui/core";
17
+ import {
18
+ AssistantRuntimeImpl,
19
+ ExternalStoreRuntimeCore,
20
+ } from "@assistant-ui/core/internal";
21
+ import { AuiProvider } from "../AuiProvider";
22
+ import { useAui } from "../useAui";
23
+ import { ThreadPrimitiveMessages } from "../primitives/ThreadPrimitiveMessages";
24
+ import {
25
+ MessagePrimitiveParts,
26
+ clearPartWarningsForTesting,
27
+ type ToolUIProps,
28
+ } from "../primitives/MessagePrimitiveParts";
29
+
30
+ type DemoMessage = {
31
+ role: "user" | "assistant";
32
+ content: ThreadMessageLike["content"];
33
+ status?: { type: "running" } | undefined;
34
+ };
35
+
36
+ const createTestRuntime = () => {
37
+ let messages: DemoMessage[] = [];
38
+ const onAddToolResult = vi.fn();
39
+ const onResumeToolCall = vi.fn();
40
+ const onRespondToToolApproval = vi.fn();
41
+ const makeAdapter = (): ExternalStoreAdapter<DemoMessage> => ({
42
+ messages,
43
+ isRunning: false,
44
+ convertMessage: (message) => ({
45
+ role: message.role,
46
+ content: message.content,
47
+ ...(message.status && { status: message.status }),
48
+ }),
49
+ onNew: async () => {},
50
+ onAddToolResult,
51
+ onResumeToolCall,
52
+ onRespondToToolApproval,
53
+ });
54
+ const core = new ExternalStoreRuntimeCore(makeAdapter());
55
+ const runtime = new AssistantRuntimeImpl(core);
56
+ const append = (message: DemoMessage) => {
57
+ messages = [...messages, message];
58
+ core.setAdapter(makeAdapter());
59
+ };
60
+ const replace = (message: DemoMessage) => {
61
+ messages = [message];
62
+ core.setAdapter(makeAdapter());
63
+ };
64
+ return {
65
+ runtime,
66
+ append,
67
+ replace,
68
+ onAddToolResult,
69
+ onResumeToolCall,
70
+ onRespondToToolApproval,
71
+ };
72
+ };
73
+
74
+ const mountChat = (
75
+ runtime: AssistantRuntimeImpl,
76
+ view: Component,
77
+ config: Parameters<typeof AuiConfig>[0] = {},
78
+ ) => {
79
+ let client: any;
80
+ const CaptureClient = defineComponent({
81
+ setup() {
82
+ client = useAui();
83
+ return () => null;
84
+ },
85
+ });
86
+ const app = createApp(
87
+ defineComponent({
88
+ setup: () => () =>
89
+ h(
90
+ AuiProvider,
91
+ {
92
+ config: AuiConfig({ threads: RuntimeAdapter(runtime), ...config }),
93
+ },
94
+ { default: () => [h(CaptureClient), h(view)] },
95
+ ),
96
+ }),
97
+ );
98
+ const el = document.createElement("div");
99
+ app.mount(el);
100
+ return { el, client: () => client, unmount: () => app.unmount() };
101
+ };
102
+
103
+ const toolCallMessage = (toolName: string): DemoMessage => ({
104
+ role: "assistant",
105
+ content: [
106
+ {
107
+ type: "tool-call",
108
+ toolCallId: "call-1",
109
+ toolName,
110
+ args: { city: "sf" },
111
+ },
112
+ ],
113
+ });
114
+
115
+ const PartsWithToolSlot = defineComponent({
116
+ setup: () => () =>
117
+ h("li", null, [
118
+ h(ThreadPrimitiveMessages, null, {
119
+ default: () =>
120
+ h(MessagePrimitiveParts, null, {
121
+ "tool-call": () => h("span", { class: "slot" }, "[slot]"),
122
+ }),
123
+ }),
124
+ ]),
125
+ });
126
+
127
+ describe("MessagePrimitiveParts tool UI registry", () => {
128
+ it("renders a registered tool UI over the tool-call slot with the part prop and callbacks", async () => {
129
+ const { runtime, append } = createTestRuntime();
130
+ const { el, client, unmount } = mountChat(runtime, PartsWithToolSlot);
131
+
132
+ const WeatherTool = defineComponent({
133
+ props: ["tool"],
134
+ setup: (props: { tool: ToolUIProps }) => () =>
135
+ h(
136
+ "span",
137
+ { class: "ui" },
138
+ [
139
+ props.tool.part.toolName,
140
+ (props.tool.part.args as { city?: string }).city,
141
+ typeof props.tool.addResult,
142
+ typeof props.tool.resume,
143
+ typeof props.tool.respondToApproval,
144
+ ].join(","),
145
+ ),
146
+ });
147
+
148
+ flushTapSync(() => client().tools.setToolUI("weather", WeatherTool));
149
+ flushTapSync(() => append(toolCallMessage("weather")));
150
+
151
+ await vi.waitFor(async () => {
152
+ await nextTick();
153
+ expect(el.querySelector("span.ui")).not.toBeNull();
154
+ });
155
+ expect(el.querySelector("span.ui")!.textContent).toBe(
156
+ "weather,sf,function,function,function",
157
+ );
158
+ expect(el.querySelector("span.slot")).toBeNull();
159
+
160
+ unmount();
161
+ });
162
+
163
+ it("routes addResult, resume, and respondToApproval from the registered tool UI to the adapter callbacks", async () => {
164
+ const {
165
+ runtime,
166
+ append,
167
+ onAddToolResult,
168
+ onResumeToolCall,
169
+ onRespondToToolApproval,
170
+ } = createTestRuntime();
171
+ const { el, client, unmount } = mountChat(runtime, PartsWithToolSlot);
172
+
173
+ const CallbackTool = defineComponent({
174
+ props: ["tool"],
175
+ setup: (props: { tool: ToolUIProps }) => () => [
176
+ h(
177
+ "button",
178
+ { class: "add-result", onClick: () => props.tool.addResult("72F") },
179
+ "add",
180
+ ),
181
+ h(
182
+ "button",
183
+ { class: "resume", onClick: () => props.tool.resume("continue") },
184
+ "resume",
185
+ ),
186
+ h(
187
+ "button",
188
+ {
189
+ class: "approve",
190
+ onClick: () => props.tool.respondToApproval({ approved: true }),
191
+ },
192
+ "approve",
193
+ ),
194
+ ],
195
+ });
196
+
197
+ flushTapSync(() => client().tools.setToolUI("weather", CallbackTool));
198
+ flushTapSync(() =>
199
+ append({
200
+ role: "assistant",
201
+ content: [
202
+ {
203
+ type: "tool-call",
204
+ toolCallId: "call-1",
205
+ toolName: "weather",
206
+ args: { city: "sf" },
207
+ approval: { id: "approval-1" },
208
+ },
209
+ ],
210
+ }),
211
+ );
212
+
213
+ await vi.waitFor(async () => {
214
+ await nextTick();
215
+ expect(el.querySelector("button.add-result")).not.toBeNull();
216
+ });
217
+
218
+ (el.querySelector("button.add-result") as HTMLButtonElement).click();
219
+ await vi.waitFor(() => {
220
+ expect(onAddToolResult).toHaveBeenCalledTimes(1);
221
+ });
222
+ expect(onAddToolResult.mock.calls[0]![0]).toMatchObject({
223
+ toolCallId: "call-1",
224
+ toolName: "weather",
225
+ result: "72F",
226
+ });
227
+
228
+ (el.querySelector("button.resume") as HTMLButtonElement).click();
229
+ await vi.waitFor(() => {
230
+ expect(onResumeToolCall).toHaveBeenCalledTimes(1);
231
+ });
232
+ expect(onResumeToolCall.mock.calls[0]![0]).toMatchObject({
233
+ toolCallId: "call-1",
234
+ payload: "continue",
235
+ });
236
+
237
+ (el.querySelector("button.approve") as HTMLButtonElement).click();
238
+ await vi.waitFor(() => {
239
+ expect(onRespondToToolApproval).toHaveBeenCalledTimes(1);
240
+ });
241
+ expect(onRespondToToolApproval.mock.calls[0]![0]).toMatchObject({
242
+ approvalId: "approval-1",
243
+ approved: true,
244
+ });
245
+
246
+ unmount();
247
+ });
248
+
249
+ it("falls back to the tool-call slot without a matching registration and swaps live on register/unregister", async () => {
250
+ const { runtime, append } = createTestRuntime();
251
+ const { el, client, unmount } = mountChat(runtime, PartsWithToolSlot);
252
+
253
+ flushTapSync(() => append(toolCallMessage("weather")));
254
+ await vi.waitFor(async () => {
255
+ await nextTick();
256
+ expect(el.querySelector("span.slot")).not.toBeNull();
257
+ });
258
+
259
+ const Ui = defineComponent({
260
+ setup: () => () => h("span", { class: "ui" }, "ui"),
261
+ });
262
+ let dispose: () => void;
263
+ flushTapSync(() => {
264
+ dispose = client().tools.setToolUI("weather", Ui);
265
+ });
266
+ await vi.waitFor(async () => {
267
+ await nextTick();
268
+ expect(el.querySelector("span.ui")).not.toBeNull();
269
+ expect(el.querySelector("span.slot")).toBeNull();
270
+ });
271
+
272
+ flushTapSync(() => dispose());
273
+ await vi.waitFor(async () => {
274
+ await nextTick();
275
+ expect(el.querySelector("span.slot")).not.toBeNull();
276
+ expect(el.querySelector("span.ui")).toBeNull();
277
+ });
278
+
279
+ unmount();
280
+ });
281
+
282
+ it("renders tools.mcpApp for a tool call with a ui:// resource", async () => {
283
+ const { runtime, append } = createTestRuntime();
284
+ const Mcp = defineComponent({
285
+ props: ["tool"],
286
+ setup: (props: { tool: ToolUIProps }) => () =>
287
+ h("span", { class: "mcp" }, props.tool.part.toolName),
288
+ });
289
+ const McpApp = resource(function McpApp() {
290
+ return { render: Mcp as never };
291
+ });
292
+ const { el, unmount } = mountChat(runtime, PartsWithToolSlot, {
293
+ tools: Tools({ mcpApp: McpApp() }),
294
+ });
295
+
296
+ flushTapSync(() =>
297
+ append({
298
+ role: "assistant",
299
+ content: [
300
+ {
301
+ type: "tool-call",
302
+ toolCallId: "call-1",
303
+ toolName: "show_chart",
304
+ args: {},
305
+ mcp: { app: { resourceUri: "ui://chart" } },
306
+ },
307
+ ],
308
+ }),
309
+ );
310
+
311
+ await vi.waitFor(async () => {
312
+ await nextTick();
313
+ expect(el.querySelector("span.mcp")?.textContent).toBe("show_chart");
314
+ });
315
+ expect(el.querySelector("span.mcp")?.getAttribute("tool")).toBeNull();
316
+ expect(el.querySelector("span.mcp")?.getAttribute("name")).toBeNull();
317
+ expect(el.querySelector("span.slot")).toBeNull();
318
+
319
+ unmount();
320
+ });
321
+
322
+ it("renders a registered data renderer for a data part", async () => {
323
+ const { runtime, append } = createTestRuntime();
324
+ const PartsWithDataSlot = defineComponent({
325
+ setup: () => () =>
326
+ h("li", null, [
327
+ h(ThreadPrimitiveMessages, null, {
328
+ default: () =>
329
+ h(MessagePrimitiveParts, null, {
330
+ data: () => h("span", { class: "slot" }, "[slot]"),
331
+ }),
332
+ }),
333
+ ]),
334
+ });
335
+ const { el, client, unmount } = mountChat(runtime, PartsWithDataSlot);
336
+ const Chart = defineComponent({
337
+ props: ["data"],
338
+ setup: (props: { data: { part: { data: { a: number } } } }) => () =>
339
+ h("span", { class: "data" }, String(props.data.part.data.a)),
340
+ });
341
+ flushTapSync(() =>
342
+ client().dataRenderers.setDataUI("chart", Chart as never),
343
+ );
344
+ flushTapSync(() =>
345
+ append({
346
+ role: "assistant",
347
+ content: [{ type: "data", name: "chart", data: { a: 1 } }],
348
+ }),
349
+ );
350
+
351
+ await vi.waitFor(async () => {
352
+ await nextTick();
353
+ expect(el.querySelector("span.data")?.textContent).toBe("1");
354
+ });
355
+ expect(el.querySelector("span.data")?.getAttribute("type")).toBeNull();
356
+ expect(el.querySelector("span.data")?.getAttribute("name")).toBeNull();
357
+ expect(el.querySelector("span.data")?.getAttribute("data")).toBeNull();
358
+ expect(el.querySelector("span.slot")).toBeNull();
359
+
360
+ unmount();
361
+ });
362
+
363
+ it("uses the fallback data renderer and keeps its payload isolated", async () => {
364
+ const { runtime, append } = createTestRuntime();
365
+ const PartsWithDataSlot = defineComponent({
366
+ setup: () => () =>
367
+ h("li", null, [
368
+ h(ThreadPrimitiveMessages, null, {
369
+ default: () => h(MessagePrimitiveParts),
370
+ }),
371
+ ]),
372
+ });
373
+ const { el, client, unmount } = mountChat(runtime, PartsWithDataSlot);
374
+ const Fallback = defineComponent({
375
+ props: ["data"],
376
+ setup: (props: { data: { part: { data: { value: string } } } }) => () =>
377
+ h("span", { class: "fallback" }, props.data.part.data.value),
378
+ });
379
+ flushTapSync(() =>
380
+ client().dataRenderers.setFallbackDataUI(Fallback as never),
381
+ );
382
+ flushTapSync(() =>
383
+ append({
384
+ role: "assistant",
385
+ content: [
386
+ { type: "data", name: "unknown", data: { value: "fallback" } },
387
+ ],
388
+ }),
389
+ );
390
+
391
+ await vi.waitFor(async () => {
392
+ await nextTick();
393
+ expect(el.querySelector("span.fallback")?.textContent).toBe("fallback");
394
+ });
395
+ expect(el.querySelector("span.fallback")?.getAttribute("name")).toBeNull();
396
+ expect(el.querySelector("span.fallback")?.getAttribute("data")).toBeNull();
397
+
398
+ unmount();
399
+ });
400
+
401
+ it("uses the data slot when no renderer is registered", async () => {
402
+ const { runtime, append } = createTestRuntime();
403
+ const PartsWithDataSlot = defineComponent({
404
+ setup: () => () =>
405
+ h("li", null, [
406
+ h(ThreadPrimitiveMessages, null, {
407
+ default: () =>
408
+ h(MessagePrimitiveParts, null, {
409
+ data: () => h("span", { class: "slot" }, "[slot]"),
410
+ }),
411
+ }),
412
+ ]),
413
+ });
414
+ const { el, unmount } = mountChat(runtime, PartsWithDataSlot);
415
+ flushTapSync(() =>
416
+ append({
417
+ role: "assistant",
418
+ content: [{ type: "data", name: "unknown", data: { value: "slot" } }],
419
+ }),
420
+ );
421
+
422
+ await vi.waitFor(async () => {
423
+ await nextTick();
424
+ expect(el.querySelector("span.slot")?.textContent).toBe("[slot]");
425
+ });
426
+
427
+ unmount();
428
+ });
429
+
430
+ it("does not route registrations for other tool names", async () => {
431
+ const { runtime, append } = createTestRuntime();
432
+ const { el, client, unmount } = mountChat(runtime, PartsWithToolSlot);
433
+
434
+ const Ui = defineComponent({
435
+ setup: () => () => h("span", { class: "ui" }, "ui"),
436
+ });
437
+ flushTapSync(() => client().tools.setToolUI("search", Ui));
438
+ flushTapSync(() => append(toolCallMessage("weather")));
439
+
440
+ await vi.waitFor(async () => {
441
+ await nextTick();
442
+ expect(el.querySelector("span.slot")).not.toBeNull();
443
+ });
444
+ expect(el.querySelector("span.ui")).toBeNull();
445
+
446
+ unmount();
447
+ });
448
+
449
+ it("uses the first registration when a tool name is registered twice", async () => {
450
+ const { runtime, append } = createTestRuntime();
451
+ const { el, client, unmount } = mountChat(runtime, PartsWithToolSlot);
452
+
453
+ const First = defineComponent({
454
+ setup: () => () => h("span", { class: "first" }, "first"),
455
+ });
456
+ const Second = defineComponent({
457
+ setup: () => () => h("span", { class: "second" }, "second"),
458
+ });
459
+ flushTapSync(() => client().tools.setToolUI("weather", First));
460
+ flushTapSync(() => client().tools.setToolUI("weather", Second));
461
+ flushTapSync(() => append(toolCallMessage("weather")));
462
+
463
+ await vi.waitFor(async () => {
464
+ await nextTick();
465
+ expect(el.querySelector("span.first")).not.toBeNull();
466
+ });
467
+ expect(el.querySelector("span.second")).toBeNull();
468
+
469
+ unmount();
470
+ });
471
+
472
+ it("renders nothing for a react-element-valued renderText descriptor", async () => {
473
+ const { runtime, replace } = createTestRuntime();
474
+ const toolkit = {
475
+ weather: {
476
+ type: "frontend",
477
+ description: "Looks up the weather.",
478
+ parameters: { type: "object", properties: {} },
479
+ execute: async () => "sunny",
480
+ renderText: {
481
+ running: () => ({ reactElement: true }),
482
+ complete: () => ({ reactElement: true }),
483
+ },
484
+ },
485
+ } as unknown as Toolkit;
486
+ const { el, unmount } = mountChat(runtime, PartsWithToolSlot, {
487
+ tools: Tools({ toolkit }),
488
+ });
489
+
490
+ flushTapSync(() =>
491
+ replace({
492
+ role: "assistant",
493
+ status: { type: "running" },
494
+ content: [
495
+ {
496
+ type: "tool-call",
497
+ toolCallId: "call-1",
498
+ toolName: "weather",
499
+ args: { city: "sf" },
500
+ },
501
+ ],
502
+ }),
503
+ );
504
+ await nextTick();
505
+ await nextTick();
506
+ expect(el.textContent ?? "").not.toContain("reactElement");
507
+ expect(el.textContent ?? "").not.toContain("[object");
508
+
509
+ unmount();
510
+ });
511
+
512
+ it("renders toolkit renderText for running and complete tool calls", async () => {
513
+ const { runtime, replace } = createTestRuntime();
514
+ const toolkit = {
515
+ weather: {
516
+ type: "frontend",
517
+ description: "Looks up the weather.",
518
+ parameters: { type: "object", properties: {} },
519
+ execute: async () => "sunny",
520
+ renderText: {
521
+ running: ({ args }: { args: { city: string } }) =>
522
+ `Checking ${args.city}...`,
523
+ complete: ({
524
+ args,
525
+ result,
526
+ }: {
527
+ args: { city: string };
528
+ result: string | undefined;
529
+ }) => `${result} in ${args.city}`,
530
+ },
531
+ },
532
+ } as unknown as Toolkit;
533
+ const { el, unmount } = mountChat(runtime, PartsWithToolSlot, {
534
+ tools: Tools({ toolkit }),
535
+ });
536
+
537
+ flushTapSync(() =>
538
+ replace({
539
+ role: "assistant",
540
+ status: { type: "running" },
541
+ content: [
542
+ {
543
+ type: "tool-call",
544
+ toolCallId: "call-1",
545
+ toolName: "weather",
546
+ args: { city: "sf" },
547
+ },
548
+ ],
549
+ }),
550
+ );
551
+ await vi.waitFor(async () => {
552
+ await nextTick();
553
+ expect(el.textContent).toBe("Checking sf...");
554
+ });
555
+
556
+ flushTapSync(() =>
557
+ replace({
558
+ role: "assistant",
559
+ status: { type: "running" },
560
+ content: [
561
+ {
562
+ type: "tool-call",
563
+ toolCallId: "call-1",
564
+ toolName: "weather",
565
+ args: { city: "sf" },
566
+ result: "sunny",
567
+ },
568
+ ],
569
+ }),
570
+ );
571
+ await vi.waitFor(async () => {
572
+ await nextTick();
573
+ expect(el.textContent).toBe("sunny in sf");
574
+ });
575
+ expect(el.querySelector("span.slot")).toBeNull();
576
+
577
+ unmount();
578
+ });
579
+ });