@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
@@ -81,7 +81,8 @@ vi.mock("./trigger/TriggerPopoverRootContext", () => ({
81
81
 
82
82
  let escapeKeydownHandler: ((event: KeyboardEvent) => void) | null = null;
83
83
 
84
- vi.mock("@radix-ui/react-use-escape-keydown", () => ({
84
+ vi.mock("radix-ui/internal", async (importOriginal) => ({
85
+ ...(await importOriginal<typeof import("radix-ui/internal")>()),
85
86
  useEscapeKeydown: (handler: (event: KeyboardEvent) => void) => {
86
87
  escapeKeydownHandler = handler;
87
88
  },
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { composeEventHandlers } from "@radix-ui/primitive";
4
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
3
+ import { composeEventHandlers } from "radix-ui/internal";
4
+ import { useComposedRefs } from "radix-ui/internal";
5
5
  import { Slot } from "radix-ui";
6
6
  import {
7
7
  type ClipboardEvent,
@@ -19,7 +19,7 @@ import TextareaAutosize, {
19
19
  type TextareaAutosizeProps,
20
20
  type TextareaHeightChangeMeta,
21
21
  } from "react-textarea-autosize";
22
- import { useEscapeKeydown } from "@radix-ui/react-use-escape-keydown";
22
+ import { useEscapeKeydown } from "radix-ui/internal";
23
23
  import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom";
24
24
  import { useMediaQuery } from "../../utils/hooks/useMediaQuery";
25
25
  import { useAui } from "@assistant-ui/store";
@@ -51,16 +51,6 @@ export type ComposerInputPluginRegistry = {
51
51
  const ComposerInputPluginRegistryContext =
52
52
  createContext<ComposerInputPluginRegistry | null>(null);
53
53
 
54
- export const useComposerInputPluginRegistry =
55
- (): ComposerInputPluginRegistry => {
56
- const ctx = useContext(ComposerInputPluginRegistryContext);
57
- if (!ctx)
58
- throw new Error(
59
- "useComposerInputPluginRegistry must be used within a ComposerInputPluginProvider",
60
- );
61
- return ctx;
62
- };
63
-
64
54
  export const useComposerInputPluginRegistryOptional =
65
55
  (): ComposerInputPluginRegistry | null => {
66
56
  return useContext(ComposerInputPluginRegistryContext);
@@ -8,7 +8,7 @@ import {
8
8
  useCallback,
9
9
  } from "react";
10
10
  import { useAui, useAuiState } from "@assistant-ui/store";
11
- import { composeEventHandlers } from "@radix-ui/primitive";
11
+ import { composeEventHandlers } from "radix-ui/internal";
12
12
 
13
13
  // ---- Root ----
14
14
 
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { composeEventHandlers } from "@radix-ui/primitive";
3
+ import { composeEventHandlers } from "radix-ui/internal";
4
4
  import { useAuiState } from "@assistant-ui/store";
5
5
  import { Primitive } from "../../utils/Primitive";
6
6
  import {
@@ -28,6 +28,7 @@ import {
28
28
  useTriggerPopoverAriaPublish,
29
29
  useTriggerPopoverRootContext,
30
30
  } from "./TriggerPopoverRootContext";
31
+ import type { TriggerMatcher } from "./detectTrigger";
31
32
 
32
33
  const TriggerPopoverScopeContext =
33
34
  createContext<TriggerPopoverResourceOutput | null>(null);
@@ -70,6 +71,8 @@ export namespace ComposerPrimitiveTriggerPopover {
70
71
  > & {
71
72
  /** The character(s) that activate this trigger (e.g. `"@"`, `"/"`). Also serves as the trigger identity within the root. */
72
73
  readonly char: string;
74
+ /** Overrides trigger detection for both textarea and Lexical inputs. `endOffset` is the exclusive replace bound. */
75
+ readonly matcher?: TriggerMatcher | undefined;
73
76
  /** Adapter providing categories and items. */
74
77
  readonly adapter?: Unstable_TriggerAdapter | undefined;
75
78
  /** Whether the adapter is resolving items, surfaced to the popover scope for async sources. @default false */
@@ -111,6 +114,7 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
111
114
  (
112
115
  {
113
116
  char,
117
+ matcher,
114
118
  adapter,
115
119
  isLoading = false,
116
120
  "aria-label": ariaLabel,
@@ -122,6 +126,15 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
122
126
  const aui = useAui();
123
127
  const text = useAuiState((s) => s.composer.text);
124
128
  const popoverId = useId();
129
+ const matcherRef = useRef(matcher);
130
+ useEffect(() => {
131
+ matcherRef.current = matcher;
132
+ });
133
+ const [registeredMatcher] = useState(
134
+ () => (nextText: string, triggerChar: string, cursorPosition: number) =>
135
+ matcherRef.current!(nextText, triggerChar, cursorPosition),
136
+ );
137
+ const hasMatcher = matcher !== undefined;
125
138
 
126
139
  // Track in state (for resource reactivity) + ref (dev warning on duplicate registrations).
127
140
  const behaviorRef = useRef<TriggerBehavior | null>(null);
@@ -165,6 +178,7 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
165
178
  adapter,
166
179
  text,
167
180
  triggerChar: char,
181
+ matcher,
168
182
  behavior: behavior ?? undefined,
169
183
  aui,
170
184
  popoverId,
@@ -178,10 +192,11 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
178
192
  useEffect(() => {
179
193
  return root.register({
180
194
  char,
195
+ ...(hasMatcher ? { matcher: registeredMatcher } : {}),
181
196
  ...(behavior ? { behavior } : {}),
182
197
  resource: getResource(),
183
198
  });
184
- }, [root, char, behavior]);
199
+ }, [root, char, hasMatcher, behavior]);
185
200
 
186
201
  const pluginRegistry = useComposerInputPluginRegistryOptional();
187
202
  useEffect(() => {
@@ -5,7 +5,7 @@ import type {
5
5
  Unstable_TriggerItem,
6
6
  } from "@assistant-ui/core";
7
7
  import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
8
- import { useEffect, useRef, type FC } from "react";
8
+ import { useEffect, useInsertionEffect, useRef, type FC } from "react";
9
9
  import { useTriggerBehaviorRegistration } from "./TriggerPopover";
10
10
  import type { TriggerBehavior } from "./triggerSelectionResource";
11
11
 
@@ -42,7 +42,9 @@ export const ComposerPrimitiveTriggerPopoverAction: FC<
42
42
  > = ({ formatter, onExecute, removeOnExecute }) => {
43
43
  const { register } = useTriggerBehaviorRegistration();
44
44
  const onExecuteRef = useRef(onExecute);
45
- onExecuteRef.current = onExecute;
45
+ useInsertionEffect(() => {
46
+ onExecuteRef.current = onExecute;
47
+ }, [onExecute]);
46
48
 
47
49
  useEffect(() => {
48
50
  const behavior: TriggerBehavior = {
@@ -6,7 +6,7 @@ import {
6
6
  type ComponentPropsWithoutRef,
7
7
  forwardRef,
8
8
  } from "react";
9
- import { composeEventHandlers } from "@radix-ui/primitive";
9
+ import { composeEventHandlers } from "radix-ui/internal";
10
10
  import { useTriggerPopoverScopeContext } from "./TriggerPopover";
11
11
 
12
12
  export namespace ComposerPrimitiveTriggerPopoverBack {
@@ -0,0 +1,109 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { Suspense, type ReactNode } from "react";
4
+ import { render, waitFor } from "@testing-library/react";
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import type { Unstable_TriggerItem } from "@assistant-ui/core";
7
+ import type { TriggerBehavior } from "./triggerSelectionResource";
8
+
9
+ const mocks = vi.hoisted(() => ({ register: vi.fn() }));
10
+
11
+ vi.mock("./TriggerPopover", async (importOriginal) => ({
12
+ ...(await importOriginal<typeof import("./TriggerPopover")>()),
13
+ useTriggerBehaviorRegistration: () => ({ register: mocks.register }),
14
+ }));
15
+
16
+ import { ComposerPrimitiveTriggerPopoverAction } from "./TriggerPopoverAction";
17
+ import { ComposerPrimitiveTriggerPopoverDirective } from "./TriggerPopoverDirective";
18
+
19
+ const item: Unstable_TriggerItem = {
20
+ id: "item-1",
21
+ type: "test",
22
+ label: "Item",
23
+ };
24
+
25
+ const verifyCommittedCallback = async (
26
+ renderBehavior: (callback: (item: Unstable_TriggerItem) => void) => ReactNode,
27
+ invoke: (behavior: TriggerBehavior) => void,
28
+ ) => {
29
+ let behavior: TriggerBehavior | undefined;
30
+ mocks.register.mockImplementation((next: TriggerBehavior) => {
31
+ behavior = next;
32
+ return () => {};
33
+ });
34
+
35
+ let suspend = false;
36
+ const suspended = new Promise<never>(() => {});
37
+ const renderProbe = vi.fn();
38
+ const Suspender = () => {
39
+ if (suspend) throw suspended;
40
+ return null;
41
+ };
42
+ const Probe = () => {
43
+ renderProbe();
44
+ return null;
45
+ };
46
+ const View = ({
47
+ callback,
48
+ }: {
49
+ callback: (item: Unstable_TriggerItem) => void;
50
+ }) => (
51
+ <Suspense fallback={null}>
52
+ {renderBehavior(callback)}
53
+ <Probe />
54
+ <Suspender />
55
+ </Suspense>
56
+ );
57
+
58
+ const committed = vi.fn();
59
+ const abandoned = vi.fn();
60
+ const view = render(<View callback={committed} />);
61
+
62
+ await waitFor(() => expect(behavior).toBeDefined());
63
+ renderProbe.mockClear();
64
+
65
+ suspend = true;
66
+ view.rerender(<View callback={abandoned} />);
67
+ await waitFor(() => expect(renderProbe).toHaveBeenCalled());
68
+
69
+ invoke(behavior!);
70
+ expect(committed).toHaveBeenCalledExactlyOnceWith(item);
71
+ expect(abandoned).not.toHaveBeenCalled();
72
+
73
+ suspend = false;
74
+ view.rerender(<View callback={abandoned} />);
75
+ invoke(behavior!);
76
+ expect(abandoned).toHaveBeenCalledExactlyOnceWith(item);
77
+ };
78
+
79
+ describe("TriggerPopover behavior callbacks", () => {
80
+ beforeEach(() => {
81
+ mocks.register.mockReset();
82
+ });
83
+
84
+ it("keeps the committed action callback during abandoned renders", async () => {
85
+ await verifyCommittedCallback(
86
+ (onExecute) => (
87
+ <ComposerPrimitiveTriggerPopoverAction onExecute={onExecute} />
88
+ ),
89
+ (behavior) => {
90
+ if (behavior.kind !== "action") throw new Error("Expected action");
91
+ behavior.onExecute(item);
92
+ },
93
+ );
94
+ });
95
+
96
+ it("keeps the committed directive callback during abandoned renders", async () => {
97
+ await verifyCommittedCallback(
98
+ (onInserted) => (
99
+ <ComposerPrimitiveTriggerPopoverDirective onInserted={onInserted} />
100
+ ),
101
+ (behavior) => {
102
+ if (behavior.kind !== "directive") {
103
+ throw new Error("Expected directive");
104
+ }
105
+ behavior.onInserted?.(item);
106
+ },
107
+ );
108
+ });
109
+ });
@@ -8,7 +8,7 @@ import {
8
8
  forwardRef,
9
9
  useCallback,
10
10
  } from "react";
11
- import { composeEventHandlers } from "@radix-ui/primitive";
11
+ import { composeEventHandlers } from "radix-ui/internal";
12
12
  import { useTriggerPopoverScopeContext } from "./TriggerPopover";
13
13
  import type { Unstable_TriggerCategory } from "@assistant-ui/core";
14
14
 
@@ -5,7 +5,7 @@ import type {
5
5
  Unstable_TriggerItem,
6
6
  } from "@assistant-ui/core";
7
7
  import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
8
- import { useEffect, useRef, type FC } from "react";
8
+ import { useEffect, useInsertionEffect, useRef, type FC } from "react";
9
9
  import { useTriggerBehaviorRegistration } from "./TriggerPopover";
10
10
  import type { TriggerBehavior } from "./triggerSelectionResource";
11
11
 
@@ -39,7 +39,9 @@ export const ComposerPrimitiveTriggerPopoverDirective: FC<
39
39
  > = ({ formatter, onInserted }) => {
40
40
  const { register } = useTriggerBehaviorRegistration();
41
41
  const onInsertedRef = useRef(onInserted);
42
- onInsertedRef.current = onInserted;
42
+ useInsertionEffect(() => {
43
+ onInsertedRef.current = onInserted;
44
+ }, [onInserted]);
43
45
 
44
46
  useEffect(() => {
45
47
  const behavior: TriggerBehavior = {
@@ -8,7 +8,7 @@ import {
8
8
  forwardRef,
9
9
  useCallback,
10
10
  } from "react";
11
- import { composeEventHandlers } from "@radix-ui/primitive";
11
+ import { composeEventHandlers } from "radix-ui/internal";
12
12
  import { useTriggerPopoverScopeContext } from "./TriggerPopover";
13
13
  import type { Unstable_TriggerItem } from "@assistant-ui/core";
14
14
 
@@ -14,6 +14,7 @@ import {
14
14
  type SelectItemOverride,
15
15
  type TriggerBehavior,
16
16
  } from "./triggerSelectionResource";
17
+ import type { TriggerMatcher } from "./detectTrigger";
17
18
 
18
19
  export type { SelectItemOverride, TriggerBehavior };
19
20
  export type { TriggerPopoverKeyEvent } from "./triggerKeyboardResource";
@@ -57,6 +58,7 @@ const useTriggerPopoverResource = ({
57
58
  adapter,
58
59
  text,
59
60
  triggerChar,
61
+ matcher,
60
62
  behavior,
61
63
  aui,
62
64
  popoverId,
@@ -65,6 +67,7 @@ const useTriggerPopoverResource = ({
65
67
  adapter: Unstable_TriggerAdapter | undefined;
66
68
  text: string;
67
69
  triggerChar: string;
70
+ matcher: TriggerMatcher | undefined;
68
71
  behavior: TriggerBehavior | undefined;
69
72
  aui: AssistantClient;
70
73
  /** Stable ID for accessible element IDs (pass React's useId() from component layer). */
@@ -72,7 +75,7 @@ const useTriggerPopoverResource = ({
72
75
  isLoading: boolean;
73
76
  }): TriggerPopoverResourceOutput => {
74
77
  const detection = useResource(
75
- TriggerDetectionResource({ text, triggerChar }),
78
+ TriggerDetectionResource({ text, triggerChar, matcher }),
76
79
  );
77
80
 
78
81
  const open =
@@ -16,9 +16,11 @@ import {
16
16
  } from "../ComposerInputPluginContext";
17
17
  import type { TriggerPopoverResourceOutput } from "./TriggerPopoverResource";
18
18
  import type { TriggerBehavior } from "./triggerSelectionResource";
19
+ import type { TriggerMatcher } from "./detectTrigger";
19
20
 
20
21
  export type RegisteredTrigger = {
21
22
  readonly char: string;
23
+ readonly matcher?: TriggerMatcher | undefined;
22
24
  /** Behavior contributed by a child `TriggerPopover.Directive` / `.Action`. */
23
25
  readonly behavior?: TriggerBehavior;
24
26
  readonly resource: TriggerPopoverResourceOutput;
@@ -1,11 +1,27 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { detectTrigger } from "./detectTrigger";
2
+ import { detectTrigger, type TriggerMatcher } from "./detectTrigger";
3
+
4
+ const matchFromLastTrigger: TriggerMatcher = (
5
+ text,
6
+ triggerChar,
7
+ cursorPosition,
8
+ ) => {
9
+ const textUpToCursor = text.slice(0, cursorPosition);
10
+ const offset = textUpToCursor.lastIndexOf(triggerChar);
11
+ if (offset === -1) return null;
12
+ return {
13
+ query: textUpToCursor.slice(offset + triggerChar.length),
14
+ offset,
15
+ endOffset: cursorPosition,
16
+ };
17
+ };
3
18
 
4
19
  describe("detectTrigger", () => {
5
20
  it("detects @query at cursor position", () => {
6
21
  expect(detectTrigger("hello @wea", "@", 10)).toEqual({
7
22
  query: "wea",
8
23
  offset: 6,
24
+ endOffset: 10,
9
25
  });
10
26
  });
11
27
 
@@ -25,14 +41,76 @@ describe("detectTrigger", () => {
25
41
  expect(detectTrigger("@foo", "@", 4)).toEqual({
26
42
  query: "foo",
27
43
  offset: 0,
44
+ endOffset: 4,
28
45
  });
29
46
  });
30
47
 
31
48
  it("stops at whitespace in query", () => {
32
- // "@foo bar" — space terminates the mention
33
49
  expect(detectTrigger("@foo bar", "@", 8)).toBeNull();
34
50
  });
35
51
 
52
+ it("uses a custom matcher for multi-word queries", () => {
53
+ expect(
54
+ detectTrigger("hello @Example UK", "@", 17, matchFromLastTrigger),
55
+ ).toEqual({
56
+ query: "Example UK",
57
+ offset: 6,
58
+ endOffset: 17,
59
+ });
60
+ });
61
+
62
+ it("keeps a matcher endOffset that is not the query length", () => {
63
+ const matchNormalized: TriggerMatcher = (
64
+ text,
65
+ triggerChar,
66
+ cursorPosition,
67
+ ) => {
68
+ const textUpToCursor = text.slice(0, cursorPosition);
69
+ const offset = textUpToCursor.lastIndexOf(triggerChar);
70
+ if (offset === -1) return null;
71
+ return {
72
+ query: "example",
73
+ offset,
74
+ endOffset: cursorPosition,
75
+ };
76
+ };
77
+
78
+ expect(
79
+ detectTrigger("hello @Example UK", "@", 17, matchNormalized),
80
+ ).toEqual({
81
+ query: "example",
82
+ offset: 6,
83
+ endOffset: 17,
84
+ });
85
+ });
86
+
87
+ it("rejects a matcher whose offset is not the trigger", () => {
88
+ const match: TriggerMatcher = () => ({
89
+ query: "x",
90
+ offset: 0,
91
+ endOffset: 2,
92
+ });
93
+ expect(detectTrigger("hello @x", "@", 8, match)).toBeNull();
94
+ });
95
+
96
+ it("rejects a matcher that matches at the caret", () => {
97
+ const match: TriggerMatcher = () => ({
98
+ query: "",
99
+ offset: 6,
100
+ endOffset: 7,
101
+ });
102
+ expect(detectTrigger("hello @foo", "@", 6, match)).toBeNull();
103
+ });
104
+
105
+ it("rejects a matcher whose endOffset is inside the trigger", () => {
106
+ const match: TriggerMatcher = () => ({
107
+ query: "",
108
+ offset: 6,
109
+ endOffset: 6,
110
+ });
111
+ expect(detectTrigger("hello @foo", "@", 10, match)).toBeNull();
112
+ });
113
+
36
114
  it("stops at newline", () => {
37
115
  expect(detectTrigger("@foo\nbar", "@", 8)).toBeNull();
38
116
  });
@@ -45,20 +123,19 @@ describe("detectTrigger", () => {
45
123
  expect(detectTrigger("hello\t@foo", "@", 10)).toEqual({
46
124
  query: "foo",
47
125
  offset: 6,
126
+ endOffset: 10,
48
127
  });
49
128
  });
50
129
 
51
130
  it("finds trigger closest to cursor, not earlier ones", () => {
52
- // Text has two @: "hello @old text @new"
53
- // Cursor at end → should find @new
54
131
  expect(detectTrigger("hello @old text @new", "@", 20)).toEqual({
55
132
  query: "new",
56
133
  offset: 16,
134
+ endOffset: 20,
57
135
  });
58
136
  });
59
137
 
60
138
  it("ignores trigger after cursor", () => {
61
- // Cursor at position 5, trigger at position 10
62
139
  expect(detectTrigger("hello text @foo", "@", 5)).toBeNull();
63
140
  });
64
141
 
@@ -66,6 +143,7 @@ describe("detectTrigger", () => {
66
143
  expect(detectTrigger("hello @@foo", "@@", 11)).toEqual({
67
144
  query: "foo",
68
145
  offset: 6,
146
+ endOffset: 11,
69
147
  });
70
148
  });
71
149
 
@@ -73,6 +151,7 @@ describe("detectTrigger", () => {
73
151
  expect(detectTrigger("hello @", "@", 7)).toEqual({
74
152
  query: "",
75
153
  offset: 6,
154
+ endOffset: 7,
76
155
  });
77
156
  });
78
157
 
@@ -80,6 +159,7 @@ describe("detectTrigger", () => {
80
159
  expect(detectTrigger("全角\u3000@foo", "@", 7)).toEqual({
81
160
  query: "foo",
82
161
  offset: 3,
162
+ endOffset: 7,
83
163
  });
84
164
  });
85
165
 
@@ -87,6 +167,7 @@ describe("detectTrigger", () => {
87
167
  expect(detectTrigger("x\u00A0@foo", "@", 6)).toEqual({
88
168
  query: "foo",
89
169
  offset: 2,
170
+ endOffset: 6,
90
171
  });
91
172
  });
92
173
 
@@ -98,6 +179,7 @@ describe("detectTrigger", () => {
98
179
  expect(detectTrigger("hello\u3000@foo", "@", 10)).toEqual({
99
180
  query: "foo",
100
181
  offset: 6,
182
+ endOffset: 10,
101
183
  });
102
184
  });
103
185
 
@@ -105,6 +187,7 @@ describe("detectTrigger", () => {
105
187
  expect(detectTrigger("hello\u00a0@foo", "@", 10)).toEqual({
106
188
  query: "foo",
107
189
  offset: 6,
190
+ endOffset: 10,
108
191
  });
109
192
  });
110
193
 
@@ -1,5 +1,43 @@
1
1
  const WHITESPACE_RE = /\s/u;
2
2
 
3
+ export type TriggerMatch = {
4
+ readonly query: string;
5
+ readonly offset: number;
6
+ readonly endOffset: number;
7
+ };
8
+
9
+ /**
10
+ * Locates a trigger query in `text` relative to `cursorPosition`.
11
+ *
12
+ * `offset` is the index of `triggerChar`. `endOffset` is the exclusive end of
13
+ * the span both the textarea and Lexical replace on select. `query` is the
14
+ * search string and may differ from that span.
15
+ *
16
+ * Return null when the caret is at or before the trigger. Escape moves the
17
+ * caret to `offset` so a match there would reopen the popover.
18
+ */
19
+ export type TriggerMatcher = (
20
+ text: string,
21
+ triggerChar: string,
22
+ cursorPosition: number,
23
+ ) => TriggerMatch | null;
24
+
25
+ export function resolveTriggerMatch(
26
+ text: string,
27
+ triggerChar: string,
28
+ cursorPosition: number,
29
+ match: TriggerMatch | null,
30
+ ): TriggerMatch | null {
31
+ if (match === null) return null;
32
+ const { query, offset, endOffset } = match;
33
+ if (offset < 0 || offset + triggerChar.length > cursorPosition) return null;
34
+ if (!text.startsWith(triggerChar, offset)) return null;
35
+ if (endOffset < offset + triggerChar.length || endOffset > text.length) {
36
+ return null;
37
+ }
38
+ return { query, offset, endOffset };
39
+ }
40
+
3
41
  /**
4
42
  * Detect a trigger character in text relative to the cursor position.
5
43
  *
@@ -9,27 +47,30 @@ export function detectTrigger(
9
47
  text: string,
10
48
  triggerChar: string,
11
49
  cursorPosition: number,
12
- ): {
13
- query: string;
14
- offset: number;
15
- } | null {
16
- // Only consider text up to the cursor
50
+ matcher?: TriggerMatcher | undefined,
51
+ ): TriggerMatch | null {
52
+ if (matcher) {
53
+ return resolveTriggerMatch(
54
+ text,
55
+ triggerChar,
56
+ cursorPosition,
57
+ matcher(text, triggerChar, cursorPosition),
58
+ );
59
+ }
60
+
17
61
  const textUpToCursor = text.slice(0, cursorPosition);
18
62
 
19
- // Search backwards from cursor for the trigger character.
20
- // Stop at any whitespace during scan — trigger must be contiguous with cursor.
21
63
  for (let i = textUpToCursor.length - 1; i >= 0; i--) {
22
64
  const char = textUpToCursor[i]!;
23
65
 
24
66
  if (WHITESPACE_RE.test(char)) return null;
25
67
 
26
68
  if (textUpToCursor.startsWith(triggerChar, i)) {
27
- // Trigger must be preceded by whitespace or be at start of text
28
69
  if (i > 0 && !WHITESPACE_RE.test(textUpToCursor[i - 1]!)) continue;
29
70
 
30
71
  const query = textUpToCursor.slice(i + triggerChar.length);
31
72
 
32
- return { query, offset: i };
73
+ return { query, offset: i, endOffset: cursorPosition };
33
74
  }
34
75
  }
35
76
 
@@ -21,6 +21,7 @@ export {
21
21
  } from "./TriggerPopoverItems";
22
22
  export { ComposerPrimitiveTriggerPopoverBack } from "./TriggerPopoverBack";
23
23
  export type { TriggerBehavior } from "./triggerSelectionResource";
24
+ export type { TriggerMatch, TriggerMatcher } from "./detectTrigger";
24
25
 
25
26
  import { ComposerPrimitiveTriggerPopover as Base } from "./TriggerPopover";
26
27
  import { ComposerPrimitiveTriggerPopoverAction } from "./TriggerPopoverAction";
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { matchesTriggerItemQuery } from "./matchesTriggerItemQuery";
3
+ import type { Unstable_TriggerItem } from "@assistant-ui/core";
4
+
5
+ const item = (
6
+ overrides: Partial<Unstable_TriggerItem>,
7
+ ): Unstable_TriggerItem => ({
8
+ id: "summarize",
9
+ type: "command",
10
+ label: "Summarize",
11
+ ...overrides,
12
+ });
13
+
14
+ describe("matchesTriggerItemQuery", () => {
15
+ it("matches every item on an empty query", () => {
16
+ expect(matchesTriggerItemQuery(item({}), "")).toBe(true);
17
+ });
18
+
19
+ it("matches case-insensitively against the id", () => {
20
+ expect(
21
+ matchesTriggerItemQuery(item({ id: "SummArize", label: "y" }), "marize"),
22
+ ).toBe(true);
23
+ });
24
+
25
+ it("matches case-insensitively against the label", () => {
26
+ expect(
27
+ matchesTriggerItemQuery(
28
+ item({ id: "x", label: "Translate Text" }),
29
+ "late",
30
+ ),
31
+ ).toBe(true);
32
+ });
33
+
34
+ it("matches against the description when present", () => {
35
+ expect(
36
+ matchesTriggerItemQuery(
37
+ item({ id: "x", label: "y", description: "Condense the thread" }),
38
+ "condense",
39
+ ),
40
+ ).toBe(true);
41
+ });
42
+
43
+ it("does not match when no field contains the query", () => {
44
+ expect(
45
+ matchesTriggerItemQuery(item({ description: "nothing here" }), "zzz"),
46
+ ).toBe(false);
47
+ });
48
+
49
+ it("does not throw on items without a description", () => {
50
+ expect(matchesTriggerItemQuery(item({}), "zzz")).toBe(false);
51
+ });
52
+ });