@assistant-ui/react 0.14.0 → 0.14.3

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 (78) hide show
  1. package/README.md +58 -42
  2. package/dist/client/ExternalThread.d.ts +7 -0
  3. package/dist/client/ExternalThread.d.ts.map +1 -1
  4. package/dist/client/ExternalThread.js +24 -16
  5. package/dist/client/ExternalThread.js.map +1 -1
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/legacy-runtime/cloud/auiV0.d.ts +10 -1
  11. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
  12. package/dist/legacy-runtime/cloud/auiV0.js +21 -3
  13. package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
  14. package/dist/mcp-apps/McpAppRenderer.d.ts +33 -0
  15. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -0
  16. package/dist/mcp-apps/McpAppRenderer.js +115 -0
  17. package/dist/mcp-apps/McpAppRenderer.js.map +1 -0
  18. package/dist/mcp-apps/McpAppsRemoteHost.d.ts +3 -0
  19. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -0
  20. package/dist/mcp-apps/McpAppsRemoteHost.js +27 -0
  21. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -0
  22. package/dist/mcp-apps/app-frame.d.ts +3 -0
  23. package/dist/mcp-apps/app-frame.d.ts.map +1 -0
  24. package/dist/mcp-apps/app-frame.js +218 -0
  25. package/dist/mcp-apps/app-frame.js.map +1 -0
  26. package/dist/mcp-apps/bridge.d.ts +18 -0
  27. package/dist/mcp-apps/bridge.d.ts.map +1 -0
  28. package/dist/mcp-apps/bridge.js +290 -0
  29. package/dist/mcp-apps/bridge.js.map +1 -0
  30. package/dist/mcp-apps/index.d.ts +5 -0
  31. package/dist/mcp-apps/index.d.ts.map +1 -0
  32. package/dist/mcp-apps/index.js +4 -0
  33. package/dist/mcp-apps/index.js.map +1 -0
  34. package/dist/mcp-apps/types.d.ts +149 -0
  35. package/dist/mcp-apps/types.d.ts.map +1 -0
  36. package/dist/mcp-apps/types.js +3 -0
  37. package/dist/mcp-apps/types.js.map +1 -0
  38. package/dist/mcp-apps/utils.d.ts +5 -0
  39. package/dist/mcp-apps/utils.d.ts.map +1 -0
  40. package/dist/mcp-apps/utils.js +10 -0
  41. package/dist/mcp-apps/utils.js.map +1 -0
  42. package/dist/primitives/composer/ComposerInput.d.ts +6 -0
  43. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  44. package/dist/primitives/composer/ComposerInput.js +19 -2
  45. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  46. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  47. package/dist/primitives/composer/trigger/TriggerPopover.js +17 -1
  48. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  49. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +33 -0
  50. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts.map +1 -1
  51. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +80 -11
  52. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  53. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  54. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +2 -1
  55. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  56. package/dist/primitives/messagePart/useMessagePartSource.d.ts +22 -3
  57. package/dist/primitives/messagePart/useMessagePartSource.d.ts.map +1 -1
  58. package/package.json +6 -5
  59. package/src/client/ExternalThread.ts +32 -17
  60. package/src/index.ts +25 -0
  61. package/src/legacy-runtime/cloud/auiV0.ts +37 -4
  62. package/src/mcp-apps/McpAppRenderer.tsx +221 -0
  63. package/src/mcp-apps/McpAppsRemoteHost.ts +52 -0
  64. package/src/mcp-apps/app-frame.tsx +303 -0
  65. package/src/mcp-apps/bridge.test.ts +391 -0
  66. package/src/mcp-apps/bridge.ts +435 -0
  67. package/src/mcp-apps/index.ts +17 -0
  68. package/src/mcp-apps/types.ts +163 -0
  69. package/src/mcp-apps/utils.ts +16 -0
  70. package/src/primitives/composer/ComposerInput.test.tsx +48 -0
  71. package/src/primitives/composer/ComposerInput.tsx +20 -2
  72. package/src/primitives/composer/trigger/TriggerPopover.tsx +21 -1
  73. package/src/primitives/composer/trigger/TriggerPopoverRootContext.test.tsx +152 -0
  74. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +134 -17
  75. package/src/primitives/composer/trigger/triggerKeyboardResource.test.ts +236 -0
  76. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +2 -1
  77. package/src/tests/BaseComposerRuntimeCore.test.ts +4 -0
  78. package/src/tests/auiV0Encode.test.ts +55 -0
