@assistant-ui/react 0.15.14 → 0.15.16

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 (210) hide show
  1. package/dist/assistant-transport.d.ts +32 -0
  2. package/dist/assistant-transport.d.ts.map +1 -0
  3. package/dist/assistant-transport.js +27 -0
  4. package/dist/assistant-transport.js.map +1 -0
  5. package/dist/context/providers/MessageProvider.js +1 -1
  6. package/dist/devtools/DevToolsHooks.d.ts +0 -2
  7. package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
  8. package/dist/devtools/DevToolsHooks.js +10 -12
  9. package/dist/devtools/DevToolsHooks.js.map +1 -1
  10. package/dist/hooks/useMessageQuote.js +1 -1
  11. package/dist/hooks/useToolCallElapsed.js +1 -1
  12. package/dist/index.d.ts +4 -7
  13. package/dist/index.js +6 -8
  14. package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -1
  15. package/dist/mcp-apps/McpAppRenderer.js +1 -1
  16. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  17. package/dist/mcp-apps/McpAppsRemoteHost.js +18 -2
  18. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  19. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  20. package/dist/mcp-apps/bridge.js +33 -24
  21. package/dist/mcp-apps/bridge.js.map +1 -1
  22. package/dist/model-context/makeAssistantVisible.js +1 -1
  23. package/dist/primitives/actionBar/ActionBarCopy.js +1 -1
  24. package/dist/primitives/actionBar/ActionBarExportMarkdown.d.ts.map +1 -1
  25. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +5 -2
  26. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  27. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -1
  28. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -1
  29. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +24 -37
  31. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
  32. package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -1
  33. package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
  34. package/dist/primitives/attachment/AttachmentName.js +1 -1
  35. package/dist/primitives/attachment/AttachmentRemove.js +1 -1
  36. package/dist/primitives/attachment/AttachmentThumb.js +1 -1
  37. package/dist/primitives/branchPicker/BranchPickerCount.js +1 -1
  38. package/dist/primitives/branchPicker/BranchPickerNumber.js +1 -1
  39. package/dist/primitives/branchPicker/BranchPickerRoot.d.ts.map +1 -1
  40. package/dist/primitives/branchPicker/BranchPickerRoot.js +20 -15
  41. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  42. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -1
  43. package/dist/primitives/composer/ComposerAddAttachment.js +1 -1
  44. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  45. package/dist/primitives/composer/ComposerDictationTranscript.d.ts +2 -2
  46. package/dist/primitives/composer/ComposerDictationTranscript.js +3 -3
  47. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  48. package/dist/primitives/composer/ComposerInput.js +2 -2
  49. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  50. package/dist/primitives/composer/ComposerQuote.js +1 -1
  51. package/dist/primitives/composer/ComposerRoot.js +1 -1
  52. package/dist/primitives/composer/ComposerStopDictation.js +1 -1
  53. package/dist/primitives/composer/trigger/TriggerPopover.js +1 -1
  54. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  55. package/dist/primitives/composer/trigger/triggerSelectionResource.js +24 -20
  56. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  57. package/dist/primitives/composer/useComposerInputState.js +1 -1
  58. package/dist/primitives/message/MessageIf.js +1 -1
  59. package/dist/primitives/message/MessagePartsGrouped.js +1 -1
  60. package/dist/primitives/message/MessageRoot.js +1 -1
  61. package/dist/primitives/message.js +1 -1
  62. package/dist/primitives/queueItem/QueueItemRemove.js +1 -1
  63. package/dist/primitives/queueItem/QueueItemSteer.js +1 -1
  64. package/dist/primitives/queueItem/QueueItemText.js +1 -1
  65. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
  66. package/dist/primitives/suggestion/SuggestionDescription.js +1 -1
  67. package/dist/primitives/suggestion/SuggestionTitle.js +1 -1
  68. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
  69. package/dist/primitives/thread/ThreadIf.js +1 -1
  70. package/dist/primitives/thread/ThreadRoot.d.ts +3 -0
  71. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  72. package/dist/primitives/thread/ThreadRoot.js +44 -9
  73. package/dist/primitives/thread/ThreadRoot.js.map +1 -1
  74. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  75. package/dist/primitives/thread/ThreadViewport.js +2 -13
  76. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  77. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  78. package/dist/primitives/thread/useThreadViewportAutoScroll.js +23 -8
  79. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  80. package/dist/primitives/threadList/ThreadListNew.js +1 -1
  81. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  82. package/dist/sandbox-host/SandboxHost.js +6 -1
  83. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  84. package/dist/unstable/useComposerInput.d.ts +2 -2
  85. package/dist/unstable/useComposerInput.js +1 -1
  86. package/dist/unstable/useComposerInput.js.map +1 -1
  87. package/dist/unstable/useComposerInputHistory.js +2 -2
  88. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  89. package/dist/unstable/useLiveCompletionAdapter.d.ts +5 -0
  90. package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
  91. package/dist/unstable/useLiveCompletionAdapter.js +16 -3
  92. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  93. package/dist/unstable/useMentionAdapter.d.ts +1 -1
  94. package/dist/utils/invokeCallbackSafely.d.ts +5 -0
  95. package/dist/utils/invokeCallbackSafely.d.ts.map +1 -0
  96. package/dist/utils/invokeCallbackSafely.js +15 -0
  97. package/dist/utils/invokeCallbackSafely.js.map +1 -0
  98. package/dist/utils/smooth/SmoothContext.d.ts +6 -6
  99. package/dist/utils/smooth/SmoothContext.js +1 -1
  100. package/dist/utils/smooth/useSmooth.d.ts +3 -0
  101. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  102. package/dist/utils/smooth/useSmooth.js +16 -1
  103. package/dist/utils/smooth/useSmooth.js.map +1 -1
  104. package/dist/utils/useToolArgsFieldStatus.js +1 -1
  105. package/package.json +10 -10
  106. package/src/assistant-transport.ts +94 -0
  107. package/src/devtools/DevToolsHooks.test.ts +40 -0
  108. package/src/devtools/DevToolsHooks.ts +10 -15
  109. package/src/index.ts +8 -4
  110. package/src/mcp-apps/McpAppsRemoteHost.test.ts +29 -3
  111. package/src/mcp-apps/McpAppsRemoteHost.ts +42 -7
  112. package/src/mcp-apps/bridge.test.ts +153 -0
  113. package/src/mcp-apps/bridge.ts +42 -30
  114. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
  115. package/src/primitives/actionBar/ActionBarExportMarkdown.test.tsx +67 -0
  116. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +3 -1
  117. package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +0 -9
  118. package/src/primitives/branchPicker/BranchPickerRoot.tsx +5 -3
  119. package/src/primitives/composer/ComposerDictationTranscript.tsx +2 -2
  120. package/src/primitives/composer/ComposerInput.tsx +1 -1
  121. package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +135 -0
  122. package/src/primitives/composer/trigger/triggerSelectionResource.ts +2 -0
  123. package/src/primitives/thread/ThreadRoot.test.tsx +251 -0
  124. package/src/primitives/thread/ThreadRoot.tsx +33 -0
  125. package/src/primitives/thread/ThreadViewport.tsx +1 -8
  126. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +152 -0
  127. package/src/primitives/thread/useThreadViewportAutoScroll.ts +26 -19
  128. package/src/sandbox-host/SandboxHost.test.tsx +90 -0
  129. package/src/sandbox-host/SandboxHost.tsx +8 -2
  130. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +24 -3
  131. package/src/tests/RemoteThreadListRuntime.reloadMainThread.test.tsx +1 -2
  132. package/src/tests/augmentations.test.ts +49 -0
  133. package/src/tests/in-memory-thread-list.test.tsx +1 -1
  134. package/src/tests/threadMessageById.test.tsx +61 -1
  135. package/src/unstable/useComposerInput.ts +2 -2
  136. package/src/unstable/useComposerInputHistory.test.tsx +4 -4
  137. package/src/unstable/useComposerInputHistory.ts +1 -1
  138. package/src/unstable/useLiveCompletionAdapter.test.tsx +157 -0
  139. package/src/unstable/useLiveCompletionAdapter.ts +41 -17
  140. package/src/utils/invokeCallbackSafely.ts +11 -0
  141. package/src/utils/smooth/useSmooth.test.tsx +110 -0
  142. package/src/utils/smooth/useSmooth.ts +20 -6
  143. package/dist/client/InMemoryThreadList.d.ts +0 -12
  144. package/dist/client/InMemoryThreadList.d.ts.map +0 -1
  145. package/dist/client/InMemoryThreadList.js +0 -374
  146. package/dist/client/InMemoryThreadList.js.map +0 -1
  147. package/dist/context/react/utils/ensureBinding.d.ts +0 -5
  148. package/dist/context/react/utils/ensureBinding.d.ts.map +0 -1
  149. package/dist/context/react/utils/ensureBinding.js +0 -24
  150. package/dist/context/react/utils/ensureBinding.js.map +0 -1
  151. package/dist/context/react/utils/useRuntimeState.d.ts +0 -13
  152. package/dist/context/react/utils/useRuntimeState.d.ts.map +0 -1
  153. package/dist/context/react/utils/useRuntimeState.js +0 -33
  154. package/dist/context/react/utils/useRuntimeState.js.map +0 -1
  155. package/dist/legacy-runtime/cloud/auiV0.d.ts +0 -119
  156. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +0 -1
  157. package/dist/legacy-runtime/cloud/auiV0.js +0 -137
  158. package/dist/legacy-runtime/cloud/auiV0.js.map +0 -1
  159. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts +0 -16
  160. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts.map +0 -1
  161. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js +0 -36
  162. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js.map +0 -1
  163. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +0 -17
  164. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts.map +0 -1
  165. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +0 -55
  166. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +0 -1
  167. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts +0 -11
  168. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts.map +0 -1
  169. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +0 -165
  170. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +0 -1
  171. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts +0 -15
  172. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +0 -1
  173. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +0 -81
  174. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +0 -1
  175. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +0 -126
  176. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +0 -1
  177. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.js +0 -0
  178. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts +0 -14
  179. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +0 -1
  180. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +0 -290
  181. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +0 -1
  182. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts +0 -7
  183. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts.map +0 -1
  184. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +0 -25
  185. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +0 -1
  186. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts +0 -7
  187. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts.map +0 -1
  188. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +0 -27
  189. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js.map +0 -1
  190. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts +0 -2
  191. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js +0 -2
  192. package/src/client/InMemoryThreadList.ts +0 -247
  193. package/src/context/react/utils/ensureBinding.ts +0 -46
  194. package/src/context/react/utils/useRuntimeState.ts +0 -46
  195. package/src/legacy-runtime/cloud/auiV0.ts +0 -331
  196. package/src/legacy-runtime/cloud/useCloudThreadListRuntime.ts +0 -31
  197. package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +0 -68
  198. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.test.ts +0 -455
  199. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.ts +0 -172
  200. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +0 -113
  201. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +0 -79
  202. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +0 -97
  203. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +0 -307
  204. package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +0 -170
  205. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +0 -141
  206. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +0 -585
  207. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +0 -476
  208. package/src/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.ts +0 -20
  209. package/src/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.ts +0 -9
  210. package/src/legacy-runtime/runtime-cores/assistant-transport/utils.ts +0 -1
