@assistant-ui/react 0.15.15 → 0.15.17

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 (267) hide show
  1. package/README.md +3 -3
  2. package/dist/devtools/DevToolsHooks.d.ts +0 -2
  3. package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
  4. package/dist/devtools/DevToolsHooks.js +10 -12
  5. package/dist/devtools/DevToolsHooks.js.map +1 -1
  6. package/dist/index.d.ts +4 -3
  7. package/dist/index.js +2 -2
  8. package/dist/mcp-apps/McpAppRenderer.d.ts +6 -1
  9. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  10. package/dist/mcp-apps/McpAppRenderer.js +20 -13
  11. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  12. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  13. package/dist/mcp-apps/McpAppsRemoteHost.js +20 -17
  14. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  15. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  16. package/dist/mcp-apps/app-frame.js +189 -94
  17. package/dist/mcp-apps/app-frame.js.map +1 -1
  18. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  19. package/dist/mcp-apps/bridge.js +2 -8
  20. package/dist/mcp-apps/bridge.js.map +1 -1
  21. package/dist/mcp-apps/index.d.ts +2 -2
  22. package/dist/model-context/frame/useAssistantFrameHost.d.ts +1 -1
  23. package/dist/model-context/frame/useAssistantFrameHost.js +10 -11
  24. package/dist/model-context/frame/useAssistantFrameHost.js.map +1 -1
  25. package/dist/model-context/makeAssistantVisible.js +1 -1
  26. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  27. package/dist/primitives/actionBar/ActionBarCopy.js +3 -3
  28. package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
  29. package/dist/primitives/actionBar/ActionBarExportMarkdown.d.ts.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +7 -4
  31. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  32. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +3 -3
  33. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
  34. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +3 -3
  35. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
  36. package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
  37. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +3 -3
  38. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
  39. package/dist/primitives/actionBarMore/ActionBarMoreContent.js.map +1 -1
  40. package/dist/primitives/actionBarMore/ActionBarMoreItem.js.map +1 -1
  41. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js.map +1 -1
  42. package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js.map +1 -1
  43. package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js.map +1 -1
  44. package/dist/primitives/actionBarMore/scope.d.ts +2 -2
  45. package/dist/primitives/actionBarMore/scope.d.ts.map +1 -1
  46. package/dist/primitives/actionBarMore/scope.js.map +1 -1
  47. package/dist/primitives/assistantModal/AssistantModalAnchor.js.map +1 -1
  48. package/dist/primitives/assistantModal/AssistantModalContent.js +1 -1
  49. package/dist/primitives/assistantModal/AssistantModalContent.js.map +1 -1
  50. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  51. package/dist/primitives/assistantModal/AssistantModalTrigger.js.map +1 -1
  52. package/dist/primitives/assistantModal/scope.d.ts +2 -2
  53. package/dist/primitives/assistantModal/scope.d.ts.map +1 -1
  54. package/dist/primitives/assistantModal/scope.js.map +1 -1
  55. package/dist/primitives/branchPicker/BranchPickerRoot.d.ts.map +1 -1
  56. package/dist/primitives/branchPicker/BranchPickerRoot.js +20 -15
  57. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  58. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  59. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  60. package/dist/primitives/composer/ComposerDictationTranscript.d.ts +2 -2
  61. package/dist/primitives/composer/ComposerDictationTranscript.js +2 -2
  62. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  63. package/dist/primitives/composer/ComposerInput.js +1 -3
  64. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  65. package/dist/primitives/composer/ComposerQuote.js +5 -5
  66. package/dist/primitives/composer/ComposerQuote.js.map +1 -1
  67. package/dist/primitives/composer/ComposerRoot.js +3 -3
  68. package/dist/primitives/composer/ComposerRoot.js.map +1 -1
  69. package/dist/primitives/composer/trigger/TriggerPopover.d.ts +5 -0
  70. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  71. package/dist/primitives/composer/trigger/TriggerPopover.js +157 -130
  72. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  73. package/dist/primitives/composer/trigger/TriggerPopoverBack.js +3 -3
  74. package/dist/primitives/composer/trigger/TriggerPopoverBack.js.map +1 -1
  75. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +4 -4
  76. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  77. package/dist/primitives/composer/trigger/TriggerPopoverItems.js +4 -4
  78. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  79. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -0
  80. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  81. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +63 -61
  82. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
  83. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +2 -0
  84. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts.map +1 -1
  85. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  86. package/dist/primitives/composer/trigger/detectTrigger.d.ts +21 -1
  87. package/dist/primitives/composer/trigger/detectTrigger.d.ts.map +1 -0
  88. package/dist/primitives/composer/trigger/detectTrigger.js +17 -3
  89. package/dist/primitives/composer/trigger/detectTrigger.js.map +1 -1
  90. package/dist/primitives/composer/trigger/index.d.ts +3 -1
  91. package/dist/primitives/composer/trigger/index.d.ts.map +1 -1
  92. package/dist/primitives/composer/trigger/index.js.map +1 -1
  93. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +3 -4
  94. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  95. package/dist/primitives/composer/trigger/triggerDetectionResource.js +15 -14
  96. package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
  97. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  98. package/dist/primitives/composer/trigger/triggerSelectionResource.js +10 -7
  99. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  100. package/dist/primitives/composer.d.ts +3 -2
  101. package/dist/primitives/composer.js.map +1 -1
  102. package/dist/primitives/error/ErrorMessage.js.map +1 -1
  103. package/dist/primitives/message/MessageIf.js.map +1 -1
  104. package/dist/primitives/message/MessageParts.js.map +1 -1
  105. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  106. package/dist/primitives/message/MessageRoot.js +4 -4
  107. package/dist/primitives/message/MessageRoot.js.map +1 -1
  108. package/dist/primitives/message.js +1 -1
  109. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  110. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +3 -3
  111. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
  112. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +3 -3
  113. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  114. package/dist/primitives/thread/ThreadIf.js.map +1 -1
  115. package/dist/primitives/thread/ThreadRoot.d.ts +3 -0
  116. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  117. package/dist/primitives/thread/ThreadRoot.js +3 -0
  118. package/dist/primitives/thread/ThreadRoot.js.map +1 -1
  119. package/dist/primitives/thread/ThreadViewport.js +3 -3
  120. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  121. package/dist/primitives/thread/ThreadViewportFooter.js +3 -3
  122. package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
  123. package/dist/primitives/thread/useThreadViewportAutoScroll.js +1 -1
  124. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  125. package/dist/primitives/threadList/ThreadListNew.js +3 -3
  126. package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
  127. package/dist/primitives/threadListFocusGroup.d.ts +2 -1
  128. package/dist/primitives/threadListFocusGroup.d.ts.map +1 -1
  129. package/dist/primitives/threadListFocusGroup.js +2 -2
  130. package/dist/primitives/threadListFocusGroup.js.map +1 -1
  131. package/dist/primitives/threadListItem/ThreadListItemRoot.js +3 -3
  132. package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
  133. package/dist/primitives/threadListItem/ThreadListItemTrigger.js +1 -1
  134. package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
  135. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -1
  136. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
  137. package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js.map +1 -1
  138. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +1 -1
  139. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js.map +1 -1
  140. package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js.map +1 -1
  141. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -2
  142. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js.map +1 -1
  143. package/dist/primitives/threadListItemMore/scope.d.ts +2 -2
  144. package/dist/primitives/threadListItemMore/scope.d.ts.map +1 -1
  145. package/dist/primitives/threadListItemMore/scope.js.map +1 -1
  146. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  147. package/dist/sandbox-host/SandboxHost.js +138 -68
  148. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  149. package/dist/unstable/useComposerInputHistory.js +1 -1
  150. package/dist/unstable/useLiveCompletionAdapter.js +3 -1
  151. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  152. package/dist/unstable/useSlashCommandAdapter.js +41 -27
  153. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  154. package/dist/utils/Primitive.d.ts +2 -2
  155. package/dist/utils/Primitive.js +2 -2
  156. package/dist/utils/Primitive.js.map +1 -1
  157. package/dist/utils/createActionButton.js +3 -3
  158. package/dist/utils/createActionButton.js.map +1 -1
  159. package/dist/utils/hooks/useOnResizeContent.js +1 -1
  160. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  161. package/dist/utils/hooks/useOnScrollToBottom.js +1 -1
  162. package/dist/utils/hooks/useOnScrollToBottom.js.map +1 -1
  163. package/dist/utils/invokeCallbackSafely.d.ts +5 -0
  164. package/dist/utils/invokeCallbackSafely.d.ts.map +1 -0
  165. package/dist/utils/invokeCallbackSafely.js +15 -0
  166. package/dist/utils/invokeCallbackSafely.js.map +1 -0
  167. package/dist/utils/smooth/SmoothContext.d.ts +30 -6
  168. package/dist/utils/smooth/SmoothContext.d.ts.map +1 -1
  169. package/dist/utils/smooth/useSmooth.d.ts +3 -0
  170. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  171. package/dist/utils/smooth/useSmooth.js +17 -2
  172. package/dist/utils/smooth/useSmooth.js.map +1 -1
  173. package/dist/utils/useToolArgsFieldStatus.d.ts +5 -1
  174. package/package.json +12 -20
  175. package/src/devtools/DevToolsHooks.test.ts +40 -0
  176. package/src/devtools/DevToolsHooks.ts +10 -15
  177. package/src/index.ts +9 -1
  178. package/src/mcp-apps/McpAppRenderer.test.tsx +133 -4
  179. package/src/mcp-apps/McpAppRenderer.tsx +28 -21
  180. package/src/mcp-apps/McpAppsRemoteHost.concurrent.test.tsx +215 -0
  181. package/src/mcp-apps/McpAppsRemoteHost.ts +28 -12
  182. package/src/mcp-apps/app-frame.test.tsx +102 -0
  183. package/src/mcp-apps/app-frame.tsx +17 -4
  184. package/src/mcp-apps/bridge.test.ts +21 -0
  185. package/src/mcp-apps/bridge.ts +2 -14
  186. package/src/mcp-apps/index.ts +1 -0
  187. package/src/model-context/frame/AssistantFrame.test.ts +3 -3
  188. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
  189. package/src/model-context/frame/useAssistantFrameHost.ts +2 -2
  190. package/src/model-context/makeAssistantVisible.tsx +1 -1
  191. package/src/primitives/actionBar/ActionBarCopy.tsx +1 -1
  192. package/src/primitives/actionBar/ActionBarExportMarkdown.test.tsx +67 -0
  193. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +4 -2
  194. package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +1 -1
  195. package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +1 -1
  196. package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +1 -1
  197. package/src/primitives/actionBarMore/scope.ts +2 -2
  198. package/src/primitives/assistantModal/AssistantModalContent.tsx +1 -1
  199. package/src/primitives/assistantModal/scope.ts +2 -2
  200. package/src/primitives/branchPicker/BranchPickerRoot.tsx +5 -3
  201. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +1 -1
  202. package/src/primitives/composer/ComposerDictationTranscript.tsx +2 -2
  203. package/src/primitives/composer/ComposerInput.test.tsx +2 -1
  204. package/src/primitives/composer/ComposerInput.tsx +3 -3
  205. package/src/primitives/composer/ComposerQuote.tsx +1 -1
  206. package/src/primitives/composer/ComposerRoot.tsx +1 -1
  207. package/src/primitives/composer/trigger/TriggerPopover.tsx +16 -1
  208. package/src/primitives/composer/trigger/TriggerPopoverBack.tsx +1 -1
  209. package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +1 -1
  210. package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +1 -1
  211. package/src/primitives/composer/trigger/TriggerPopoverResource.ts +4 -1
  212. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +2 -0
  213. package/src/primitives/composer/trigger/detectTrigger.test.ts +88 -5
  214. package/src/primitives/composer/trigger/detectTrigger.ts +50 -9
  215. package/src/primitives/composer/trigger/index.ts +1 -0
  216. package/src/primitives/composer/trigger/triggerDetectionResource.ts +10 -7
  217. package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +147 -0
  218. package/src/primitives/composer/trigger/triggerSelectionResource.ts +3 -4
  219. package/src/primitives/composer.ts +1 -0
  220. package/src/primitives/message/MessageRoot.tsx +1 -1
  221. package/src/primitives/selectionToolbar/SelectionToolbarQuote.tsx +1 -1
  222. package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +1 -1
  223. package/src/primitives/thread/ThreadRoot.tsx +3 -0
  224. package/src/primitives/thread/ThreadViewport.tsx +1 -1
  225. package/src/primitives/thread/ThreadViewportFooter.tsx +1 -1
  226. package/src/primitives/thread/useThreadViewportAutoScroll.ts +1 -1
  227. package/src/primitives/threadList/ThreadListNew.tsx +1 -1
  228. package/src/primitives/threadListFocusGroup.ts +2 -2
  229. package/src/primitives/threadListItem/ThreadListItemRoot.tsx +1 -1
  230. package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +1 -1
  231. package/src/primitives/threadListItemMore/ThreadListItemMoreContent.tsx +1 -1
  232. package/src/primitives/threadListItemMore/ThreadListItemMoreRoot.tsx +1 -1
  233. package/src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx +2 -2
  234. package/src/primitives/threadListItemMore/scope.ts +2 -2
  235. package/src/sandbox-host/SandboxHost.test.tsx +106 -1
  236. package/src/sandbox-host/SandboxHost.tsx +22 -5
  237. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +31 -1
  238. package/src/unstable/useLiveCompletionAdapter.test.tsx +117 -1
  239. package/src/unstable/useLiveCompletionAdapter.ts +4 -1
  240. package/src/unstable/useSlashCommandAdapter.test.tsx +141 -0
  241. package/src/unstable/useSlashCommandAdapter.ts +52 -17
  242. package/src/utils/Primitive.tsx +2 -2
  243. package/src/utils/createActionButton.tsx +1 -1
  244. package/src/utils/hooks/useOnResizeContent.ts +1 -1
  245. package/src/utils/hooks/useOnScrollToBottom.ts +1 -1
  246. package/src/utils/invokeCallbackSafely.ts +11 -0
  247. package/src/utils/smooth/useSmooth.test.tsx +110 -0
  248. package/src/utils/smooth/useSmooth.ts +21 -7
  249. package/dist/context/react/utils/ensureBinding.d.ts +0 -5
  250. package/dist/context/react/utils/ensureBinding.d.ts.map +0 -1
  251. package/dist/context/react/utils/ensureBinding.js +0 -24
  252. package/dist/context/react/utils/ensureBinding.js.map +0 -1
  253. package/dist/context/react/utils/useRuntimeState.d.ts +0 -13
  254. package/dist/context/react/utils/useRuntimeState.d.ts.map +0 -1
  255. package/dist/context/react/utils/useRuntimeState.js +0 -33
  256. package/dist/context/react/utils/useRuntimeState.js.map +0 -1
  257. package/dist/legacy-runtime/runtime/MessagePartRuntime.d.ts +0 -3
  258. package/dist/legacy-runtime/runtime/MessagePartRuntime.js +0 -2
  259. package/dist/legacy-runtime/runtime/MessageRuntime.d.ts +0 -3
  260. package/dist/legacy-runtime/runtime/MessageRuntime.js +0 -2
  261. package/dist/legacy-runtime/runtime/ThreadListRuntime.d.ts +0 -3
  262. package/dist/legacy-runtime/runtime/ThreadListRuntime.js +0 -2
  263. package/src/context/react/utils/ensureBinding.ts +0 -46
  264. package/src/context/react/utils/useRuntimeState.ts +0 -46
  265. package/src/legacy-runtime/runtime/MessagePartRuntime.ts +0 -2
  266. package/src/legacy-runtime/runtime/MessageRuntime.ts +0 -3
  267. package/src/legacy-runtime/runtime/ThreadListRuntime.ts +0 -4
