@assistant-ui/react 0.15.17 → 0.15.19

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 (169) hide show
  1. package/README.md +8 -2
  2. package/dist/context/providers/ThreadViewportProvider.js +2 -2
  3. package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
  4. package/dist/hooks/useToolCallElapsed.js.map +1 -1
  5. package/dist/index.d.ts +7 -4
  6. package/dist/index.js +5 -3
  7. package/dist/mcp-apps/McpAppRenderer.d.ts +30 -8
  8. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  9. package/dist/mcp-apps/McpAppRenderer.js +91 -47
  10. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  11. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  12. package/dist/mcp-apps/app-frame.js +57 -33
  13. package/dist/mcp-apps/app-frame.js.map +1 -1
  14. package/dist/mcp-apps/bridge.js +9 -4
  15. package/dist/mcp-apps/bridge.js.map +1 -1
  16. package/dist/mcp-apps/index.d.ts +2 -2
  17. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
  18. package/dist/primitives/assistantModal/AssistantModalRoot.js +47 -79
  19. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  20. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  21. package/dist/primitives/composer/ComposerAttachmentDropzone.js +61 -138
  22. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  23. package/dist/primitives/composer/ComposerInput.js +3 -8
  24. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  25. package/dist/primitives/composer/ComposerInputPluginContext.d.ts +1 -2
  26. package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -1
  27. package/dist/primitives/composer/ComposerInputPluginContext.js +1 -6
  28. package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
  29. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +46 -15
  30. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  31. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +40 -10
  32. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  33. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts +6 -0
  34. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts.map +1 -0
  35. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +9 -0
  36. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js.map +1 -0
  37. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  38. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +53 -61
  39. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  40. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  41. package/dist/primitives/composer/trigger/triggerNavigationResource.js +129 -141
  42. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  43. package/dist/primitives/message/MessageParts.js +2 -14
  44. package/dist/primitives/message/MessageParts.js.map +1 -1
  45. package/dist/primitives/message/MessagePartsGrouped.d.ts +3 -3
  46. package/dist/primitives/message/MessagePartsGrouped.js +2 -2
  47. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  48. package/dist/primitives/messagePart/MessagePartText.d.ts +2 -0
  49. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  50. package/dist/primitives/messagePart/MessagePartText.js +13 -36
  51. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  52. package/dist/primitives/reasoning/useScrollLock.js +3 -1
  53. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  54. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  55. package/dist/primitives/thread/useThreadViewportAutoScroll.js +3 -3
  56. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  57. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  58. package/dist/sandbox-host/SandboxHost.js +36 -9
  59. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  60. package/dist/unstable/useLiveCompletionAdapter.js +24 -2
  61. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  62. package/dist/unstable/useMentionAdapter.d.ts +9 -3
  63. package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
  64. package/dist/unstable/useMentionAdapter.js +50 -43
  65. package/dist/unstable/useMentionAdapter.js.map +1 -1
  66. package/dist/unstable/useMessageStallDetection.d.ts +4 -5
  67. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
  68. package/dist/unstable/useMessageStallDetection.js +16 -15
  69. package/dist/unstable/useMessageStallDetection.js.map +1 -1
  70. package/dist/unstable/useModelContextSnapshot.d.ts +34 -0
  71. package/dist/unstable/useModelContextSnapshot.d.ts.map +1 -0
  72. package/dist/unstable/useModelContextSnapshot.js +61 -0
  73. package/dist/unstable/useModelContextSnapshot.js.map +1 -0
  74. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
  75. package/dist/unstable/useSlashCommandAdapter.js +2 -8
  76. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  77. package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +14 -0
  78. package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -0
  79. package/dist/unstable/webmcp/WebMcpRegistrationResource.js +85 -0
  80. package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -0
  81. package/dist/unstable/webmcp/convertTools.d.ts +21 -0
  82. package/dist/unstable/webmcp/convertTools.d.ts.map +1 -0
  83. package/dist/unstable/webmcp/convertTools.js +161 -0
  84. package/dist/unstable/webmcp/convertTools.js.map +1 -0
  85. package/dist/unstable/webmcp/useWebMcpProvider.d.ts +21 -0
  86. package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -0
  87. package/dist/unstable/webmcp/useWebMcpProvider.js +106 -0
  88. package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -0
  89. package/dist/unstable/webmcp/webmcp-host.d.ts +39 -0
  90. package/dist/unstable/webmcp/webmcp-host.d.ts.map +1 -0
  91. package/dist/unstable/webmcp/webmcp-host.js +29 -0
  92. package/dist/unstable/webmcp/webmcp-host.js.map +1 -0
  93. package/dist/utils/Primitive.d.ts +10 -2
  94. package/dist/utils/Primitive.d.ts.map +1 -1
  95. package/dist/utils/Primitive.js +25 -6
  96. package/dist/utils/Primitive.js.map +1 -1
  97. package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
  98. package/dist/utils/getSelectionMessageId.js +17 -3
  99. package/dist/utils/getSelectionMessageId.js.map +1 -1
  100. package/package.json +13 -13
  101. package/src/context/providers/ThreadViewportProvider.tsx +2 -2
  102. package/src/hooks/useToolCallElapsed.ts +3 -0
  103. package/src/index.ts +14 -1
  104. package/src/mcp-apps/McpAppRenderer.test.tsx +253 -5
  105. package/src/mcp-apps/McpAppRenderer.tsx +147 -52
  106. package/src/mcp-apps/app-frame.test.tsx +437 -1
  107. package/src/mcp-apps/app-frame.tsx +76 -21
  108. package/src/mcp-apps/bridge.test.ts +98 -0
  109. package/src/mcp-apps/bridge.ts +14 -3
  110. package/src/mcp-apps/index.ts +5 -1
  111. package/src/primitives/assistantModal/AssistantModalRoot.test.tsx +123 -0
  112. package/src/primitives/assistantModal/AssistantModalRoot.tsx +11 -31
  113. package/src/primitives/assistantModal/AssistantModalTrigger.test.tsx +1 -16
  114. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +43 -0
  115. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +2 -10
  116. package/src/primitives/composer/ComposerInput.test.tsx +22 -0
  117. package/src/primitives/composer/ComposerInput.tsx +5 -9
  118. package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
  119. package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
  120. package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
  121. package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
  122. package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
  123. package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
  124. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +13 -7
  125. package/src/primitives/composer/trigger/triggerNavigationResource.ts +8 -13
  126. package/src/primitives/message/MessageParts.tsx +1 -21
  127. package/src/primitives/message/MessagePartsGrouped.test.tsx +93 -0
  128. package/src/primitives/message/MessagePartsGrouped.tsx +6 -6
  129. package/src/primitives/messagePart/MessagePartText.test.tsx +69 -0
  130. package/src/primitives/messagePart/MessagePartText.tsx +22 -9
  131. package/src/primitives/reasoning/useScrollLock.test.tsx +129 -0
  132. package/src/primitives/reasoning/useScrollLock.ts +15 -1
  133. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +38 -1
  134. package/src/primitives/thread/useThreadViewportAutoScroll.ts +7 -6
  135. package/src/sandbox-host/SandboxHost.test.tsx +160 -2
  136. package/src/sandbox-host/SandboxHost.tsx +50 -10
  137. package/src/tests/MessageParts.rendererOptions.test.tsx +117 -0
  138. package/src/tests/toolCallTiming.test.tsx +55 -1
  139. package/src/unstable/useComposerInputHistory.test.tsx +2 -1
  140. package/src/unstable/useLiveCompletionAdapter.test.tsx +202 -1
  141. package/src/unstable/useLiveCompletionAdapter.ts +38 -8
  142. package/src/unstable/useMentionAdapter.test.tsx +383 -0
  143. package/src/unstable/useMentionAdapter.ts +94 -57
  144. package/src/unstable/useMessageStallDetection.ts +26 -19
  145. package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
  146. package/src/unstable/useModelContextSnapshot.ts +67 -0
  147. package/src/unstable/useSlashCommandAdapter.ts +2 -9
  148. package/src/unstable/webmcp/WebMcpRegistrationResource.ts +92 -0
  149. package/src/unstable/webmcp/__tests__/webmcp.fake.tsx +168 -0
  150. package/src/unstable/webmcp/convertTools.test.ts +553 -0
  151. package/src/unstable/webmcp/convertTools.ts +260 -0
  152. package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
  153. package/src/unstable/webmcp/useWebMcpProvider.ts +148 -0
  154. package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
  155. package/src/unstable/webmcp/webmcp-host.ts +83 -0
  156. package/src/utils/Primitive.test.tsx +46 -1
  157. package/src/utils/Primitive.tsx +37 -7
  158. package/src/utils/getSelectionMessageId.test.ts +176 -1
  159. package/src/utils/getSelectionMessageId.ts +21 -3
  160. package/dist/utils/invokeCallbackSafely.d.ts +0 -5
  161. package/dist/utils/invokeCallbackSafely.d.ts.map +0 -1
  162. package/dist/utils/invokeCallbackSafely.js +0 -15
  163. package/dist/utils/invokeCallbackSafely.js.map +0 -1
  164. package/dist/utils/useToolArgsFieldStatus.d.ts +0 -19
  165. package/dist/utils/useToolArgsFieldStatus.d.ts.map +0 -1
  166. package/dist/utils/useToolArgsFieldStatus.js +0 -23
  167. package/dist/utils/useToolArgsFieldStatus.js.map +0 -1
  168. package/src/utils/invokeCallbackSafely.ts +0 -11
  169. package/src/utils/useToolArgsFieldStatus.ts +0 -18