@@ -0,0 +1,435 @@
1
+ import type { RenderedFrame } from "safe-content-frame";
2
+ import {
3
+ MCP_APP_PROTOCOL_VERSION,
4
+ type McpAppBridgeHandlers,
5
+ type McpAppDisplayMode,
6
+ type McpAppHostContext,
7
+ type McpAppHostInfo,
8
+ type McpAppJsonRpcMessage,
9
+ type McpAppJsonRpcNotification,
10
+ type McpAppJsonRpcRequest,
11
+ type McpAppJsonRpcResponse,
12
+ } from "./types";
13
+
14
+ const VALID_DISPLAY_MODES = [
15
+ "inline",
16
+ "fullscreen",
17
+ "pip",
18
+ ] as const satisfies readonly McpAppDisplayMode[];
19
+
20
+ export type McpAppBridgeFrame = Pick<
21
+ RenderedFrame,
22
+ "iframe" | "origin" | "sendMessage"
23
+ >;
24
+
25
+ export type CreateMcpAppBridgeOptions = {
26
+ frame: McpAppBridgeFrame;
27
+ handlers?: McpAppBridgeHandlers | undefined;
28
+ hostInfo?: McpAppHostInfo | undefined;
29
+ hostContext?: McpAppHostContext | undefined;
30
+ targetWindow?: Window | undefined;
31
+ };
32
+
33
+ export type McpAppBridge = {
34
+ dispose: () => void;
35
+ notifyToolInput: (input: unknown) => void;
36
+ notifyToolResult: (result: unknown) => void;
37
+ notifyHostContextChanged: (hostContext: McpAppHostContext) => void;
38
+ };
39
+
40
+ const DEFAULT_HOST_INFO: McpAppHostInfo = {
41
+ name: "assistant-ui",
42
+ version: "0.1",
43
+ };
44
+
45
+ const JSONRPC_ERROR = {
46
+ parseError: -32700,
47
+ invalidRequest: -32600,
48
+ methodNotFound: -32601,
49
+ invalidParams: -32602,
50
+ internalError: -32603,
51
+ } as const;
52
+
53
+ function isJsonRpcMessage(value: unknown): value is McpAppJsonRpcMessage {
54
+ if (!value || typeof value !== "object") return false;
55
+ const v = value as Record<string, unknown>;
56
+ return v.jsonrpc === "2.0" && typeof v.method === "string";
57
+ }
58
+
59
+ function isRequest(msg: McpAppJsonRpcMessage): msg is McpAppJsonRpcRequest {
60
+ return "id" in msg;
61
+ }
62
+
63
+ function isNotification(
64
+ msg: McpAppJsonRpcMessage,
65
+ ): msg is McpAppJsonRpcNotification {
66
+ return !("id" in msg);
67
+ }
68
+
69
+ export function createMcpAppBridge(
70
+ opts: CreateMcpAppBridgeOptions,
71
+ ): McpAppBridge {
72
+ const {
73
+ frame,
74
+ handlers = {},
75
+ hostInfo = DEFAULT_HOST_INFO,
76
+ hostContext = {},
77
+ targetWindow = typeof window !== "undefined" ? window : undefined,
78
+ } = opts;
79
+
80
+ if (!targetWindow) {
81
+ throw new Error("createMcpAppBridge requires a window context");
82
+ }
83
+
84
+ const post = (msg: McpAppJsonRpcMessage) => {
85
+ frame.sendMessage(msg);
86
+ };
87
+
88
+ const respond = (
89
+ id: McpAppJsonRpcRequest["id"],
90
+ payload:
91
+ | { result: unknown }
92
+ | { error: { code: number; message: string; data?: unknown } },
93
+ ) => {
94
+ const res: McpAppJsonRpcResponse = {
95
+ jsonrpc: "2.0",
96
+ id,
97
+ ...payload,
98
+ };
99
+ post(res);
100
+ };
101
+
102
+ const errorResponse = (
103
+ id: McpAppJsonRpcRequest["id"],
104
+ code: number,
105
+ message: string,
106
+ data?: unknown,
107
+ ) => {
108
+ respond(id, {
109
+ error: {
110
+ code,
111
+ message,
112
+ ...(data !== undefined ? { data } : {}),
113
+ },
114
+ });
115
+ };
116
+
117
+ const handleRequest = async (req: McpAppJsonRpcRequest) => {
118
+ try {
119
+ const params = req.params;
120
+
121
+ switch (req.method) {
122
+ case "ui/initialize": {
123
+ respond(req.id, {
124
+ result: {
125
+ protocolVersion: MCP_APP_PROTOCOL_VERSION,
126
+ host: hostInfo,
127
+ hostContext,
128
+ capabilities: {
129
+ tools: handlers.callTool ? {} : undefined,
130
+ resources:
131
+ handlers.readResource || handlers.listResources
132
+ ? {}
133
+ : undefined,
134
+ ui: {
135
+ sendMessage: !!handlers.sendMessage,
136
+ openLink: !!handlers.openLink,
137
+ requestDisplayMode: !!handlers.requestDisplayMode,
138
+ updateModelContext: !!handlers.updateModelContext,
139
+ },
140
+ },
141
+ },
142
+ });
143
+ return;
144
+ }
145
+
146
+ case "tools/call": {
147
+ if (!handlers.callTool) {
148
+ errorResponse(
149
+ req.id,
150
+ JSONRPC_ERROR.methodNotFound,
151
+ "tools/call is not supported by this host",
152
+ );
153
+ return;
154
+ }
155
+ const callParams = (params ?? {}) as {
156
+ name?: unknown;
157
+ arguments?: unknown;
158
+ };
159
+ if (typeof callParams.name !== "string") {
160
+ errorResponse(
161
+ req.id,
162
+ JSONRPC_ERROR.invalidParams,
163
+ "tools/call requires a string 'name'",
164
+ );
165
+ return;
166
+ }
167
+ if (
168
+ handlers.allowedTools &&
169
+ !handlers.allowedTools.includes(callParams.name)
170
+ ) {
171
+ errorResponse(
172
+ req.id,
173
+ JSONRPC_ERROR.invalidParams,
174
+ `tool '${callParams.name}' is not allowed for this app`,
175
+ );
176
+ return;
177
+ }
178
+ let callArgs: Record<string, unknown> | undefined;
179
+ if (callParams.arguments !== undefined) {
180
+ if (
181
+ callParams.arguments === null ||
182
+ typeof callParams.arguments !== "object" ||
183
+ Array.isArray(callParams.arguments)
184
+ ) {
185
+ errorResponse(
186
+ req.id,
187
+ JSONRPC_ERROR.invalidParams,
188
+ "tools/call 'arguments' must be an object",
189
+ );
190
+ return;
191
+ }
192
+ callArgs = callParams.arguments as Record<string, unknown>;
193
+ }
194
+ const result = await handlers.callTool({
195
+ name: callParams.name,
196
+ ...(callArgs !== undefined ? { arguments: callArgs } : {}),
197
+ });
198
+ respond(req.id, { result });
199
+ return;
200
+ }
201
+
202
+ case "resources/read": {
203
+ if (!handlers.readResource) {
204
+ errorResponse(
205
+ req.id,
206
+ JSONRPC_ERROR.methodNotFound,
207
+ "resources/read is not supported by this host",
208
+ );
209
+ return;
210
+ }
211
+ const readParams = (params ?? {}) as { uri?: unknown };
212
+ if (typeof readParams.uri !== "string") {
213
+ errorResponse(
214
+ req.id,
215
+ JSONRPC_ERROR.invalidParams,
216
+ "resources/read requires a string 'uri'",
217
+ );
218
+ return;
219
+ }
220
+ respond(req.id, {
221
+ result: await handlers.readResource({ uri: readParams.uri }),
222
+ });
223
+ return;
224
+ }
225
+
226
+ case "resources/list": {
227
+ if (!handlers.listResources) {
228
+ errorResponse(
229
+ req.id,
230
+ JSONRPC_ERROR.methodNotFound,
231
+ "resources/list is not supported by this host",
232
+ );
233
+ return;
234
+ }
235
+ respond(req.id, {
236
+ result: (await handlers.listResources(params)) ?? null,
237
+ });
238
+ return;
239
+ }
240
+
241
+ case "openLink": {
242
+ if (!handlers.openLink) {
243
+ errorResponse(
244
+ req.id,
245
+ JSONRPC_ERROR.methodNotFound,
246
+ "openLink is not supported by this host",
247
+ );
248
+ return;
249
+ }
250
+ const linkParams = (params ?? {}) as { url?: unknown };
251
+ if (typeof linkParams.url !== "string") {
252
+ errorResponse(
253
+ req.id,
254
+ JSONRPC_ERROR.invalidParams,
255
+ "openLink requires a string 'url'",
256
+ );
257
+ return;
258
+ }
259
+ let linkProtocol: string;
260
+ try {
261
+ linkProtocol = new URL(linkParams.url).protocol;
262
+ } catch {
263
+ errorResponse(
264
+ req.id,
265
+ JSONRPC_ERROR.invalidParams,
266
+ "openLink requires a valid URL",
267
+ );
268
+ return;
269
+ }
270
+ if (linkProtocol !== "https:" && linkProtocol !== "http:") {
271
+ errorResponse(
272
+ req.id,
273
+ JSONRPC_ERROR.invalidParams,
274
+ "openLink only accepts http(s) URLs",
275
+ );
276
+ return;
277
+ }
278
+ respond(req.id, {
279
+ result: await handlers.openLink({ url: linkParams.url }),
280
+ });
281
+ return;
282
+ }
283
+
284
+ case "sendMessage": {
285
+ if (!handlers.sendMessage) {
286
+ errorResponse(
287
+ req.id,
288
+ JSONRPC_ERROR.methodNotFound,
289
+ "sendMessage is not supported by this host",
290
+ );
291
+ return;
292
+ }
293
+ respond(req.id, {
294
+ result: (await handlers.sendMessage(params)) ?? null,
295
+ });
296
+ return;
297
+ }
298
+
299
+ case "updateModelContext": {
300
+ if (!handlers.updateModelContext) {
301
+ errorResponse(
302
+ req.id,
303
+ JSONRPC_ERROR.methodNotFound,
304
+ "updateModelContext is not supported by this host",
305
+ );
306
+ return;
307
+ }
308
+ respond(req.id, {
309
+ result: (await handlers.updateModelContext(params)) ?? null,
310
+ });
311
+ return;
312
+ }
313
+
314
+ case "requestDisplayMode": {
315
+ if (!handlers.requestDisplayMode) {
316
+ errorResponse(
317
+ req.id,
318
+ JSONRPC_ERROR.methodNotFound,
319
+ "requestDisplayMode is not supported by this host",
320
+ );
321
+ return;
322
+ }
323
+ const modeParams = (params ?? {}) as { mode?: unknown };
324
+ if (
325
+ typeof modeParams.mode !== "string" ||
326
+ !VALID_DISPLAY_MODES.includes(modeParams.mode as McpAppDisplayMode)
327
+ ) {
328
+ errorResponse(
329
+ req.id,
330
+ JSONRPC_ERROR.invalidParams,
331
+ "requestDisplayMode requires a valid 'mode'",
332
+ );
333
+ return;
334
+ }
335
+ respond(req.id, {
336
+ result: await handlers.requestDisplayMode({
337
+ mode: modeParams.mode as McpAppDisplayMode,
338
+ }),
339
+ });
340
+ return;
341
+ }
342
+
343
+ default: {
344
+ errorResponse(
345
+ req.id,
346
+ JSONRPC_ERROR.methodNotFound,
347
+ `Unknown method: ${req.method}`,
348
+ );
349
+ }
350
+ }
351
+ } catch (err) {
352
+ const error = err instanceof Error ? err : new Error(String(err));
353
+ handlers.onError?.(error);
354
+ errorResponse(req.id, JSONRPC_ERROR.internalError, error.message);
355
+ }
356
+ };
357
+
358
+ const handleNotification = (note: McpAppJsonRpcNotification) => {
359
+ switch (note.method) {
360
+ case "notifications/initialized": {
361
+ handlers.onInitialized?.();
362
+ return;
363
+ }
364
+ case "notifications/size_changed": {
365
+ const p = (note.params ?? {}) as { width?: number; height?: number };
366
+ handlers.onSizeChange?.({
367
+ ...(typeof p.width === "number" ? { width: p.width } : {}),
368
+ ...(typeof p.height === "number" ? { height: p.height } : {}),
369
+ });
370
+ return;
371
+ }
372
+ case "notifications/log": {
373
+ handlers.onLog?.(note.params);
374
+ return;
375
+ }
376
+ case "notifications/request_teardown": {
377
+ handlers.onRequestTeardown?.(note.params);
378
+ return;
379
+ }
380
+ case "notifications/error": {
381
+ const p = (note.params ?? {}) as { message?: string };
382
+ handlers.onError?.(
383
+ new Error(typeof p.message === "string" ? p.message : "Widget error"),
384
+ );
385
+ return;
386
+ }
387
+ default:
388
+ return;
389
+ }
390
+ };
391
+
392
+ // Cross-origin guard: ignore any postMessage not originating from this
393
+ // app's iframe contentWindow at the SafeContentFrame-issued origin.
394
+ const onMessage = (event: MessageEvent) => {
395
+ if (event.source !== frame.iframe.contentWindow) return;
396
+ if (event.origin !== frame.origin) return;
397
+ if (!isJsonRpcMessage(event.data)) return;
398
+
399
+ const msg = event.data;
400
+ if (isRequest(msg)) {
401
+ void handleRequest(msg);
402
+ } else if (isNotification(msg)) {
403
+ handleNotification(msg);
404
+ }
405
+ };
406
+
407
+ targetWindow.addEventListener("message", onMessage);
408
+
409
+ return {
410
+ dispose: () => {
411
+ targetWindow.removeEventListener("message", onMessage);
412
+ },
413
+ notifyToolInput: (input: unknown) => {
414
+ post({
415
+ jsonrpc: "2.0",
416
+ method: "notifications/tools/call/input",
417
+ params: { input },
418
+ });
419
+ },
420
+ notifyToolResult: (result: unknown) => {
421
+ post({
422
+ jsonrpc: "2.0",
423
+ method: "notifications/tools/call/result",
424
+ params: { result },
425
+ });
426
+ },
427
+ notifyHostContextChanged: (ctx: McpAppHostContext) => {
428
+ post({
429
+ jsonrpc: "2.0",
430
+ method: "notifications/host_context/changed",
431
+ params: ctx,
432
+ });
433
+ },
434
+ };
435
+ }
@@ -0,0 +1,17 @@
1
+ export { McpAppRenderer, type McpAppRendererOptions } from "./McpAppRenderer";
2
+ export { McpAppsRemoteHost } from "./McpAppsRemoteHost";
3
+ export { getMcpAppFromToolPart } from "./utils";
4
+ export type {
5
+ McpAppMetadata,
6
+ McpAppResource,
7
+ McpAppResourceMeta,
8
+ McpAppResourceCSP,
9
+ McpAppSandboxConfig,
10
+ McpAppHostInfo,
11
+ McpAppHostContext,
12
+ McpAppDisplayMode,
13
+ McpAppsHost,
14
+ McpAppsRemoteHostOptions,
15
+ McpAppToolCallParams,
16
+ ToolCallMessagePartMcpMetadata,
17
+ } from "./types";
@@ -0,0 +1,163 @@
1
+ import type { CSSProperties } from "react";
2
+ import type {
3
+ McpAppMetadata,
4
+ ToolCallMessagePartMcpMetadata,
5
+ } from "@assistant-ui/core";
6
+ import type { SandboxOption } from "safe-content-frame";
7
+
8
+ export type { McpAppMetadata, ToolCallMessagePartMcpMetadata };
9
+
10
+ export const MCP_APP_MIME_TYPE = "text/html;profile=mcp-app" as const;
11
+
12
+ export const MCP_APP_PROTOCOL_VERSION = "0.1" as const;
13
+
14
+ export type McpAppResourceCSP = {
15
+ connectDomains?: string[];
16
+ resourceDomains?: string[];
17
+ frameDomains?: string[];
18
+ [k: string]: unknown;
19
+ };
20
+
21
+ export type McpAppResourceMeta = {
22
+ prefersBorder?: boolean;
23
+ csp?: McpAppResourceCSP;
24
+ permissions?: Record<string, unknown>;
25
+ [k: string]: unknown;
26
+ };
27
+
28
+ export type McpAppResource = {
29
+ uri: string;
30
+ mimeType: typeof MCP_APP_MIME_TYPE;
31
+ html: string;
32
+ meta?: McpAppResourceMeta;
33
+ };
34
+
35
+ export type McpAppDisplayMode = "inline" | "fullscreen" | "pip";
36
+
37
+ export type McpAppHostContext = {
38
+ theme?: "light" | "dark";
39
+ displayMode?: McpAppDisplayMode;
40
+ availableDisplayModes?: McpAppDisplayMode[];
41
+ [k: string]: unknown;
42
+ };
43
+
44
+ export type McpAppHostInfo = {
45
+ name: string;
46
+ version: string;
47
+ };
48
+
49
+ /**
50
+ * What `McpAppRenderer` needs from its host — the data-plane operations
51
+ * the widget can request. Provided by a host resource like
52
+ * `McpAppsRemoteHost`.
53
+ */
54
+ export type McpAppsHost = {
55
+ loadResource: (params: { uri: string }) => Promise<McpAppResource>;
56
+ callTool: (params: McpAppToolCallParams) => Promise<unknown>;
57
+ readResource: (params: { uri: string }) => Promise<unknown>;
58
+ listResources: (params?: unknown) => Promise<unknown>;
59
+ };
60
+
61
+ /**
62
+ * Options for `McpAppsRemoteHost`. The host POSTs `{ method, params }` to
63
+ * `url` and expects JSON responses. Method names sent:
64
+ * - `mcp-apps/read-resource` (`{ uri }`) → `McpAppResource`
65
+ * - `tools/call` (`{ name, arguments? }`) → tool result
66
+ * - `resources/read` (`{ uri }`) → resource read result
67
+ * - `resources/list` (`params?`) → list result
68
+ */
69
+ export type McpAppsRemoteHostOptions = {
70
+ url: string;
71
+ fetch?: typeof fetch;
72
+ headers?:
73
+ | Record<string, string>
74
+ | (() => Record<string, string> | Promise<Record<string, string>>);
75
+ };
76
+
77
+ export type McpAppToolCallParams = {
78
+ name: string;
79
+ arguments?: Record<string, unknown>;
80
+ };
81
+
82
+ export type McpAppBridgeHandlers = {
83
+ allowedTools?: readonly string[];
84
+ callTool?: (params: McpAppToolCallParams) => Promise<unknown> | unknown;
85
+ readResource?: (params: { uri: string }) => Promise<unknown> | unknown;
86
+ listResources?: (params?: unknown) => Promise<unknown> | unknown;
87
+ openLink?: (params: { url: string }) => Promise<unknown> | unknown;
88
+ sendMessage?: (params: unknown) => Promise<unknown> | unknown;
89
+ updateModelContext?: (params: unknown) => Promise<unknown> | unknown;
90
+ requestDisplayMode?: (params: {
91
+ mode: McpAppDisplayMode;
92
+ }) => Promise<{ mode: McpAppDisplayMode }> | { mode: McpAppDisplayMode };
93
+ onSizeChange?: (params: { width?: number; height?: number }) => void;
94
+ onInitialized?: () => void;
95
+ onRequestTeardown?: (params: unknown) => void;
96
+ onLog?: (params: unknown) => void;
97
+ onError?: (error: Error) => void;
98
+ };
99
+
100
+ export type McpAppSandboxConfig = {
101
+ sandbox?: SandboxOption[];
102
+ useShadowDom?: boolean;
103
+ enableBrowserCaching?: boolean;
104
+ salt?: string;
105
+ product?: string;
106
+ className?: string;
107
+ style?: CSSProperties;
108
+ unsafeDocumentWrite?: boolean;
109
+ };
110
+
111
+ export type McpAppFrameProps = {
112
+ app: McpAppMetadata;
113
+ resource: McpAppResource;
114
+ input?: unknown;
115
+ output?: unknown;
116
+ sandbox?: McpAppSandboxConfig | undefined;
117
+ handlers?: McpAppBridgeHandlers | undefined;
118
+ hostInfo?: McpAppHostInfo | undefined;
119
+ hostContext?: McpAppHostContext | undefined;
120
+ /**
121
+ * Upper bound (in pixels) for the auto-resize height driven by the widget's
122
+ * `notifications/size_changed`. Defaults to 800.
123
+ */
124
+ maxHeight?: number | undefined;
125
+ };
126
+
127
+ export type McpAppJsonRpcRequest = {
128
+ jsonrpc: "2.0";
129
+ id: string | number;
130
+ method: string;
131
+ params?: unknown;
132
+ };
133
+
134
+ export type McpAppJsonRpcNotification = {
135
+ jsonrpc: "2.0";
136
+ method: string;
137
+ params?: unknown;
138
+ };
139
+
140
+ export type McpAppJsonRpcError = {
141
+ code: number;
142
+ message: string;
143
+ data?: unknown;
144
+ };
145
+
146
+ export type McpAppJsonRpcResponse =
147
+ | {
148
+ jsonrpc: "2.0";
149
+ id: string | number;
150
+ result: unknown;
151
+ error?: never;
152
+ }
153
+ | {
154
+ jsonrpc: "2.0";
155
+ id: string | number;
156
+ result?: never;
157
+ error: McpAppJsonRpcError;
158
+ };
159
+
160
+ export type McpAppJsonRpcMessage =
161
+ | McpAppJsonRpcRequest
162
+ | McpAppJsonRpcNotification
163
+ | McpAppJsonRpcResponse;
@@ -0,0 +1,16 @@
1
+ import {
2
+ isMcpAppUri,
3
+ type McpAppMetadata,
4
+ type ToolCallMessagePart,
5
+ } from "@assistant-ui/core";
6
+
7
+ type ToolPartLike = Pick<ToolCallMessagePart, "mcp">;
8
+
9
+ export function getMcpAppFromToolPart(
10
+ part: ToolPartLike,
11
+ ): McpAppMetadata | undefined {
12
+ const app = part.mcp?.app;
13
+ if (!app) return undefined;
14
+ if (!isMcpAppUri(app.resourceUri)) return undefined;
15
+ return app;
16
+ }
@@ -66,6 +66,15 @@ vi.mock("./ComposerInputPluginContext", () => ({
66
66
  useComposerInputPluginRegistryOptional: () => pluginRegistry,
67
67
  }));