@@ -0,0 +1,40 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { DevToolsHooks, DevToolsProviderApi } from "./DevToolsHooks";
3
+
4
+ describe("DevToolsHooks", () => {
5
+ afterEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ it("notifies every listener when one throws", () => {
10
+ const error = new Error("listener failed");
11
+ const consoleError = vi
12
+ .spyOn(console, "error")
13
+ .mockImplementation(() => {});
14
+ const laterListener = vi.fn();
15
+ const unsubscribeThrowing = DevToolsHooks.subscribe(() => {
16
+ throw error;
17
+ });
18
+ const unsubscribeLater = DevToolsHooks.subscribe(laterListener);
19
+ let unregister = () => {};
20
+
21
+ try {
22
+ unregister = DevToolsProviderApi.register({});
23
+ const apiId = [...DevToolsHooks.getApis().keys()].at(-1);
24
+ expect(apiId).toBeDefined();
25
+
26
+ DevToolsHooks.clearEventLogs(apiId!);
27
+
28
+ expect(laterListener).toHaveBeenCalledTimes(2);
29
+ expect(consoleError).toHaveBeenCalledTimes(2);
30
+ expect(consoleError).toHaveBeenCalledWith(
31
+ "[assistant-ui] DevTools listener threw an error",
32
+ error,
33
+ );
34
+ } finally {
35
+ unsubscribeThrowing();
36
+ unsubscribeLater();
37
+ unregister();
38
+ }
39
+ });
40
+ });
@@ -1,4 +1,5 @@
1
1
  import type { Unsubscribe } from "@assistant-ui/core";
