@assistant-ui/react 0.15.16 → 0.15.18

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 (297) hide show
  1. package/README.md +4 -4
  2. package/dist/index.d.ts +8 -4
  3. package/dist/index.js +4 -2
  4. package/dist/mcp-apps/McpAppRenderer.d.ts +36 -9
  5. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  6. package/dist/mcp-apps/McpAppRenderer.js +108 -57
  7. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  8. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  9. package/dist/mcp-apps/McpAppsRemoteHost.js +20 -17
  10. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  11. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  12. package/dist/mcp-apps/app-frame.js +229 -115
  13. package/dist/mcp-apps/app-frame.js.map +1 -1
  14. package/dist/mcp-apps/bridge.js +2 -3
  15. package/dist/mcp-apps/bridge.js.map +1 -1
  16. package/dist/mcp-apps/index.d.ts +3 -3
  17. package/dist/model-context/frame/useAssistantFrameHost.d.ts +1 -1
  18. package/dist/model-context/frame/useAssistantFrameHost.js +10 -11
  19. package/dist/model-context/frame/useAssistantFrameHost.js.map +1 -1
  20. package/dist/model-context/makeAssistantVisible.js +1 -1
  21. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  22. package/dist/primitives/actionBar/ActionBarCopy.js +3 -3
  23. package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
  24. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +3 -3
  25. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  26. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +3 -3
  27. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
  28. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +3 -3
  29. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
  31. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +3 -3
  32. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
  33. package/dist/primitives/actionBarMore/ActionBarMoreContent.js.map +1 -1
  34. package/dist/primitives/actionBarMore/ActionBarMoreItem.js.map +1 -1
  35. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js.map +1 -1
  36. package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js.map +1 -1
  37. package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js.map +1 -1
  38. package/dist/primitives/actionBarMore/scope.d.ts +2 -2
  39. package/dist/primitives/actionBarMore/scope.d.ts.map +1 -1
  40. package/dist/primitives/actionBarMore/scope.js.map +1 -1
  41. package/dist/primitives/assistantModal/AssistantModalAnchor.js.map +1 -1
  42. package/dist/primitives/assistantModal/AssistantModalContent.js +1 -1
  43. package/dist/primitives/assistantModal/AssistantModalContent.js.map +1 -1
  44. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  45. package/dist/primitives/assistantModal/AssistantModalTrigger.js.map +1 -1
  46. package/dist/primitives/assistantModal/scope.d.ts +2 -2
  47. package/dist/primitives/assistantModal/scope.d.ts.map +1 -1
  48. package/dist/primitives/assistantModal/scope.js.map +1 -1
  49. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  50. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  51. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  52. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  53. package/dist/primitives/composer/ComposerInput.js +1 -3
  54. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  55. package/dist/primitives/composer/ComposerInputPluginContext.d.ts +1 -2
  56. package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -1
  57. package/dist/primitives/composer/ComposerInputPluginContext.js +1 -6
  58. package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
  59. package/dist/primitives/composer/ComposerQuote.js +5 -5
  60. package/dist/primitives/composer/ComposerQuote.js.map +1 -1
  61. package/dist/primitives/composer/ComposerRoot.js +3 -3
  62. package/dist/primitives/composer/ComposerRoot.js.map +1 -1
  63. package/dist/primitives/composer/trigger/TriggerPopover.d.ts +5 -0
  64. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  65. package/dist/primitives/composer/trigger/TriggerPopover.js +157 -130
  66. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  67. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +46 -15
  68. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  69. package/dist/primitives/composer/trigger/TriggerPopoverBack.js +3 -3
  70. package/dist/primitives/composer/trigger/TriggerPopoverBack.js.map +1 -1
  71. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +4 -4
  72. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  73. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +40 -10
  74. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  75. package/dist/primitives/composer/trigger/TriggerPopoverItems.js +4 -4
  76. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  77. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -0
  78. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  79. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +63 -61
  80. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
  81. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +2 -0
  82. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts.map +1 -1
  83. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  84. package/dist/primitives/composer/trigger/detectTrigger.d.ts +21 -1
  85. package/dist/primitives/composer/trigger/detectTrigger.d.ts.map +1 -0
  86. package/dist/primitives/composer/trigger/detectTrigger.js +17 -3
  87. package/dist/primitives/composer/trigger/detectTrigger.js.map +1 -1
  88. package/dist/primitives/composer/trigger/index.d.ts +3 -1
  89. package/dist/primitives/composer/trigger/index.d.ts.map +1 -1
  90. package/dist/primitives/composer/trigger/index.js.map +1 -1
  91. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts +6 -0
  92. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts.map +1 -0
  93. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +9 -0
  94. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js.map +1 -0
  95. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +3 -4
  96. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  97. package/dist/primitives/composer/trigger/triggerDetectionResource.js +15 -14
  98. package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
  99. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  100. package/dist/primitives/composer/trigger/triggerNavigationResource.js +3 -5
  101. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  102. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  103. package/dist/primitives/composer/trigger/triggerSelectionResource.js +18 -19
  104. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  105. package/dist/primitives/composer.d.ts +3 -2
  106. package/dist/primitives/composer.js.map +1 -1
  107. package/dist/primitives/error/ErrorMessage.js.map +1 -1
  108. package/dist/primitives/message/MessageIf.js.map +1 -1
  109. package/dist/primitives/message/MessageParts.js.map +1 -1
  110. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  111. package/dist/primitives/message/MessageRoot.js +4 -4
  112. package/dist/primitives/message/MessageRoot.js.map +1 -1
  113. package/dist/primitives/messagePart/MessagePartText.d.ts +1 -1
  114. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  115. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +3 -3
  116. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
  117. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +3 -3
  118. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  119. package/dist/primitives/thread/ThreadIf.js.map +1 -1
  120. package/dist/primitives/thread/ThreadViewport.js +3 -3
  121. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  122. package/dist/primitives/thread/ThreadViewportFooter.js +3 -3
  123. package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
  124. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  125. package/dist/primitives/thread/useThreadViewportAutoScroll.js +4 -4
  126. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  127. package/dist/primitives/threadList/ThreadListNew.js +3 -3
  128. package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
  129. package/dist/primitives/threadListFocusGroup.d.ts +2 -1
  130. package/dist/primitives/threadListFocusGroup.d.ts.map +1 -1
  131. package/dist/primitives/threadListFocusGroup.js +2 -2
  132. package/dist/primitives/threadListFocusGroup.js.map +1 -1
  133. package/dist/primitives/threadListItem/ThreadListItemRoot.js +3 -3
  134. package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
  135. package/dist/primitives/threadListItem/ThreadListItemTrigger.js +1 -1
  136. package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
  137. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -1
  138. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
  139. package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js.map +1 -1
  140. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +1 -1
  141. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js.map +1 -1
  142. package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js.map +1 -1
  143. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -2
  144. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js.map +1 -1
  145. package/dist/primitives/threadListItemMore/scope.d.ts +2 -2
  146. package/dist/primitives/threadListItemMore/scope.d.ts.map +1 -1
  147. package/dist/primitives/threadListItemMore/scope.js.map +1 -1
  148. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  149. package/dist/sandbox-host/SandboxHost.js +148 -72
  150. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  151. package/dist/unstable/useLiveCompletionAdapter.js +3 -1
  152. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  153. package/dist/unstable/useMentionAdapter.d.ts +9 -3
  154. package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
  155. package/dist/unstable/useMentionAdapter.js +50 -43
  156. package/dist/unstable/useMentionAdapter.js.map +1 -1
  157. package/dist/unstable/useModelContextSnapshot.d.ts +34 -0
  158. package/dist/unstable/useModelContextSnapshot.d.ts.map +1 -0
  159. package/dist/unstable/useModelContextSnapshot.js +61 -0
  160. package/dist/unstable/useModelContextSnapshot.js.map +1 -0
  161. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
  162. package/dist/unstable/useSlashCommandAdapter.js +37 -29
  163. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  164. package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +14 -0
  165. package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -0
  166. package/dist/unstable/webmcp/WebMcpRegistrationResource.js +85 -0
  167. package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -0
  168. package/dist/unstable/webmcp/convertTools.d.ts +21 -0
  169. package/dist/unstable/webmcp/convertTools.d.ts.map +1 -0
  170. package/dist/unstable/webmcp/convertTools.js +110 -0
  171. package/dist/unstable/webmcp/convertTools.js.map +1 -0
  172. package/dist/unstable/webmcp/useWebMcpProvider.d.ts +21 -0
  173. package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -0
  174. package/dist/unstable/webmcp/useWebMcpProvider.js +105 -0
  175. package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -0
  176. package/dist/unstable/webmcp/webmcp-host.d.ts +39 -0
  177. package/dist/unstable/webmcp/webmcp-host.d.ts.map +1 -0
  178. package/dist/unstable/webmcp/webmcp-host.js +29 -0
  179. package/dist/unstable/webmcp/webmcp-host.js.map +1 -0
  180. package/dist/utils/Primitive.d.ts +6 -6
  181. package/dist/utils/Primitive.js +2 -2
  182. package/dist/utils/Primitive.js.map +1 -1
  183. package/dist/utils/createActionButton.js +3 -3
  184. package/dist/utils/createActionButton.js.map +1 -1
  185. package/dist/utils/hooks/useOnResizeContent.js +1 -1
  186. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  187. package/dist/utils/hooks/useOnScrollToBottom.js +1 -1
  188. package/dist/utils/hooks/useOnScrollToBottom.js.map +1 -1
  189. package/dist/utils/smooth/SmoothContext.d.ts +24 -0
  190. package/dist/utils/smooth/SmoothContext.d.ts.map +1 -1
  191. package/dist/utils/smooth/useSmooth.js +1 -1
  192. package/dist/utils/smooth/useSmooth.js.map +1 -1
  193. package/package.json +14 -22
  194. package/src/index.ts +22 -1
  195. package/src/mcp-apps/McpAppRenderer.test.tsx +386 -9
  196. package/src/mcp-apps/McpAppRenderer.tsx +171 -69
  197. package/src/mcp-apps/McpAppsRemoteHost.concurrent.test.tsx +215 -0
  198. package/src/mcp-apps/McpAppsRemoteHost.ts +28 -12
  199. package/src/mcp-apps/app-frame.test.tsx +349 -0
  200. package/src/mcp-apps/app-frame.tsx +68 -9
  201. package/src/mcp-apps/bridge.ts +7 -2
  202. package/src/mcp-apps/index.ts +6 -1
  203. package/src/model-context/frame/AssistantFrame.test.ts +3 -3
  204. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
  205. package/src/model-context/frame/useAssistantFrameHost.ts +2 -2
  206. package/src/model-context/makeAssistantVisible.tsx +1 -1
  207. package/src/primitives/actionBar/ActionBarCopy.tsx +1 -1
  208. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +1 -1
  209. package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +1 -1
  210. package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +1 -1
  211. package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +1 -1
  212. package/src/primitives/actionBarMore/scope.ts +2 -2
  213. package/src/primitives/assistantModal/AssistantModalContent.tsx +1 -1
  214. package/src/primitives/assistantModal/scope.ts +2 -2
  215. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +1 -1
  216. package/src/primitives/composer/ComposerInput.test.tsx +2 -1
  217. package/src/primitives/composer/ComposerInput.tsx +3 -3
  218. package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
  219. package/src/primitives/composer/ComposerQuote.tsx +1 -1
  220. package/src/primitives/composer/ComposerRoot.tsx +1 -1
  221. package/src/primitives/composer/trigger/TriggerPopover.tsx +16 -1
  222. package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
  223. package/src/primitives/composer/trigger/TriggerPopoverBack.tsx +1 -1
  224. package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
  225. package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +1 -1
  226. package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
  227. package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +1 -1
  228. package/src/primitives/composer/trigger/TriggerPopoverResource.ts +4 -1
  229. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +2 -0
  230. package/src/primitives/composer/trigger/detectTrigger.test.ts +88 -5
  231. package/src/primitives/composer/trigger/detectTrigger.ts +50 -9
  232. package/src/primitives/composer/trigger/index.ts +1 -0
  233. package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
  234. package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
  235. package/src/primitives/composer/trigger/triggerDetectionResource.ts +10 -7
  236. package/src/primitives/composer/trigger/triggerNavigationResource.ts +3 -10
  237. package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +15 -3
  238. package/src/primitives/composer/trigger/triggerSelectionResource.ts +1 -4
  239. package/src/primitives/composer.ts +1 -0
  240. package/src/primitives/message/MessageRoot.tsx +1 -1
  241. package/src/primitives/selectionToolbar/SelectionToolbarQuote.tsx +1 -1
  242. package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +1 -1
  243. package/src/primitives/thread/ThreadViewport.tsx +1 -1
  244. package/src/primitives/thread/ThreadViewportFooter.tsx +1 -1
  245. package/src/primitives/thread/useThreadViewportAutoScroll.ts +8 -7
  246. package/src/primitives/threadList/ThreadListNew.tsx +1 -1
  247. package/src/primitives/threadListFocusGroup.ts +2 -2
  248. package/src/primitives/threadListItem/ThreadListItemRoot.tsx +1 -1
  249. package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +1 -1
  250. package/src/primitives/threadListItemMore/ThreadListItemMoreContent.tsx +1 -1
  251. package/src/primitives/threadListItemMore/ThreadListItemMoreRoot.tsx +1 -1
  252. package/src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx +2 -2
  253. package/src/primitives/threadListItemMore/scope.ts +2 -2
  254. package/src/sandbox-host/SandboxHost.test.tsx +165 -3
  255. package/src/sandbox-host/SandboxHost.tsx +42 -9
  256. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +31 -1
  257. package/src/unstable/useComposerInputHistory.test.tsx +2 -1
  258. package/src/unstable/useLiveCompletionAdapter.test.tsx +60 -1
  259. package/src/unstable/useLiveCompletionAdapter.ts +4 -1
  260. package/src/unstable/useMentionAdapter.test.tsx +337 -0
  261. package/src/unstable/useMentionAdapter.ts +94 -57
  262. package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
  263. package/src/unstable/useModelContextSnapshot.ts +67 -0
  264. package/src/unstable/useSlashCommandAdapter.test.tsx +141 -0
  265. package/src/unstable/useSlashCommandAdapter.ts +47 -19
  266. package/src/unstable/webmcp/WebMcpRegistrationResource.ts +92 -0
  267. package/src/unstable/webmcp/__tests__/webmcp.fake.tsx +168 -0
  268. package/src/unstable/webmcp/convertTools.test.ts +393 -0
  269. package/src/unstable/webmcp/convertTools.ts +180 -0
  270. package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
  271. package/src/unstable/webmcp/useWebMcpProvider.ts +150 -0
  272. package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
  273. package/src/unstable/webmcp/webmcp-host.ts +83 -0
  274. package/src/utils/Primitive.tsx +2 -2
  275. package/src/utils/createActionButton.tsx +1 -1
  276. package/src/utils/hooks/useOnResizeContent.ts +1 -1
  277. package/src/utils/hooks/useOnScrollToBottom.ts +1 -1
  278. package/src/utils/smooth/useSmooth.ts +1 -1
  279. package/dist/legacy-runtime/runtime/MessagePartRuntime.d.ts +0 -3
  280. package/dist/legacy-runtime/runtime/MessagePartRuntime.js +0 -2
  281. package/dist/legacy-runtime/runtime/MessageRuntime.d.ts +0 -3
  282. package/dist/legacy-runtime/runtime/MessageRuntime.js +0 -2
  283. package/dist/legacy-runtime/runtime/ThreadListRuntime.d.ts +0 -3
  284. package/dist/legacy-runtime/runtime/ThreadListRuntime.js +0 -2
  285. package/dist/utils/invokeCallbackSafely.d.ts +0 -5
  286. package/dist/utils/invokeCallbackSafely.d.ts.map +0 -1
  287. package/dist/utils/invokeCallbackSafely.js +0 -15
  288. package/dist/utils/invokeCallbackSafely.js.map +0 -1
  289. package/dist/utils/useToolArgsFieldStatus.d.ts +0 -15
  290. package/dist/utils/useToolArgsFieldStatus.d.ts.map +0 -1
  291. package/dist/utils/useToolArgsFieldStatus.js +0 -23
  292. package/dist/utils/useToolArgsFieldStatus.js.map +0 -1
  293. package/src/legacy-runtime/runtime/MessagePartRuntime.ts +0 -2
  294. package/src/legacy-runtime/runtime/MessageRuntime.ts +0 -3
  295. package/src/legacy-runtime/runtime/ThreadListRuntime.ts +0 -4
  296. package/src/utils/invokeCallbackSafely.ts +0 -11
  297. package/src/utils/useToolArgsFieldStatus.ts +0 -18