68
68
 
69
+ let activeAria: {
70
+ popoverId: string;
71
+ highlightedItemId: string | undefined;
72
+ } | null = null;
73
+
74
+ vi.mock("./trigger/TriggerPopoverRootContext", () => ({
75
+ useTriggerPopoverActiveAriaOptional: () => activeAria,
76
+ }));
77
+
69
78
  vi.mock("@radix-ui/react-use-escape-keydown", () => ({
70
79
  useEscapeKeydown: () => {},
71
80
  }));
@@ -121,6 +130,7 @@ describe("ComposerPrimitiveInput", () => {
121
130
  threadState.isRunning = false;
122
131
  threadState.capabilities = { queue: false, attachments: false };
123
132
  pluginRegistry = null;
133
+ activeAria = null;
124
134
 
125
135
  container = document.createElement("div");
126
136
  document.body.appendChild(container);
@@ -229,4 +239,42 @@ describe("ComposerPrimitiveInput", () => {
229
239
  });
230
240
  expect(setText).not.toHaveBeenCalled();
231
241
  });
242
+
243
+ it("does not apply ARIA combobox attributes when no trigger popover is open", async () => {
244
+ activeAria = null;
245
+ const textarea = await mount();
246
+
247
+ expect(textarea.getAttribute("aria-controls")).toBeNull();
248
+ expect(textarea.getAttribute("aria-expanded")).toBeNull();
249
+ expect(textarea.getAttribute("aria-haspopup")).toBeNull();
250
+ expect(textarea.getAttribute("aria-activedescendant")).toBeNull();
251
+ });
252
+
253
+ it("applies ARIA combobox attributes when a trigger popover is open", async () => {
254
+ activeAria = {
255
+ popoverId: "popover-1",
256
+ highlightedItemId: "popover-1-option-foo",
257
+ };
258
+ const textarea = await mount();
259
+
260
+ expect(textarea.getAttribute("aria-controls")).toBe("popover-1");
261
+ expect(textarea.getAttribute("aria-expanded")).toBe("true");
262
+ expect(textarea.getAttribute("aria-haspopup")).toBe("listbox");
263
+ expect(textarea.getAttribute("aria-activedescendant")).toBe(
264
+ "popover-1-option-foo",
265
+ );
266
+ });
267
+
268
+ it("omits aria-activedescendant when no item is highlighted", async () => {
269
+ activeAria = {
270
+ popoverId: "popover-1",
271
+ highlightedItemId: undefined,
272
+ };
273
+ const textarea = await mount();
274
+
275
+ expect(textarea.getAttribute("aria-controls")).toBe("popover-1");
276
+ expect(textarea.getAttribute("aria-expanded")).toBe("true");
277
+ expect(textarea.getAttribute("aria-haspopup")).toBe("listbox");
278
+ expect(textarea.getAttribute("aria-activedescendant")).toBeNull();
279
+ });
232
280
  });