2
+ import { notifyEventListeners } from "@assistant-ui/core/internal";
2
3
  import type { AssistantClient } from "@assistant-ui/store";
3
4
 
4
5
  export interface EventLog {
@@ -54,6 +55,10 @@ const getHook = (): DevToolsHook => {
54
55
  return newHook;
55
56
  };
56
57
 
58
+ const notifyListeners = (apiId: number): void => {
59
+ notifyEventListeners(getHook().listeners, apiId, "DevTools");
60
+ };
61
+
57
62
  export class DevToolsHooks {
58
63
  static subscribe(listener: () => void): Unsubscribe {
59
64
  const hook = getHook();
@@ -69,17 +74,12 @@ export class DevToolsHooks {
69
74
  if (!entry) return;
70
75
 
71
76
  entry.logs = [];
72
- DevToolsHooks.notifyListeners(apiId);
77
+ notifyListeners(apiId);
73
78
  }
74
79
 
75
80
  static getApis(): Map<number, DevToolsApiEntry> {
76
81
  return getHook().apis;
77
82
  }
78
-
79
- private static notifyListeners(apiId: number): void {
80
- const hook = getHook();
81
- hook.listeners.forEach((listener) => listener(apiId));
82
- }
83
83
  }
84
84
 
85
85
  export class DevToolsProviderApi {
@@ -116,15 +116,15 @@ export class DevToolsProviderApi {
116
116
  );
117
117
  }
118
118
 
119
- DevToolsProviderApi.notifyListeners(apiId);
119
+ notifyListeners(apiId);
120
120
  });