@@ -0,0 +1,260 @@
1
+ import {
2
+ ToolResponse,
3
+ toJSONSchema,
4
+ type Tool,
5
+ type ToolModelContentPart,
6
+ } from "assistant-stream";
7
+ import { generateId } from "@assistant-ui/core/internal";
8
+ import type {
9
+ WebMcpCallToolResult,
10
+ WebMcpContent,
11
+ WebMcpToolDescriptor,
12
+ } from "./webmcp-host";
13
+
14
+ /**
15
+ * The predicate the WebMCP provider uses when no `filter` is passed: an enabled
16
+ * frontend tool with a client-side `execute`. A tool authored without a `type`
17
+ * is included, because `execute` is what distinguishes the deprecated
18
+ * type-less form from a backend or human tool. A `filter` replaces this, so
19
+ * pass it through to narrow the default set rather than widen it.
20
+ */
21
+ export const defaultWebMcpFilter = (
22
+ _name: string,
23
+ tool: Tool<any, any>,
24
+ ): boolean =>
25
+ (tool.type === "frontend" || tool.type === undefined) &&
26
+ !!tool.execute &&
27
+ !tool.disabled;
28
+
29
+ export const toWebMcpInputSchema = (tool: Tool<any, any>): unknown =>
30
+ tool.parameters
31
+ ? toJSONSchema(tool.parameters)
32
+ : { type: "object", properties: {} };
33
+
34
+ const textContent = (text: string): WebMcpContent => ({ type: "text", text });
35
+
36
+ const errorResult = (message: string): WebMcpCallToolResult => ({
37
+ isError: true,
38
+ content: [textContent(message)],
39
+ });
40
+
41
+ // bigint throws in JSON.stringify and symbol serializes to undefined, but both
42
+ // have a faithful string form. A value that cannot be serialized at all still
43
+ // throws through to the error result.
44
+ const toText = (value: unknown): string => {
45
+ if (typeof value === "string") return value;
46
+ if (typeof value === "bigint" || typeof value === "symbol") {
47
+ return value.toString();
48
+ }
49
+ return JSON.stringify(value) ?? String(value);
50
+ };
51
+
52
+ const primitiveContent = (value: unknown): WebMcpContent[] => [
53
+ textContent(toText(value)),
54
+ ];
55
+
56
+ const mapModelContentPart = (part: ToolModelContentPart): WebMcpContent => {
57
+ if (part.type === "text") {
58
+ return textContent(part.text ?? "");
59
+ }
60
+ if (part.type === "file") {
61
+ if (
62
+ typeof part.mediaType === "string" &&
63
+ part.mediaType.startsWith("image/")
64
+ ) {
65
+ return { type: "image", data: part.data ?? "", mimeType: part.mediaType };
66
+ }
67
+ return textContent(part.data ?? "");
68
+ }
69
+ return textContent(toText(part));
70
+ };
71
+
72
+ export const toMcpContent = async (
73
+ result: unknown,
74
+ options: {
75
+ tool: Tool<any, any>;
76
+ toolCallId: string;
77
+ args: Record<string, unknown>;
78
+ },
79
+ ): Promise<WebMcpCallToolResult> => {
80
+ const response = ToolResponse.toResponse(result);
81
+ if (response.modelContent) {
82
+ const content = response.modelContent.map(mapModelContentPart);
83
+ return response.isError ? { isError: true, content } : { content };
84
+ }
85
+ if (!response.isError && options.tool.toModelOutput) {
86
+ try {
87
+ const parts = await options.tool.toModelOutput({
88
+ toolCallId: options.toolCallId,
89
+ input: options.args,
90
+ output: response.result,
91
+ });
92
+ return { content: parts.map(mapModelContentPart) };
93
+ } catch (e) {
94
+ console.warn(
95
+ "[assistant-ui] toModelOutput threw; falling back to default projection.",
96
+ e,
97
+ );
98
+ }
99
+ }
100
+ const content = primitiveContent(response.result);
101
+ return response.isError ? { isError: true, content } : { content };
102
+ };
103
+
104
+ type StandardSchemaLike = {
105
+ "~standard": {
106
+ version: number;
107
+ validate: (
108
+ value: unknown,
109
+ ) =>
110
+ | { issues?: readonly unknown[] | undefined }
111
+ | Promise<{ issues?: readonly unknown[] | undefined }>;
112
+ };
113
+ };
114
+
115
+ const isStandardSchema = (schema: unknown): schema is StandardSchemaLike =>
116
+ typeof schema === "object" &&
117
+ schema !== null &&
118
+ "~standard" in schema &&
119
+ (schema as StandardSchemaLike)["~standard"].version === 1;
120
+
121
+ const TOOL_ABORTED = Symbol("assistant-ui.webmcp-tool-aborted");
122
+
123
+ const isThenable = <T>(value: T | PromiseLike<T>): value is PromiseLike<T> =>
124
+ typeof (value as PromiseLike<T> | null | undefined)?.then === "function";
125
+
126
+ const raceWithAbort = async <T>(
127
+ value: PromiseLike<T>,
128
+ abortSignal: AbortSignal,
129
+ ): Promise<T | typeof TOOL_ABORTED> => {
130
+ let onAbort!: () => void;
131
+ const abortPromise = new Promise<typeof TOOL_ABORTED>((resolve) => {
132
+ onAbort = () => resolve(TOOL_ABORTED);
133
+ if (abortSignal.aborted) {
134
+ onAbort();
135
+ } else {
136
+ abortSignal.addEventListener("abort", onAbort, { once: true });
137
+ }
138
+ });
139
+
140
+ try {
141
+ // Unlike assistant-stream's helper, cancellation wins when validation aborts and rejects synchronously.
142
+ return await Promise.race([abortPromise, value]);
143
+ } finally {
144
+ abortSignal.removeEventListener("abort", onAbort);
145
+ }
146
+ };
147
+
148
+ // AbortSignal.any sits above the browserslist floor and rejects any input that
149
+ // is not a native AbortSignal, which a navigator.modelContext polyfill's signal
150
+ // is not. The merged signal tracks its inputs only until cleanup runs, where
151
+ // the single-signal path hands the caller the lifecycle signal itself.
152
+ const combineAbortSignals = (
153
+ callerSignal: AbortSignal,
154
+ lifecycleSignal: AbortSignal,
155
+ ): { signal: AbortSignal; cleanup: () => void } => {
156
+ const controller = new AbortController();
157
+ const teardown: (() => void)[] = [];
158
+ const cleanup = () => {
159
+ while (teardown.length) teardown.pop()!();
160
+ };
161
+ const abort = (reason: unknown) => {
162
+ cleanup();
163
+ controller.abort(reason);
164
+ };
165
+ const listen = (signal: AbortSignal) => {
166
+ const onAbort = () => abort(signal.reason);
167
+ signal.addEventListener("abort", onAbort, { once: true });
168
+ teardown.push(() => signal.removeEventListener("abort", onAbort));
169
+ };
170
+
171
+ if (callerSignal.aborted) {
172
+ abort(callerSignal.reason);
173
+ } else if (lifecycleSignal.aborted) {
174
+ abort(lifecycleSignal.reason);
175
+ } else {
176
+ listen(callerSignal);
177
+ listen(lifecycleSignal);
178
+ }
179
+
180
+ return { signal: controller.signal, cleanup };
181
+ };
182
+
183
+ export const toWebMcpTool = (
184
+ name: string,
185
+ getTool: () => Tool<any, any>,
186
+ lifecycleSignal?: AbortSignal,
187
+ ): WebMcpToolDescriptor => ({
188
+ name,
189
+ description: getTool().description ?? "",
190
+ inputSchema: toWebMcpInputSchema(getTool()),
191
+ execute: async (rawArgs, context) => {
192
+ if (lifecycleSignal?.aborted) {
193
+ return errorResult(`Tool "${name}" is no longer registered`);
194
+ }
195
+ const tool = getTool();
196
+ const args = (rawArgs ?? {}) as Record<string, unknown>;
197
+ const toolCallId = generateId();
198
+ let cleanup: (() => void) | undefined;
199
+ try {
200
+ const callerSignal = context?.signal;
201
+ let abortSignal: AbortSignal | undefined;
202
+ if (callerSignal && lifecycleSignal) {
203
+ const combined = combineAbortSignals(callerSignal, lifecycleSignal);
204
+ abortSignal = combined.signal;
205
+ cleanup = combined.cleanup;
206
+ } else {
207
+ abortSignal = callerSignal ?? lifecycleSignal;
208
+ }
209
+
210
+ if (abortSignal?.aborted) {
211
+ return errorResult("Tool execution was cancelled.");
212
+ }
213
+
214
+ let executeFn = tool.execute;
215
+ if (isStandardSchema(tool.parameters)) {
216
+ const result = tool.parameters["~standard"].validate(args);
217
+ const validation = isThenable(result)
218
+ ? abortSignal
219
+ ? await raceWithAbort(result, abortSignal)
220
+ : await result
221
+ : result;
222
+ if (validation === TOOL_ABORTED) {
223
+ return errorResult("Tool execution was cancelled.");
224
+ }
225
+ if (validation.issues) {
226
+ const issues = validation.issues;
227
+ executeFn =
228
+ tool.experimental_onSchemaValidationError ??
229
+ (() => {
230
+ throw new Error(
231
+ `Function parameter validation failed. ${JSON.stringify(issues)}`,
232
+ );
233
+ });
234
+ }
235
+ }
236
+
237
+ if (abortSignal?.aborted) {
238
+ return errorResult("Tool execution was cancelled.");
239
+ }
240
+
241
+ if (!executeFn) {
242
+ return errorResult(`Tool "${name}" has no client-side implementation.`);
243
+ }
244
+
245
+ const result = await executeFn(args, {
246
+ toolCallId,
247
+ abortSignal: abortSignal ?? new AbortController().signal,
248
+ human: () =>
249
+ Promise.reject(
250
+ new Error("human input not supported in WebMCP context"),
251
+ ),
252
+ });
253
+ return await toMcpContent(result, { tool, toolCallId, args });
254
+ } catch (e) {
255
+ return errorResult(e instanceof Error ? e.message : String(e));
256
+ } finally {
257
+ cleanup?.();
258
+ }
259
+ },
260
+ });