@@ -124,6 +124,53 @@ describe("createMcpAppBridge", () => {
124
124
  bridge.dispose();
125
125
  });
126
126
 
127
+ it.each([
128
+ [
129
+ "throws",
130
+ () => {
131
+ throw new Error("error callback failed");
132
+ },
133
+ ],
134
+ ["rejects", () => Promise.reject(new Error("error callback failed"))],
135
+ ])(
136
+ "returns an error response when onError %s",
137
+ async (_behavior, onErrorCallback) => {
138
+ const { frame, captured } = makeFrame();
139
+ const callTool = vi.fn().mockRejectedValue(new Error("tool failed"));
140
+ const consoleError = vi
141
+ .spyOn(console, "error")
142
+ .mockImplementation(() => {});
143
+ const onError = vi.fn(onErrorCallback);
144
+ const bridge = createMcpAppBridge({
145
+ frame,
146
+ handlers: { callTool, onError },
147
+ });
148
+
149
+ deliver(bridge, {
150
+ jsonrpc: "2.0",
151
+ id: 8,
152
+ method: "tools/call",
153
+ params: { name: "search" },
154
+ });
155
+ await flush();
156
+
157
+ expect(onError).toHaveBeenCalledWith(new Error("tool failed"));
158
+ expect(captured).toEqual([
159
+ {
160
+ jsonrpc: "2.0",
161
+ id: 8,
162
+ error: { code: -32603, message: "tool failed" },
163
+ },
164
+ ]);
165
+ expect(consoleError).toHaveBeenCalledWith(
166
+ "[assistant-ui] MCP App onError callback threw an error",
167
+ new Error("error callback failed"),
168
+ );
169
+ consoleError.mockRestore();
170
+ bridge.dispose();
171
+ },
172
+ );
173
+
127
174
  it("rejects tools/call for disallowed tool with -32602", async () => {
128
175
  const { frame, captured } = makeFrame();
129
176
  const callTool = vi.fn();
@@ -244,6 +291,60 @@ describe("createMcpAppBridge", () => {
244
291
  bridge.dispose();
245
292
  });
246
293
 
294
+ it("isolates throwing notification handlers and reports their errors", () => {
295
+ const { frame, captured } = makeFrame();
296
+ const initializedError = new Error("initialized callback failed");
297
+ const sizeChangeError = new Error("size change callback failed");
298
+ const teardownError = new Error("teardown callback failed");
299
+ const onError = vi.fn();
300
+ const bridge = createMcpAppBridge({
301
+ frame,
302
+ handlers: {
303
+ onInitialized: () => {
304
+ throw initializedError;
305
+ },
306
+ onSizeChange: () => {
307
+ throw sizeChangeError;
308
+ },
309
+ onLog: () => {
310
+ throw "log callback failed";
311
+ },
312
+ onRequestTeardown: () => {
313
+ throw teardownError;
314
+ },
315
+ onError,
316
+ },
317
+ });
318
+
319
+ expect(() => {
320
+ deliver(bridge, {
321
+ jsonrpc: "2.0",
322
+ method: "notifications/initialized",
323
+ });
324
+ deliver(bridge, {
325
+ jsonrpc: "2.0",
326
+ method: "notifications/size_changed",
327
+ });
328
+ deliver(bridge, {
329
+ jsonrpc: "2.0",
330
+ method: "notifications/log",
331
+ });
332
+ deliver(bridge, {
333
+ jsonrpc: "2.0",
334
+ method: "notifications/request_teardown",
335
+ });
336
+ }).not.toThrow();
337
+
338
+ expect(onError.mock.calls.map(([error]) => error)).toEqual([
339
+ initializedError,
340
+ sizeChangeError,
341
+ new Error("log callback failed"),
342
+ teardownError,
343
+ ]);
344
+ expect(captured).toEqual([]);
345
+ bridge.dispose();
346
+ });
347
+
247
348
  it("notifyToolInput / notifyToolResult / notifyHostContextChanged post both legacy and 2026-01-26 notifications", () => {
248
349
  const { frame, captured } = makeFrame();
249
350
  const bridge = createMcpAppBridge({ frame });
@@ -428,4 +529,56 @@ describe("createMcpAppBridge", () => {
428
529
  expect(onRequestTeardown).toHaveBeenCalledWith({ reason: "done" });
429
530
  bridge.dispose();
430
531
  });
532
+
533
+ it("isolates a throwing onError from widget error notifications", () => {
534
+ const { frame } = makeFrame();
535
+ const callbackError = new Error("error callback failed");
536
+ const consoleError = vi
537
+ .spyOn(console, "error")
538
+ .mockImplementation(() => {});
539
+ const onError = vi.fn(() => {
540
+ throw callbackError;
541
+ });
542
+ const bridge = createMcpAppBridge({
543
+ frame,
544
+ handlers: { callTool: vi.fn(), onError },
545
+ });
546
+
547
+ expect(() =>
548
+ deliver(bridge, {
549
+ jsonrpc: "2.0",
550
+ method: "notifications/error",
551
+ params: { message: "kaboom" },
552
+ }),
553
+ ).not.toThrow();
554
+
555
+ expect(onError).toHaveBeenCalledWith(new Error("kaboom"));
556
+ expect(consoleError).toHaveBeenCalledWith(
557
+ "[assistant-ui] MCP App onError callback threw an error",
558
+ callbackError,
559
+ );
560
+ consoleError.mockRestore();
561
+ bridge.dispose();
562
+ });
563
+
564
+ it("preserves the onError receiver", () => {
565
+ const { frame } = makeFrame();
566
+ const handlers = {
567
+ calls: 0,
568
+ onError() {
569
+ expect(this).toBe(handlers);
570
+ this.calls += 1;
571
+ },
572
+ };
573
+ const bridge = createMcpAppBridge({ frame, handlers });
574
+
575
+ deliver(bridge, {
576
+ jsonrpc: "2.0",
577
+ method: "notifications/error",
578
+ params: { message: "kaboom" },
579
+ });
580
+
581
+ expect(handlers.calls).toBe(1);
582
+ bridge.dispose();
583
+ });
431
584
  });
@@ -1,4 +1,5 @@
1
1
  import type { SandboxHostFrame } from "../sandbox-host/SandboxHost";
2
+ import { invokeCallbackSafely } from "../utils/invokeCallbackSafely";
2
3
  import {
3
4
  MCP_APP_PROTOCOL_VERSION,
4
5
  type McpAppBridgeHandlers,
@@ -123,6 +124,10 @@ export function createMcpAppBridge(
123
124
  });
124
125
  };
125
126
 
127
+ const reportError = (error: Error) => {
128
+ invokeCallbackSafely(() => handlers.onError?.(error), "MCP App onError");
129
+ };
130
+
126
131
  const handleRequest = async (req: McpAppJsonRpcRequest) => {
127
132
  try {
128
133
  const params = req.params;
@@ -376,42 +381,49 @@ export function createMcpAppBridge(
376
381
  }
377
382
  } catch (err) {
378
383
  const error = err instanceof Error ? err : new Error(String(err));
379
- handlers.onError?.(error);
384
+ reportError(error);
380
385
  errorResponse(req.id, JSONRPC_ERROR.internalError, error.message);
381
386
  }
382
387
  };
383
388
 
384
389
  const handleNotification = (note: McpAppJsonRpcNotification) => {
385
- switch (normalizeMethod(note.method)) {
386
- case "notifications/initialized": {
387
- handlers.onInitialized?.();
388
- return;
389
- }
390
- case "notifications/size_changed": {
391
- const p = (note.params ?? {}) as { width?: number; height?: number };
392
- handlers.onSizeChange?.({
393
- ...(typeof p.width === "number" ? { width: p.width } : {}),
394
- ...(typeof p.height === "number" ? { height: p.height } : {}),
395
- });
396
- return;
397
- }
398
- case "notifications/log": {
399
- handlers.onLog?.(note.params);
400
- return;
401
- }
402
- case "notifications/request_teardown": {
403
- handlers.onRequestTeardown?.(note.params);
404
- return;
405
- }
406
- case "notifications/error": {
407
- const p = (note.params ?? {}) as { message?: string };
408
- handlers.onError?.(
409
- new Error(typeof p.message === "string" ? p.message : "Widget error"),
410
- );
411
- return;
390
+ try {
391
+ switch (normalizeMethod(note.method)) {
392
+ case "notifications/initialized": {
393
+ handlers.onInitialized?.();
394
+ return;
395
+ }
396
+ case "notifications/size_changed": {
397
+ const p = (note.params ?? {}) as { width?: number; height?: number };
398
+ handlers.onSizeChange?.({
399
+ ...(typeof p.width === "number" ? { width: p.width } : {}),
400
+ ...(typeof p.height === "number" ? { height: p.height } : {}),
401
+ });
402
+ return;
403
+ }
404
+ case "notifications/log": {
405
+ handlers.onLog?.(note.params);
406
+ return;
407
+ }
408
+ case "notifications/request_teardown": {
409
+ handlers.onRequestTeardown?.(note.params);
410
+ return;
411
+ }
412
+ case "notifications/error": {
413
+ const p = (note.params ?? {}) as { message?: string };
414
+ reportError(
415
+ new Error(
416
+ typeof p.message === "string" ? p.message : "Widget error",
417
+ ),
418
+ );
419
+ return;
420
+ }
421
+ default:
422
+ return;
412
423
  }
413
- default:
414
- return;
424
+ } catch (err) {
425
+ const error = err instanceof Error ? err : new Error(String(err));
426
+ reportError(error);
415
427
  }
416
428
  };
417
429
 
@@ -88,7 +88,7 @@ All messages are wrapped with a channel identifier to avoid conflicts with other
88
88
 
89
89
  #### Security Considerations
90
90
 
91
- 1. **Origin Validation**: Both sides can specify `targetOrigin` to restrict message sources
91
+ 1. **Origin Validation**: Both sides can specify `targetOrigin` to restrict message sources; providers share a fail-closed origin policy
92
92
  2. **Window Reference**: Host (parent) only accepts messages from the specific iframe window it's connected to
93
93
  3. **Message Channel**: Using a unique channel identifier prevents cross-talk with other postMessage users
94
94
 
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { act } from "react";
5
+ import { createRoot, type Root } from "react-dom/client";
6
+ import type * as AssistantStore from "@assistant-ui/store";
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
+ import { ActionBarPrimitiveExportMarkdown } from "./ActionBarExportMarkdown";
9
+
10
+ const { getCopyText } = vi.hoisted(() => ({
11
+ getCopyText: vi.fn(() => "# Exported message"),
12
+ }));
13
+
14
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
15
+
16
+ vi.mock("@assistant-ui/store", async (importOriginal) => {
17
+ const actual = await importOriginal<typeof AssistantStore>();
18
+ return {
19
+ ...actual,
20
+ useAui: () => ({ message: { getCopyText } }),
21
+ useAuiState: () => true,
22
+ };
23
+ });
24
+
25
+ describe("ActionBarPrimitiveExportMarkdown", () => {
26
+ let container: HTMLDivElement;
27
+ let root: Root;
28
+
29
+ beforeEach(() => {
30
+ getCopyText.mockClear();
31
+ container = document.createElement("div");
32
+ document.body.appendChild(container);
33
+ root = createRoot(container);
34
+ });
35
+
36
+ afterEach(async () => {
37
+ await act(async () => {
38
+ root.unmount();
39
+ });
40
+ container.remove();
41
+ vi.restoreAllMocks();
42
+ });
43
+
44
+ it("handles rejected asynchronous exports", async () => {
45
+ const error = new Error("remote save failed");
46
+ const onExport = vi.fn().mockRejectedValue(error);
47
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
48
+
49
+ await act(async () => {
50
+ root.render(
51
+ <ActionBarPrimitiveExportMarkdown onExport={onExport}>
52
+ Export
53
+ </ActionBarPrimitiveExportMarkdown>,
54
+ );
55
+ });
56
+
57
+ await act(async () => {
58
+ container.querySelector("button")!.click();
59
+ });
60
+
61
+ expect(onExport).toHaveBeenCalledWith("# Exported message");
62
+ expect(errorSpy).toHaveBeenCalledWith(
63
+ "[assistant-ui] markdown export failed:",
64
+ error,
65
+ );
66
+ });
67
+ });
@@ -61,7 +61,9 @@ export const ActionBarPrimitiveExportMarkdown = forwardRef<
61
61
  ref={forwardedRef}
62
62
  disabled={disabled || !callback}
63
63
  onClick={composeEventHandlers(onClick, () => {
64
- callback?.();
64
+ void callback?.().catch((error: unknown) => {
65
+ console.error("[assistant-ui] markdown export failed:", error);
66
+ });
65
67
  })}
66
68
  />
67
69
  );
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { forwardRef } from "react";
4
4
  import type { ActionButtonProps } from "../../utils/createActionButton";
5
- import { useEscapeKeydown } from "@radix-ui/react-use-escape-keydown";
6
5
  import { Primitive } from "../../utils/Primitive";
7
6
  import { composeEventHandlers } from "@radix-ui/primitive";
8
7
  import { useActionBarStopSpeaking as useActionBarStopSpeakingBehavior } from "@assistant-ui/core/react";
@@ -24,14 +23,6 @@ export const ActionBarPrimitiveStopSpeaking = forwardRef<
24
23
  >((props, ref) => {
25
24
  const callback = useActionBarStopSpeaking();
26
25
 
27
- // TODO this stops working if the user is not hovering over an older message
28
- useEscapeKeydown((e) => {
29
- if (callback) {
30
- e.preventDefault();
31
- callback();
32
- }
33
- });
34
-
35
26
  return (
36
27
  <Primitive.button
37
28
  type="button"
@@ -6,7 +6,7 @@ import {
6
6
  forwardRef,
7
7
  type ComponentPropsWithoutRef,
8
8
  } from "react";
9
- import { MessagePrimitiveIf as If } from "../message/MessageIf";
9
+ import { AuiIf } from "@assistant-ui/store";
10
10
 
11
11
  export namespace BranchPickerPrimitiveRoot {
12
12
  export type Element = ComponentRef<typeof Primitive.div>;
@@ -42,9 +42,11 @@ export const BranchPickerPrimitiveRoot = forwardRef<
42
42
  BranchPickerPrimitiveRoot.Props
43
43
  >(({ hideWhenSingleBranch, ...rest }, ref) => {
44
44
  return (
45
- <If hasBranches={hideWhenSingleBranch ? true : undefined}>
45
+ <AuiIf
46
+ condition={(s) => !hideWhenSingleBranch || s.message.branchCount >= 2}
47
+ >
46
48
  <Primitive.div {...rest} ref={ref} />
47
- </If>
49
+ </AuiIf>
48
50
  );
49
51
  });
50
52
 
@@ -21,11 +21,11 @@ export namespace ComposerPrimitiveDictationTranscript {
21
21
  *
22
22
  * @example
23
23
  * ```tsx
24
- * <ComposerPrimitive.If dictation>
24
+ * <AuiIf condition={(s) => s.composer.dictation != null}>
25
25
  * <div className="dictation-preview">
26
26
  * <ComposerPrimitive.DictationTranscript />
27
27
  * </div>
28
- * </ComposerPrimitive.If>
28
+ * </AuiIf>
29
29
  * ```
30
30
  */
31
31
  export const ComposerPrimitiveDictationTranscript = forwardRef<
@@ -346,7 +346,7 @@ export const ComposerPrimitiveInput = forwardRef<
346
346
  )
347
347
  return undefined;
348
348
 
349
- return aui.on("threadListItem.switchedTo", focus);
349
+ return aui.on("threads.selectionChanged", focus);
350
350
  }, [unstable_focusOnThreadSwitched, focus, aui]);
351
351
 
352
352
  useEffect(() => {
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createTapRoot, flushTapSync, useResource } from "@assistant-ui/tap";
3
+ import type {
4
+ Unstable_DirectiveFormatter,
5
+ Unstable_TriggerItem,
6
+ } from "@assistant-ui/core";
7
+ import type { AssistantClient } from "@assistant-ui/store";
8
+ import { TriggerDetectionResource } from "./triggerDetectionResource";
9
+ import {
10
+ TriggerSelectionResource,
11
+ type TriggerBehavior,
12
+ } from "./triggerSelectionResource";
13
+
14
+ const item: Unstable_TriggerItem = {
15
+ id: "file",
16
+ type: "file",
17
+ label: "file.txt",
18
+ };
19
+
20
+ const formatter: Unstable_DirectiveFormatter = {
21
+ serialize: () => "@file.txt",
22
+ parse: () => [],
23
+ };
24
+
25
+ const makeAui = (textRef: { value: string }) => {
26
+ const setText = vi.fn((value: string) => {
27
+ textRef.value = value;
28
+ });
29
+
30
+ return {
31
+ aui: {
32
+ composer: {
33
+ getState: () => ({ text: textRef.value }),
34
+ setText,
35
+ },
36
+ } as unknown as AssistantClient,
37
+ setText,
38
+ };
39
+ };
40
+
41
+ const render = ({
42
+ behavior,
43
+ text,
44
+ trigger,
45
+ }: {
46
+ behavior: TriggerBehavior;
47
+ text: string;
48
+ trigger: { offset: number; query: string };
49
+ }) => {
50
+ const textRef = { value: text };
51
+ const { aui, setText } = makeAui(textRef);
52
+ const setCursorPosition = vi.fn();
53
+ const onSelected = vi.fn();
54
+ const root = createTapRoot(function Root() {
55
+ return useResource(
56
+ TriggerSelectionResource({
57
+ behavior,
58
+ trigger,
59
+ aui,
60
+ triggerChar: "@",
61
+ setCursorPosition,
62
+ onSelected,
63
+ }),
64
+ );
65
+ });
66
+
67
+ return { root, setText, setCursorPosition, onSelected };
68
+ };
69
+
70
+ describe("TriggerSelectionResource", () => {
71
+ it("resynchronizes the cursor after inserting a directive", () => {
72
+ const { root, setText, setCursorPosition, onSelected } = render({
73
+ behavior: { kind: "directive", formatter },
74
+ text: "@file tail",
75
+ trigger: { offset: 0, query: "file" },
76
+ });
77
+
78
+ root.getValue().selectItem(item);
79
+
80
+ expect(setText).toHaveBeenCalledWith("@file.txt tail");
81
+ expect(setCursorPosition).toHaveBeenCalledWith("@file.txt".length + 1);
82
+ expect(onSelected).toHaveBeenCalledOnce();
83
+ });
84
+
85
+ it("resynchronizes the cursor after removing an action", () => {
86
+ const { root, setText, setCursorPosition } = render({
87
+ behavior: {
88
+ kind: "action",
89
+ formatter,
90
+ onExecute: vi.fn(),
91
+ removeOnExecute: true,
92
+ },
93
+ text: "before @file tail",
94
+ trigger: { offset: 7, query: "file" },
95
+ });
96
+
97
+ root.getValue().selectItem(item);
98
+
99
+ expect(setText).toHaveBeenCalledWith("before tail");
100
+ expect(setCursorPosition).toHaveBeenCalledWith(7);
101
+ });
102
+
103
+ it("does not rewrite composer text on a second select after insertion", () => {
104
+ const textRef = { value: "@fil" };
105
+ const { aui, setText } = makeAui(textRef);
106
+ const onSelected = vi.fn();
107
+ const root = createTapRoot(function Root() {
108
+ const detection = useResource(
109
+ TriggerDetectionResource({
110
+ text: textRef.value,
111
+ triggerChar: "@",
112
+ }),
113
+ );
114
+ return useResource(
115
+ TriggerSelectionResource({
116
+ behavior: { kind: "directive", formatter },
117
+ trigger: detection.trigger,
118
+ aui,
119
+ triggerChar: "@",
120
+ setCursorPosition: detection.setCursorPosition,
121
+ onSelected,
122
+ }),
123
+ );
124
+ });
125
+
126
+ flushTapSync(() => {
127
+ root.getValue().selectItem(item);
128
+ });
129
+ root.getValue().selectItem(item);
130
+
131
+ expect(setText).toHaveBeenCalledOnce();
132
+ expect(setText).toHaveBeenCalledWith("@file.txt ");
133
+ expect(textRef.value).toBe("@file.txt ");
134
+ });
135
+ });
@@ -83,6 +83,7 @@ const useTriggerSelectionResource = ({
83
83
  aui.composer.setText(
84
84
  before + directive + (after.startsWith(" ") ? after : ` ${after}`),
85
85
  );
86
+ setCursorPosition(before.length + directive.length + 1);
86
87
  };
87
88
 
88
89
  if (behavior.kind === "directive") {
@@ -93,6 +94,7 @@ const useTriggerSelectionResource = ({
93
94
  aui.composer.setText(
94
95
  before + (after.startsWith(" ") ? after.slice(1) : after),
95
96
  );
97
+ setCursorPosition(before.length);
96
98
  } else {
97
99
  // Leave directive chip in the composer as an audit trail
98
100
  insertDirective();