121
121
 
122
122
  const stateUnsubscribe = aui.subscribe?.(() => {
123
- DevToolsProviderApi.notifyListeners(apiId);
123
+ notifyListeners(apiId);
124
124
  });
125
125
 
126
126
  hook.apis.set(apiId, entry);
127
- DevToolsProviderApi.notifyListeners(apiId);
127
+ notifyListeners(apiId);
128
128
 
129
129
  return () => {
130
130
  const hook = getHook();
@@ -136,12 +136,7 @@ export class DevToolsProviderApi {
136
136
 
137
137
  hook.apis.delete(apiId);
138
138
 
139
- DevToolsProviderApi.notifyListeners(apiId);
139
+ notifyListeners(apiId);
140
140
  };
141
141
  }
142
-
143
- private static notifyListeners(apiId: number): void {
144
- const hook = getHook();
145
- hook.listeners.forEach((listener) => listener(apiId));
146
- }
147
142
  }
package/src/index.ts CHANGED
@@ -83,7 +83,12 @@ export {
83
83
  } from "@assistant-ui/core";
84
84
 
85
85
  // --- adapters/suggestion ---
86
- export type { SuggestionAdapter } from "@assistant-ui/core";
86
+ export type {
87
+ SuggestionAdapter,
88
+ SuggestionAdapterGenerateOptions,
89
+ CreateSuggestionAdapterOptions,
90
+ } from "@assistant-ui/core";
91
+ export { createSuggestionAdapter } from "@assistant-ui/core";
87
92
 
88
93
  // --- adapters/RuntimeAdapterProvider ---