@@ -2,13 +2,17 @@
2
2
 
3
3
  import {
4
4
  useEffect,
5
+ useLayoutEffect,
5
6
  useMemo,
6
7
  useRef,
7
8
  useState,
8
- type MutableRefObject,
9
9
  type ReactNode,
10
10
  } from "react";
11
- import type { McpAppMetadata } from "@assistant-ui/core";
11
+ import type {
12
+ McpAppMetadata,
13
+ TextMessagePart,
14
+ ToolCallMessagePart,
15
+ } from "@assistant-ui/core";
12
16
  import type {
13
17
  ToolCallMessagePartComponent,
14
18
  ToolCallMessagePartProps,
@@ -29,13 +33,11 @@ import type {
29
33
  import { getMcpAppFromToolPart } from "./utils";
30
34
  import { isRecord } from "@assistant-ui/core/internal";
31
35
 
32
- export type McpAppRendererOptions = {
33
- /**
34
- * Provides the data-plane operations the widget can request
35
- * (`loadResource`, `callTool`, `readResource`, `listResources`). Use
36
- * `McpAppsRemoteHost({ url })` for the default HTTP-route convention.
37
- */
38
- host: ResourceElement<McpAppsHost>;
36
+ /**
37
+ * Options that apply to a single MCP app. `McpAppRenderer` takes them as
38
+ * thread-wide defaults, and its `forPart` resolver overrides them for one part.
39
+ */
40
+ export type McpAppPartOptions = {
39
41
  /** Sandbox + container styling. Passes through to SafeContentFrame. */
40
42
  sandbox?: McpAppSandboxConfig;
41
43
  /**
@@ -47,6 +49,18 @@ export type McpAppRendererOptions = {
47
49
  hostInfo?: McpAppHostInfo;
48
50
  /** Delivered to the widget on initialize and pushed via `notifications/host_context/changed` on change. */
49
51
  hostContext?: McpAppHostContext;
52
+ /**
53
+ * Optional widget interaction and lifecycle handlers. Data-plane handlers
54
+ * (`callTool`, `readResource`, `listResources`) always use `host`.
55
+ *
56
+ * Which handlers a widget may call is captured when its frame mounts, so a
57
+ * `forPart` resolver returns the same handler keys for a part on every
58
+ * render even when the implementations change.
59
+ */
60
+ handlers?: Omit<
61
+ McpAppBridgeHandlers,
62
+ "callTool" | "readResource" | "listResources"
63
+ >;
50
64
  /** Rendered when no MCP app is on the part, or while load is in flight / failed (unless overridden). */
51
65
  fallback?: ReactNode;
52
66
  /** Rendered while the resource is loading. Defaults to `fallback`. */
@@ -55,6 +69,26 @@ export type McpAppRendererOptions = {
55
69
  errorFallback?: ReactNode | ((error: Error) => ReactNode);
56
70
  };
57
71
 
72
+ export type McpAppRendererOptions = McpAppPartOptions & {
73
+ /**
74
+ * Provides the data-plane operations the widget can request
75
+ * (`loadResource`, `callTool`, `readResource`, `listResources`). Use
76
+ * `McpAppsRemoteHost({ url })` for the default HTTP-route convention.
77
+ */
78
+ host: ResourceElement<McpAppsHost>;
79
+ /**
80
+ * Resolves the options for one part. Each returned value replaces the
81
+ * thread-wide option of the same name, so a host can give every MCP app its
82
+ * own display mode and answer `requestDisplayMode` for the part that asked.
83
+ *
84
+ * `handlers` is the exception and merges per key. Its keys are capabilities
85
+ * negotiated with the widget rather than data, so replacing the whole bag to
86
+ * add one handler would withdraw the others from that part's widget with
87
+ * nothing to observe it by.
88
+ */
89
+ forPart?: (part: ToolCallMessagePart) => McpAppPartOptions;
90
+ };
91
+
58
92
  type LoadedResourceState = {
59
93
  host: McpAppsHost;
60
94
  resourceUri: string;
@@ -63,7 +97,31 @@ type LoadedResourceState = {
63
97
  error?: Error;
64
98
  };
65
99
 
66
- type UseHostStore = UseBoundStore<StoreApi<{ host: McpAppsHost }>>;
100
+ type RendererState = {
101
+ host: McpAppsHost;
102
+ options: McpAppRendererOptions;
103
+ };
104
+
105
+ type UseRendererStore = UseBoundStore<StoreApi<RendererState>>;
106
+
107
+ // Callers pass options as an object literal, so the resource hands this a fresh
108
+ // identity on every run; comparing the values keeps an unchanged run from
109
+ // re-rendering every mounted part. `host` is excluded because it is an element
110
+ // rebuilt on every run, and the store carries the resolved host separately.
111
+ const isSameOptions = (
112
+ a: McpAppRendererOptions,
113
+ b: McpAppRendererOptions,
114
+ ): boolean => {
115
+ const aKeys = Object.keys(a).filter((key) => key !== "host");
116
+ const bKeys = Object.keys(b).filter((key) => key !== "host");
117
+ if (aKeys.length !== bKeys.length) return false;
118
+ const aValues = a as Record<string, unknown>;
119
+ const bValues = b as Record<string, unknown>;
120
+ return aKeys.every(
121
+ (key) =>
122
+ Object.hasOwn(bValues, key) && Object.is(aValues[key], bValues[key]),
123
+ );
124
+ };
67
125
 
68
126
  function getInput(part: {
69
127
  status: { type: string };
@@ -83,45 +141,80 @@ const defaultOpenLink = ({ url }: { url: string }) => {
83
141
  window.open(url, "_blank", "noopener,noreferrer");
84
142
  };
85
143
 
86
- function extractSendMessageText(params: unknown): string | undefined {
87
- if (typeof params === "string") return params;
144
+ function extractSendMessageTexts(params: unknown): string[] | undefined {
145
+ if (typeof params === "string") return [params];
88
146
  if (!params || typeof params !== "object") return undefined;
89
147
  const obj = params as Record<string, unknown>;
90
- if (typeof obj["prompt"] === "string") return obj["prompt"];
91
- if (typeof obj["text"] === "string") return obj["text"];
92
- if (typeof obj["message"] === "string") return obj["message"];
148
+ if (typeof obj["prompt"] === "string") return [obj["prompt"]];
149
+ if (typeof obj["text"] === "string") return [obj["text"]];
150
+ if (typeof obj["message"] === "string") return [obj["message"]];
151
+ if (Array.isArray(obj["content"])) {
152
+ return obj["content"]
153
+ .filter(
154
+ (block): block is { type: "text"; text: string } =>
155
+ isRecord(block) &&
156
+ block["type"] === "text" &&
157
+ typeof block["text"] === "string",
158
+ )
159
+ .map((block) => block.text);
160
+ }
93
161
  return undefined;
94
162
  }
95
163
 
164
+ function extractSendMessageContent(
165
+ params: unknown,
166
+ ): { content: TextMessagePart[] } | { reason: string } {
167
+ const texts = extractSendMessageTexts(params);
168
+ if (!texts) return { reason: "unrecognised params shape" };
169
+ const content = texts
170
+ .filter((text) => text !== "")
171
+ .map((text): TextMessagePart => ({ type: "text", text }));
172
+ if (content.length === 0) {
173
+ return { reason: "no text content to append" };
174
+ }
175
+ return { content };
176
+ }
177
+
178
+ function resolvePartOptions(
179
+ options: McpAppRendererOptions,
180
+ part: ToolCallMessagePartProps,
181
+ ): McpAppRendererOptions {
182
+ const overrides = options.forPart?.(part);
183
+ if (!overrides) return options;
184
+ const handlers =
185
+ options.handlers && overrides.handlers
186
+ ? { ...options.handlers, ...overrides.handlers }
187
+ : (overrides.handlers ?? options.handlers);
188
+ return {
189
+ ...options,
190
+ ...overrides,
191
+ ...(handlers === undefined ? {} : { handlers }),
192
+ };
193
+ }
194
+
96
195
  function InlineRenderer({
97
196
  part,
98
- useHostStore,
99
- optionsRef,
197
+ useRendererStore,
100
198
  }: {
101
199
  part: ToolCallMessagePartProps;
102
- useHostStore: UseHostStore;
103
- optionsRef: MutableRefObject<McpAppRendererOptions>;
200
+ useRendererStore: UseRendererStore;
104
201
  }) {
105
- const opts = optionsRef.current;
106
202
  const aui = useAui();
203
+ const rendererOptions = useRendererStore((state) => state.options);
204
+ const opts = resolvePartOptions(rendererOptions, part);
107
205
  const app = getMcpAppFromToolPart(part);
108
206
  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
- }
207
+ useLayoutEffect(() => {
208
+ if (app != null) cachedAppRef.current = app;
209
+ }, [app]);
116
210
  const appForRender = app ?? cachedAppRef.current;
117
211
 
118
212
  const [loadedResource, setLoadedResource] = useState<LoadedResourceState>();
119
213
 
120
- const host = useHostStore((state) => state.host);
214
+ const host = useRendererStore((state) => state.host);
121
215
  const resourceUri = appForRender?.resourceUri;
122
216
  const serverId = appForRender?.serverId;
123
- const serverIdRef = useRef<string | undefined>(undefined);
124
- serverIdRef.current = serverId;
217
+ const callerHandlers = opts.handlers;
125
218
  useEffect(() => {
126
219
  if (resourceUri == null) return;
127
220
  let cancelled = false;
@@ -129,12 +222,16 @@ function InlineRenderer({
129
222
  const targetUri = resourceUri;
130
223
  const targetServerId = serverId;
131
224
 
132
- targetHost
133
- .loadResource({
134
- uri: targetUri,
135
- ...(targetServerId ? { serverId: targetServerId } : {}),
136
- })
137
- .then((res) => {
225
+ // Host changes are published later in this passive flush. Defer until that
226
+ // publication lands, then verify this effect still owns the current host.
227
+ const loadResource = async () => {
228
+ await Promise.resolve();
229
+ if (cancelled || useRendererStore.getState().host !== targetHost) return;
230
+ try {
231
+ const res = await targetHost.loadResource({
232
+ uri: targetUri,
233
+ ...(targetServerId ? { serverId: targetServerId } : {}),
234
+ });
138
235
  if (!cancelled)
139
236
  setLoadedResource({
140
237
  host: targetHost,
@@ -144,8 +241,7 @@ function InlineRenderer({
144
241
  : {}),
145
242
  resource: res,
146
243
  });
147
- })
148
- .catch((error: unknown) => {
244
+ } catch (error: unknown) {
149
245
  if (!cancelled) {
150
246
  setLoadedResource({
151
247
  host: targetHost,
@@ -156,43 +252,50 @@ function InlineRenderer({
156
252
  error: error instanceof Error ? error : new Error(String(error)),
157
253
  });
158
254
  }
159
- });
255
+ }
256
+ };
257
+ void loadResource();
160
258
 
161
259
  return () => {
162
260
  cancelled = true;
163
261
  };
164
- }, [host, resourceUri, serverId]);
262
+ }, [host, resourceUri, serverId, useRendererStore]);
165
263
 
166
264
  const bridgeHandlers = useMemo<McpAppBridgeHandlers>(
167
265
  () => ({
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
- },
266
+ ...callerHandlers,
267
+ openLink: callerHandlers?.openLink ?? defaultOpenLink,
268
+ sendMessage:
269
+ callerHandlers?.sendMessage ??
270
+ ((params) => {
271
+ const result = extractSendMessageContent(params);
272
+ if ("reason" in result) {
273
+ return { isError: true, ok: false, reason: result.reason };
274
+ }
275
+ aui.thread.append({ content: result.content });
276
+ return { ok: true };
277
+ }),
175
278
  callTool: (params) =>
176
- useHostStore.getState().host.callTool({
279
+ useRendererStore.getState().host.callTool({
177
280
  ...params,
178
- ...(serverIdRef.current ? { serverId: serverIdRef.current } : {}),
281
+ ...(serverId ? { serverId } : {}),
179
282
  }),
180
283
  readResource: (params) =>
181
- useHostStore.getState().host.readResource({
284
+ useRendererStore.getState().host.readResource({
182
285
  ...params,
183
- ...(serverIdRef.current ? { serverId: serverIdRef.current } : {}),
286
+ ...(serverId ? { serverId } : {}),
184
287
  }),
185
288
  listResources: (params) => {
186
- if (!serverIdRef.current) {
187
- return useHostStore.getState().host.listResources(params);
289
+ if (!serverId) {
290
+ return useRendererStore.getState().host.listResources(params);
188
291
  }
189
- return useHostStore.getState().host.listResources({
292
+ return useRendererStore.getState().host.listResources({
190
293
  ...(isRecord(params) ? params : {}),
191
- serverId: serverIdRef.current,
294
+ serverId,
192
295
  });
193
296
  },
194
297
  }),
195
- [aui, useHostStore],
298
+ [aui, callerHandlers, serverId, useRendererStore],
196
299
  );
197
300
 
198
301
  const loadedResourceForApp =
@@ -246,27 +349,26 @@ const useMcpAppRenderer = (
246
349
  ): { readonly render: ToolCallMessagePartComponent } => {
247
350
  const host = useResource(options.host);
248
351
 
249
- const optionsRef = useRef<McpAppRendererOptions>(options);
250
- optionsRef.current = options;
251
-
252
- const [useHostStore] = useState(() =>
253
- create<{ host: McpAppsHost }>(() => ({ host })),
352
+ // The rendered component identity has to stay stable, or every part remounts
353
+ // its frame; options reach the parts through the store instead of props.
354
+ const [useRendererStore] = useState(() =>
355
+ create<RendererState>(() => ({ host, options })),
254
356
  );
255
357
  useEffect(() => {
256
- useHostStore.setState({ host });
257
- }, [host, useHostStore]);
358
+ useRendererStore.setState((prev) =>
359
+ prev.host === host && isSameOptions(prev.options, options)
360
+ ? prev
361
+ : { host, options },
362
+ );
363
+ }, [host, options, useRendererStore]);
258
364
 
259
365
  const render = useMemo((): ToolCallMessagePartComponent => {
260
366
  const Render: ToolCallMessagePartComponent = (props) => (
261
- <InlineRenderer
262
- part={props}
263
- useHostStore={useHostStore}
264
- optionsRef={optionsRef}
265
- />
367
+ <InlineRenderer part={props} useRendererStore={useRendererStore} />
266
368
  );
267
369
  Render.displayName = "McpAppRenderer";
268
370
  return Render;
269
- }, [useHostStore]);
371
+ }, [useRendererStore]);
270
372
 
271
373
  return { render };
272
374
  };
@@ -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);