89
94
  export {
@@ -475,6 +480,8 @@ export {
475
480
  useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers,
476
481
  useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional,
477
482
  type RegisteredTrigger as Unstable_RegisteredTrigger,
483
+ type TriggerMatch as Unstable_TriggerMatch,
484
+ type TriggerMatcher as Unstable_TriggerMatcher,
478
485
  type TriggerBehavior as Unstable_TriggerBehavior,
479
486
  } from "./primitives/composer/trigger";
480
487
  export type {
@@ -520,6 +527,7 @@ export type {
520
527
  McpAppsHost,
521
528
  McpAppsRemoteHostOptions,
522
529
  McpAppToolCallParams,
530
+ McpAppBridgeHandlers,
523
531
  ToolCallMessagePartMcpMetadata,
524
532
  } from "./mcp-apps";
525
533
  export type { McpAppResourceOutput } from "@assistant-ui/core/react";
@@ -1,7 +1,7 @@
1
1
  // @vitest-environment jsdom
2
2
  import { render, waitFor } from "@testing-library/react";
3
3
  import { resource, useResource, withKey } from "@assistant-ui/tap";
4
- import { memo } from "react";
4
+ import { act, memo, startTransition, Suspense } from "react";
5
5
  import type {
6
6
  ToolCallMessagePartComponent,
7
7
  ToolCallMessagePartProps,
@@ -29,7 +29,7 @@ vi.mock("./app-frame", () => ({
29
29
  },
30
30
  }));
31
31
 
32
- import { McpAppRenderer } from "./McpAppRenderer";
32
+ import { McpAppRenderer, type McpAppRendererOptions } from "./McpAppRenderer";
33
33
  import { McpAppsRemoteHost } from "./McpAppsRemoteHost";
34
34
 
35
35
  const useHost = ({ host }: { host: McpAppsHost }) => host;
@@ -61,14 +61,31 @@ const createPart = (serverId?: string): ToolCallMessagePartProps => ({
61
61
  respondToApproval: vi.fn(),
62
62
  });
63
63
 
64
- function Harness({ host, serverId }: { host: McpAppsHost; serverId?: string }) {
64
+ const createPartWithoutApp = (): ToolCallMessagePartProps => {
65
+ const part = createPart();
66
+ delete part.mcp;
67
+ return part;
68
+ };
69
+
70
+ function Harness({
71
+ host,
72
+ serverId,
73
+ handlers,
74
+ part,
75
+ }: {
76
+ host: McpAppsHost;
77
+ serverId?: string;
78
+ handlers?: McpAppRendererOptions["handlers"];
79
+ part?: ToolCallMessagePartProps;
80
+ }) {
65
81
  const renderer = useResource(
66
82
  McpAppRenderer({
67
83
  host: Host({ host }),
84
+ ...(handlers === undefined ? {} : { handlers }),
68
85
  }),
69
86
  );
70
87
  const Renderer = renderer.render;
71
- return <Renderer {...createPart(serverId)} />;
88
+ return <Renderer {...(part ?? createPart(serverId))} />;
72
89
  }
73
90
 
74
91
  const MemoizedPart = memo(function MemoizedPart({
@@ -142,6 +159,64 @@ describe("McpAppRenderer", () => {
142
159
  });
143
160
  });
144
161
 
162
+ it("keeps bridge server ids scoped to committed renders", async () => {
163
+ const host: McpAppsHost = {
164
+ loadResource: vi.fn(async ({ uri }) => ({
165
+ uri,
166
+ mimeType: "text/html;profile=mcp-app" as const,
167
+ html: "",
168
+ })),
169
+ callTool: vi.fn(),
170
+ readResource: vi.fn(),
171
+ listResources: vi.fn(),
172
+ };
173
+ const interruptedRender = vi.fn();
174
+ const pending = new Promise<never>(() => {});
175
+ const Blocker = ({ blocked }: { blocked: boolean }) => {
176
+ if (blocked) {
177
+ interruptedRender();
178
+ throw pending;
179
+ }
180
+ return null;
181
+ };
182
+ const view = (part: ToolCallMessagePartProps, blocked: boolean) => (
183
+ <Suspense fallback={null}>
184
+ <Harness host={host} part={part} />
185
+ <Blocker blocked={blocked} />
186
+ </Suspense>
187
+ );
188
+ const rendered = render(view(createPart("server-a"), false));
189
+ await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
190
+ const handlers = framePropsMock.mock.lastCall?.[0]
191
+ .handlers as McpAppBridgeHandlers;
192
+
193
+ act(() => {
194
+ startTransition(() =>
195
+ rendered.rerender(view(createPart("server-b"), true)),
196
+ );
197
+ });
198
+ expect(interruptedRender).toHaveBeenCalled();
199
+ await handlers.callTool?.({ name: "search" });
200
+
201
+ expect(host.callTool).toHaveBeenCalledWith({
202
+ name: "search",
203
+ serverId: "server-a",
204
+ });
205
+
206
+ framePropsMock.mockClear();
207
+ vi.mocked(host.callTool).mockClear();
208
+ rendered.rerender(view(createPartWithoutApp(), false));
209
+ await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
210
+ expect(framePropsMock.mock.lastCall?.[0].app.serverId).toBe("server-a");
211
+ const fallbackHandlers = framePropsMock.mock.lastCall?.[0]
212
+ .handlers as McpAppBridgeHandlers;
213
+ await fallbackHandlers.callTool?.({ name: "search" });
214
+ expect(host.callTool).toHaveBeenCalledWith({
215
+ name: "search",
216
+ serverId: "server-a",
217
+ });
218
+ });
219
+
145
220
  it("reloads the resource and hides stale HTML when the host changes", async () => {
146
221
  const nextResource = createDeferred<{
147
222
  uri: string;
@@ -397,4 +472,58 @@ describe("McpAppRenderer", () => {
397
472
  await handlers.listResources?.(params);
398
473
  expect(listResources).toHaveBeenCalledWith(params);
399
474
  });
475
+
476
+ it("uses caller UI handlers and keeps data-plane handlers on the host", async () => {
477
+ const host: McpAppsHost = {
478
+ loadResource: vi.fn(async ({ uri }) => ({
479
+ uri,
480
+ mimeType: "text/html;profile=mcp-app" as const,
481
+ html: "",
482
+ })),
483
+ callTool: vi.fn(),
484
+ readResource: vi.fn(),
485
+ listResources: vi.fn(),
486
+ };
487
+ const requestDisplayMode = vi.fn(({ mode }) => ({ mode }));
488
+ const updateModelContext = vi.fn();
489
+ const openLink = vi.fn();
490
+ const sendMessage = vi.fn();
491
+ const onInitialized = vi.fn();
492
+
493
+ render(
494
+ <Harness
495
+ host={host}
496
+ handlers={{
497
+ requestDisplayMode,
498
+ updateModelContext,
499
+ openLink,
500
+ sendMessage,
501
+ onInitialized,
502
+ }}
503
+ />,
504
+ );
505
+ await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
506
+ const handlers = framePropsMock.mock.lastCall?.[0]
507
+ .handlers as McpAppBridgeHandlers;
508
+
509
+ expect(await handlers.requestDisplayMode?.({ mode: "fullscreen" })).toEqual(
510
+ { mode: "fullscreen" },
511
+ );
512
+ await handlers.updateModelContext?.({ text: "context" });
513
+ await handlers.openLink?.({ url: "https://example.com" });
514
+ await handlers.sendMessage?.({ text: "hello" });
515
+ handlers.onInitialized?.();
516
+ await handlers.callTool?.({ name: "search" });
517
+ await handlers.readResource?.({ uri: "ui://resource" });
518
+ await handlers.listResources?.();
519
+
520
+ expect(requestDisplayMode).toHaveBeenCalledWith({ mode: "fullscreen" });
521
+ expect(updateModelContext).toHaveBeenCalledWith({ text: "context" });
522
+ expect(openLink).toHaveBeenCalledWith({ url: "https://example.com" });
523
+ expect(sendMessage).toHaveBeenCalledWith({ text: "hello" });
524
+ expect(onInitialized).toHaveBeenCalledOnce();
525
+ expect(host.callTool).toHaveBeenCalledWith({ name: "search" });
526
+ expect(host.readResource).toHaveBeenCalledWith({ uri: "ui://resource" });
527
+ expect(host.listResources).toHaveBeenCalledWith(undefined);
528
+ });
400
529
  });
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {
4
4
  useEffect,
5
+ useLayoutEffect,
5
6
  useMemo,
6
7
  useRef,
7
8
  useState,
@@ -47,6 +48,14 @@ export type McpAppRendererOptions = {
47
48
  hostInfo?: McpAppHostInfo;
48
49
  /** Delivered to the widget on initialize and pushed via `notifications/host_context/changed` on change. */
49
50
  hostContext?: McpAppHostContext;
51
+ /**
52
+ * Optional widget interaction and lifecycle handlers. Data-plane handlers
53
+ * (`callTool`, `readResource`, `listResources`) always use `host`.
54
+ */
55
+ handlers?: Omit<
56
+ McpAppBridgeHandlers,
57
+ "callTool" | "readResource" | "listResources"
58
+ >;
50
59
  /** Rendered when no MCP app is on the part, or while load is in flight / failed (unless overridden). */
51
60
  fallback?: ReactNode;
52
61
  /** Rendered while the resource is loading. Defaults to `fallback`. */
@@ -106,13 +115,9 @@ function InlineRenderer({
106
115
  const aui = useAui();
107
116
  const app = getMcpAppFromToolPart(part);
108
117
  const cachedAppRef = useRef<McpAppMetadata | undefined>(undefined);
109
- if (
110
- app != null &&
111
- (cachedAppRef.current?.resourceUri !== app.resourceUri ||
112
- cachedAppRef.current?.serverId !== app.serverId)
113
- ) {
114
- cachedAppRef.current = app;
115
- }
118
+ useLayoutEffect(() => {
119
+ if (app != null) cachedAppRef.current = app;
120
+ }, [app]);
116
121
  const appForRender = app ?? cachedAppRef.current;
117
122
 
118
123
  const [loadedResource, setLoadedResource] = useState<LoadedResourceState>();
@@ -120,8 +125,7 @@ function InlineRenderer({
120
125
  const host = useHostStore((state) => state.host);
121
126
  const resourceUri = appForRender?.resourceUri;
122
127
  const serverId = appForRender?.serverId;
123
- const serverIdRef = useRef<string | undefined>(undefined);
124
- serverIdRef.current = serverId;
128
+ const callerHandlers = opts.handlers;
125
129
  useEffect(() => {
126
130
  if (resourceUri == null) return;
127
131
  let cancelled = false;
@@ -165,34 +169,37 @@ function InlineRenderer({
165
169
 
166
170
  const bridgeHandlers = useMemo<McpAppBridgeHandlers>(
167
171
  () => ({
168
- openLink: defaultOpenLink,
169
- sendMessage: (params) => {
170
- const text = extractSendMessageText(params);
171
- if (!text) return { ok: false, reason: "unrecognised params shape" };
172
- aui.thread.append({ content: [{ type: "text", text }] });
173
- return { ok: true };
174
- },
172
+ ...callerHandlers,
173
+ openLink: callerHandlers?.openLink ?? defaultOpenLink,
174
+ sendMessage:
175
+ callerHandlers?.sendMessage ??
176
+ ((params) => {
177
+ const text = extractSendMessageText(params);
178
+ if (!text) return { ok: false, reason: "unrecognised params shape" };
179
+ aui.thread.append({ content: [{ type: "text", text }] });
180
+ return { ok: true };
181
+ }),
175
182
  callTool: (params) =>
176
183
  useHostStore.getState().host.callTool({
177
184
  ...params,
178
- ...(serverIdRef.current ? { serverId: serverIdRef.current } : {}),
185
+ ...(serverId ? { serverId } : {}),
179
186
  }),
180
187
  readResource: (params) =>
181
188
  useHostStore.getState().host.readResource({
182
189
  ...params,
183
- ...(serverIdRef.current ? { serverId: serverIdRef.current } : {}),
190
+ ...(serverId ? { serverId } : {}),
184
191
  }),
185
192
  listResources: (params) => {
186
- if (!serverIdRef.current) {
193
+ if (!serverId) {
187
194
  return useHostStore.getState().host.listResources(params);
188
195
  }
189
196
  return useHostStore.getState().host.listResources({
190
197
  ...(isRecord(params) ? params : {}),
191
- serverId: serverIdRef.current,
198
+ serverId,
192
199
  });
193
200
  },
194
201
  }),
195
- [aui, useHostStore],
202
+ [aui, callerHandlers, serverId, useHostStore],
196
203
  );
197
204
 
198
205
  const loadedResourceForApp =
@@ -0,0 +1,215 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, render, waitFor } from "@testing-library/react";
4
+ import { useResource } from "@assistant-ui/tap";
5
+ import {
6
+ startTransition,
7
+ Suspense,
8
+ useLayoutEffect,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { describe, expect, it, vi } from "vitest";
12
+ import type { McpAppsHost } from "./types";
13
+ import { McpAppsRemoteHost } from "./McpAppsRemoteHost";
14
+
15
+ describe("McpAppsRemoteHost concurrent rendering", () => {
16
+ it("keeps committed authorization options during an interrupted render", async () => {
17
+ const pending = new Promise<never>(() => {});
18
+ const fetch = vi.fn(async () => Response.json({ content: [] }));
19
+ let committedHost: McpAppsHost | undefined;
20
+
21
+ const Probe = ({
22
+ authorization,
23
+ suspend,
24
+ children,
25
+ }: {
26
+ authorization: string;
27
+ suspend: boolean;
28
+ children: ReactNode;
29
+ }) => {
30
+ const host = useResource(
31
+ McpAppsRemoteHost({
32
+ url: "/api/mcp-apps",
33
+ fetch,
34
+ headers: { authorization },
35
+ }),
36
+ );
37
+ useLayoutEffect(() => {
38
+ committedHost = host;
39
+ }, [host]);
40
+ if (suspend) throw pending;
41
+ return children;
42
+ };
43
+
44
+ const view = render(
45
+ <Suspense fallback={null}>
46
+ <Probe authorization="Bearer workspace-a" suspend={false}>
47
+ workspace-a
48
+ </Probe>
49
+ </Suspense>,
50
+ );
51
+
52
+ act(() => {
53
+ startTransition(() => {
54
+ view.rerender(
55
+ <Suspense fallback={null}>
56
+ <Probe authorization="Bearer workspace-b" suspend>
57
+ workspace-b
58
+ </Probe>
59
+ </Suspense>,
60
+ );
61
+ });
62
+ });
63
+
64
+ expect(view.container.textContent).toBe("workspace-a");
65
+ await committedHost?.callTool({ name: "search" });
66
+
67
+ expect(fetch).toHaveBeenCalledWith("/api/mcp-apps", {
68
+ method: "POST",
69
+ headers: {
70
+ "content-type": "application/json",
71
+ authorization: "Bearer workspace-a",
72
+ },
73
+ body: JSON.stringify({
74
+ method: "tools/call",
75
+ params: { name: "search" },
76
+ }),
77
+ });
78
+
79
+ view.unmount();
80
+ });
81
+
82
+ it("keeps the URL and authorization from one committed options snapshot", async () => {
83
+ const fetch = vi.fn(async () => Response.json({ content: [] }));
84
+ let currentHost: McpAppsHost | undefined;
85
+
86
+ const Probe = ({
87
+ url,
88
+ authorization,
89
+ }: {
90
+ url: string;
91
+ authorization: string;
92
+ }) => {
93
+ const host = useResource(
94
+ McpAppsRemoteHost({
95
+ url,
96
+ fetch,
97
+ headers: { authorization },
98
+ }),
99
+ );
100
+ useLayoutEffect(() => {
101
+ currentHost = host;
102
+ void host.callTool({ name: "layout-request" });
103
+ }, [host]);
104
+ return null;
105
+ };
106
+
107
+ const view = render(
108
+ <Probe url="/workspace-a/mcp" authorization="Bearer workspace-a" />,
109
+ );
110
+ fetch.mockClear();
111
+
112
+ view.rerender(
113
+ <Probe url="/workspace-b/mcp" authorization="Bearer workspace-b" />,
114
+ );
115
+
116
+ expect(fetch).toHaveBeenNthCalledWith(1, "/workspace-b/mcp", {
117
+ method: "POST",
118
+ headers: {
119
+ "content-type": "application/json",
120
+ authorization: "Bearer workspace-b",
121
+ },
122
+ body: JSON.stringify({
123
+ method: "tools/call",
124
+ params: { name: "layout-request" },
125
+ }),
126
+ });
127
+
128
+ await currentHost?.callTool({ name: "committed-request" });
129
+
130
+ expect(fetch).toHaveBeenNthCalledWith(2, "/workspace-b/mcp", {
131
+ method: "POST",
132
+ headers: {
133
+ "content-type": "application/json",
134
+ authorization: "Bearer workspace-b",
135
+ },
136
+ body: JSON.stringify({
137
+ method: "tools/call",
138
+ params: { name: "committed-request" },
139
+ }),
140
+ });
141
+
142
+ view.unmount();
143
+ });
144
+
145
+ it("refreshes pending options when a URL switch render retries", async () => {
146
+ let authorization = "Bearer workspace-b-old";
147
+ let resumed = false;
148
+ let resume: (() => void) | undefined;
149
+ const pending = new Promise<void>((resolve) => {
150
+ resume = () => {
151
+ resumed = true;
152
+ resolve();
153
+ };
154
+ });
155
+ const fetch = vi.fn(async () => Response.json({ content: [] }));
156
+
157
+ const Probe = ({ url }: { url: string }) => {
158
+ const host = useResource(
159
+ McpAppsRemoteHost({
160
+ url,
161
+ fetch,
162
+ headers: {
163
+ authorization:
164
+ url === "/workspace-a/mcp" ? "Bearer workspace-a" : authorization,
165
+ },
166
+ }),
167
+ );
168
+ useLayoutEffect(() => {
169
+ void host.callTool({ name: "layout-request" });
170
+ }, [host]);
171
+ if (url === "/workspace-b/mcp" && !resumed) throw pending;
172
+ return <span>{url}</span>;
173
+ };
174
+
175
+ const view = render(
176
+ <Suspense fallback={null}>
177
+ <Probe url="/workspace-a/mcp" />
178
+ </Suspense>,
179
+ );
180
+ try {
181
+ fetch.mockClear();
182
+
183
+ act(() => {
184
+ startTransition(() => {
185
+ view.rerender(
186
+ <Suspense fallback={null}>
187
+ <Probe url="/workspace-b/mcp" />
188
+ </Suspense>,
189
+ );
190
+ });
191
+ });
192
+ expect(view.container.textContent).toBe("/workspace-a/mcp");
193
+
194
+ authorization = "Bearer workspace-b-new";
195
+ await act(async () => resume?.());
196
+ await waitFor(() =>
197
+ expect(view.container.textContent).toBe("/workspace-b/mcp"),
198
+ );
199
+
200
+ expect(fetch).toHaveBeenCalledWith("/workspace-b/mcp", {
201
+ method: "POST",
202
+ headers: {
203
+ "content-type": "application/json",
204
+ authorization: "Bearer workspace-b-new",
205
+ },
206
+ body: JSON.stringify({
207
+ method: "tools/call",
208
+ params: { name: "layout-request" },
209
+ }),
210
+ });
211
+ } finally {
212
+ view.unmount();
213
+ }
214
+ });
215
+ });
@@ -1,4 +1,4 @@
1
- import { useMemo, useRef } from "react";
1
+ import { useEffect, useMemo, useRef } from "react";
2
2
  import { resource } from "@assistant-ui/tap";
3
3
  import type {
4
4
  McpAppResource,
@@ -111,20 +111,17 @@ const useMcpAppsRemoteHost = (
111
111
  options: McpAppsRemoteHostOptions,
112
112
  ): McpAppsHost => {
113
113
  const optionsRef = useRef(options);
114
- optionsRef.current = options;
114
+ useEffect(() => {
115
+ optionsRef.current = options;
116
+ });
115
117
 
116
118
  const url = options.url;
117
119
 
118
- return useMemo((): McpAppsHost => {
119
- const getCurrentOptions = (): McpAppsRemoteHostOptions => {
120
- const current = optionsRef.current;
121
- return {
122
- url,
123
- ...(current.fetch !== undefined ? { fetch: current.fetch } : {}),
124
- ...(current.headers !== undefined ? { headers: current.headers } : {}),
125
- };
126
- };
127
- return {
120
+ const hostState = useMemo(() => {
121
+ let pendingOptions = options;
122
+ const getCurrentOptions = () =>
123
+ optionsRef.current.url === url ? optionsRef.current : pendingOptions;
124
+ const host: McpAppsHost = {
128
125
  loadResource: async (params) => {
129
126
  const options = getCurrentOptions();
130
127
  return parseMcpAppResource(
@@ -139,7 +136,26 @@ const useMcpAppsRemoteHost = (
139
136
  listResources: (params) =>
140
137
  postToHost(getCurrentOptions(), "resources/list", params),
141
138
  };
139
+ return {
140
+ host,
141
+ updatePendingOptions: (next: McpAppsRemoteHostOptions) => {
142
+ pendingOptions = next;
143
+ },
144
+ };
145
+ // oxlint-disable-next-line react/exhaustive-deps -- URL changes replace the host identity; pending and same-URL options are refreshed outside the memo
142
146
  }, [url]);
147
+
148
+ // The pending snapshot is read only while the committed ref still lags this
149
+ // host's URL, and every write to it carries that same render's options, so
150
+ // no render can pair one URL with another's credentials. A committed host can
151
+ // be written here (a child layout effect re-rendering synchronously runs
152
+ // before passive effects publish the ref), which stays coherent for the same
153
+ // reason.
154
+ if (optionsRef.current.url !== url) {
155
+ hostState.updatePendingOptions(options);
156
+ }
157
+
158
+ return hostState.host;
143
159
  };
144
160
 
145
161
  export const McpAppsRemoteHost = resource(useMcpAppsRemoteHost);