@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
@@ -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";
@@ -1,12 +1,13 @@
1
1
  import { useMemo, useState } from "react";
2
2
  import { resource } from "@assistant-ui/tap";
3
- import { detectTrigger } from "./detectTrigger";
3
+ import {
4
+ detectTrigger,
5
+ type TriggerMatch,
6
+ type TriggerMatcher,
7
+ } from "./detectTrigger";
4
8
 
5
9
  /** Detected trigger position within the composer text. */
6
- export type DetectedTrigger = {
7
- readonly offset: number;
8
- readonly query: string;
9
- };
10
+ export type DetectedTrigger = TriggerMatch;
10
11
 
11
12
  export type TriggerDetectionResourceOutput = {
12
13
  /** Detected trigger (or `null` when inactive). */
@@ -21,16 +22,18 @@ export type TriggerDetectionResourceOutput = {
21
22
  const useTriggerDetectionResource = ({
22
23
  text,
23
24
  triggerChar,
25
+ matcher,
24
26
  }: {
25
27
  text: string;
26
28
  triggerChar: string;
29
+ matcher?: TriggerMatcher | undefined;
27
30
  }): TriggerDetectionResourceOutput => {
28
31
  const [cursorPosition, setCursorPosition] = useState(text.length);
29
32
 
30
33
  const trigger = useMemo(() => {
31
34
  const pos = Math.min(cursorPosition, text.length);
32
- return detectTrigger(text, triggerChar, pos);
33
- }, [cursorPosition, text, triggerChar]);
35
+ return detectTrigger(text, triggerChar, pos, matcher);
36
+ }, [cursorPosition, matcher, text, triggerChar]);
34
37
 
35
38
  const query = trigger?.query ?? "";
36
39
 
@@ -0,0 +1,147 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createTapRoot, flushTapSync, useResource } from "@assistant-ui/tap";
3
+ import type {
4
+ Unstable_DirectiveFormatter,
5
+ Unstable_TriggerItem,
6
+ } from "@assistant-ui/core";
7
+ import type { AssistantClient } from "@assistant-ui/store";
8
+ import { TriggerDetectionResource } from "./triggerDetectionResource";
9
+ import {
10
+ TriggerSelectionResource,
11
+ type TriggerBehavior,
12
+ } from "./triggerSelectionResource";
13
+
14
+ const item: Unstable_TriggerItem = {
15
+ id: "file",
16
+ type: "file",
17
+ label: "file.txt",
18
+ };
19
+
20
+ const formatter: Unstable_DirectiveFormatter = {
21
+ serialize: () => "@file.txt",
22
+ parse: () => [],
23
+ };
24
+
25
+ const makeAui = (textRef: { value: string }) => {
26
+ const setText = vi.fn((value: string) => {
27
+ textRef.value = value;
28
+ });
29
+
30
+ return {
31
+ aui: {
32
+ composer: {
33
+ getState: () => ({ text: textRef.value }),
34
+ setText,
35
+ },
36
+ } as unknown as AssistantClient,
37
+ setText,
38
+ };
39
+ };
40
+
41
+ const render = ({
42
+ behavior,
43
+ text,
44
+ trigger,
45
+ }: {
46
+ behavior: TriggerBehavior;
47
+ text: string;
48
+ trigger: { offset: number; query: string; endOffset: number };
49
+ }) => {
50
+ const textRef = { value: text };
51
+ const { aui, setText } = makeAui(textRef);
52
+ const setCursorPosition = vi.fn();
53
+ const onSelected = vi.fn();
54
+ const root = createTapRoot(function Root() {
55
+ return useResource(
56
+ TriggerSelectionResource({
57
+ behavior,
58
+ trigger,
59
+ aui,
60
+ triggerChar: "@",
61
+ setCursorPosition,
62
+ onSelected,
63
+ }),
64
+ );
65
+ });
66
+
67
+ return { root, setText, setCursorPosition, onSelected };
68
+ };
69
+
70
+ describe("TriggerSelectionResource", () => {
71
+ it("resynchronizes the cursor after inserting a directive", () => {
72
+ const { root, setText, setCursorPosition, onSelected } = render({
73
+ behavior: { kind: "directive", formatter },
74
+ text: "@file tail",
75
+ trigger: { offset: 0, query: "file", endOffset: 5 },
76
+ });
77
+
78
+ root.getValue().selectItem(item);
79
+
80
+ expect(setText).toHaveBeenCalledWith("@file.txt tail");
81
+ expect(setCursorPosition).toHaveBeenCalledWith("@file.txt".length + 1);
82
+ expect(onSelected).toHaveBeenCalledOnce();
83
+ });
84
+
85
+ it("resynchronizes the cursor after removing an action", () => {
86
+ const { root, setText, setCursorPosition } = render({
87
+ behavior: {
88
+ kind: "action",
89
+ formatter,
90
+ onExecute: vi.fn(),
91
+ removeOnExecute: true,
92
+ },
93
+ text: "before @file tail",
94
+ trigger: { offset: 7, query: "file", endOffset: 12 },
95
+ });
96
+
97
+ root.getValue().selectItem(item);
98
+
99
+ expect(setText).toHaveBeenCalledWith("before tail");
100
+ expect(setCursorPosition).toHaveBeenCalledWith(7);
101
+ });
102
+
103
+ it("does not rewrite composer text on a second select after insertion", () => {
104
+ const textRef = { value: "@fil" };
105
+ const { aui, setText } = makeAui(textRef);
106
+ const onSelected = vi.fn();
107
+ const root = createTapRoot(function Root() {
108
+ const detection = useResource(
109
+ TriggerDetectionResource({
110
+ text: textRef.value,
111
+ triggerChar: "@",
112
+ }),
113
+ );
114
+ return useResource(
115
+ TriggerSelectionResource({
116
+ behavior: { kind: "directive", formatter },
117
+ trigger: detection.trigger,
118
+ aui,
119
+ triggerChar: "@",
120
+ setCursorPosition: detection.setCursorPosition,
121
+ onSelected,
122
+ }),
123
+ );
124
+ });
125
+
126
+ flushTapSync(() => {
127
+ root.getValue().selectItem(item);
128
+ });
129
+ root.getValue().selectItem(item);
130
+
131
+ expect(setText).toHaveBeenCalledOnce();
132
+ expect(setText).toHaveBeenCalledWith("@file.txt ");
133
+ expect(textRef.value).toBe("@file.txt ");
134
+ });
135
+
136
+ it("replaces the matcher endOffset even when query is shorter", () => {
137
+ const { root, setText } = render({
138
+ behavior: { kind: "directive", formatter },
139
+ text: "hello @Example UK tail",
140
+ trigger: { offset: 6, query: "example", endOffset: 17 },
141
+ });
142
+
143
+ root.getValue().selectItem(item);
144
+
145
+ expect(setText).toHaveBeenCalledWith("hello @file.txt tail");
146
+ });
147
+ });
@@ -37,7 +37,6 @@ const useTriggerSelectionResource = ({
37
37
  behavior,
38
38
  trigger,
39
39
  aui,
40
- triggerChar,
41
40
  setCursorPosition,
42
41
  onSelected,
43
42
  }: {
@@ -74,15 +73,14 @@ const useTriggerSelectionResource = ({
74
73
 
75
74
  const currentText = aui.composer.getState().text;
76
75
  const before = currentText.slice(0, trigger.offset);
77
- const after = currentText.slice(
78
- trigger.offset + triggerChar.length + trigger.query.length,
79
- );
76
+ const after = currentText.slice(trigger.endOffset);
80
77
 
81
78
  const insertDirective = () => {
82
79
  const directive = behavior.formatter.serialize(item);
83
80
  aui.composer.setText(
84
81
  before + directive + (after.startsWith(" ") ? after : ` ${after}`),
85
82
  );
83
+ setCursorPosition(before.length + directive.length + 1);
86
84
  };
87
85
 
88
86
  if (behavior.kind === "directive") {
@@ -93,6 +91,7 @@ const useTriggerSelectionResource = ({
93
91
  aui.composer.setText(
94
92
  before + (after.startsWith(" ") ? after.slice(1) : after),
95
93
  );
94
+ setCursorPosition(before.length);
96
95
  } else {
97
96
  // Leave directive chip in the composer as an audit trail
98
97
  insertDirective();
@@ -29,3 +29,4 @@ export { useTriggerPopoverScopeContextOptional as unstable_useTriggerPopoverScop
29
29
  export { useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers } from "./composer/trigger";
30
30
  export { useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional } from "./composer/trigger";
31
31
  export type { RegisteredTrigger as Unstable_RegisteredTrigger } from "./composer/trigger";
32
+ export type { TriggerMatcher as Unstable_TriggerMatcher } from "./composer/trigger";
@@ -10,7 +10,7 @@ import {
10
10
  } from "react";
11
11
  import { useAui, useAuiState } from "@assistant-ui/store";
12
12
  import { useManagedRef } from "../../utils/hooks/useManagedRef";
13
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
13
+ import { useComposedRefs } from "radix-ui/internal";
14
14
  import {
15
15
  useThreadViewport,
16
16
  useThreadViewportStore,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { Primitive } from "../../utils/Primitive";
4
- import { composeEventHandlers } from "@radix-ui/primitive";
4
+ import { composeEventHandlers } from "radix-ui/internal";
5
5
  import {
6
6
  type ComponentPropsWithoutRef,
7
7
  type ComponentRef,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { Primitive } from "../../utils/Primitive";
4
- import { composeEventHandlers } from "@radix-ui/primitive";
4
+ import { composeEventHandlers } from "radix-ui/internal";
5
5
  import {
6
6
  type ComponentPropsWithoutRef,
7
7
  type ComponentRef,
@@ -24,6 +24,9 @@ export namespace ThreadPrimitiveRoot {
24
24
  * This component serves as the foundational wrapper for all thread-related components.
25
25
  * It provides the basic structure and context needed for thread functionality.
26
26
  *
27
+ * While this component is mounted, an unhandled Escape keydown stops active speech, even if
28
+ * the action bar that started it is no longer mounted.
29
+ *
27
30
  * @example
28
31
  * ```tsx
29
32
  * <ThreadPrimitive.Root>
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
3
+ import { useComposedRefs } from "radix-ui/internal";
4
4
  import { Primitive } from "../../utils/Primitive";
5
5
  import {
6
6
  type ComponentRef,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
3
+ import { useComposedRefs } from "radix-ui/internal";
4
4
  import { Primitive } from "../../utils/Primitive";
5
5
  import {
6
6
  type ComponentRef,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
3
+ import { useComposedRefs } from "radix-ui/internal";
4
4
  import { useCallback, useLayoutEffect, useRef, type RefCallback } from "react";
5
5
  import { useAuiEvent, useAuiState } from "@assistant-ui/store";
6
6
  import { isUserScrollUp } from "@assistant-ui/store/client";
@@ -6,7 +6,7 @@ import type {
6
6
  } from "../../utils/createActionButton";
7
7
  import { forwardRef } from "react";
8
8
  import { Primitive } from "../../utils/Primitive";
9
- import { composeEventHandlers } from "@radix-ui/primitive";
9
+ import { composeEventHandlers } from "radix-ui/internal";
10
10
  import { useAuiState } from "@assistant-ui/store";
11
11
  import { useThreadListNew as useThreadListNewBehavior } from "@assistant-ui/core/react";
12
12
 
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { createCollection } from "@radix-ui/react-collection";
3
+ import { Collection } from "radix-ui/internal";
4
4
  import { createContext, useContext, type RefObject } from "react";
5
5
 
6
6
  const [collection, useThreadListCollection] =
7
- createCollection<HTMLButtonElement>("ThreadList");
7
+ Collection.createCollection<HTMLButtonElement>("ThreadList");
8
8
 
9
9
  export const ThreadListCollection = collection;
10
10
  export { useThreadListCollection };
@@ -10,7 +10,7 @@ import {
10
10
  useRef,
11
11
  } from "react";
12
12
  import { useAuiState } from "@assistant-ui/store";
13
- import { composeEventHandlers } from "@radix-ui/primitive";
13
+ import { composeEventHandlers } from "radix-ui/internal";
14
14
  import { Direction } from "radix-ui";
15
15
  import {
16
16
  ThreadListItemFocusProvider,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
4
+ import { useComposedRefs } from "radix-ui/internal";
5
5
  import {
6
6
  type ActionButtonElement,
7
7
  type ActionButtonProps,
@@ -6,7 +6,7 @@ import {
6
6
  forwardRef,
7
7
  } from "react";
8
8
  import { Direction, DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
9
- import { composeEventHandlers } from "@radix-ui/primitive";
9
+ import { composeEventHandlers } from "radix-ui/internal";
10
10
  import type { WithRenderPropProps } from "../../utils/Primitive";
11
11
  import { DropdownMenuRenderContent } from "../dropdownMenuRenderPrimitives";
12
12
  import { useThreadListItemFocus } from "../threadListFocusGroup";
@@ -8,7 +8,7 @@ import {
8
8
  useContext,
9
9
  } from "react";
10
10
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
11
- import { useControllableState } from "@radix-ui/react-use-controllable-state";
11
+ import { useControllableState } from "radix-ui/internal";
12
12
  import { type ScopedProps, useDropdownMenuScope } from "./scope";
13
13
 
14
14
  const ThreadListItemMoreSetOpenContext = createContext<
@@ -5,8 +5,8 @@ import {
5
5
  Direction,
6
6
  type DropdownMenu as DropdownMenuPrimitive,
7
7
  } from "radix-ui";
8
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
9
- import { composeEventHandlers } from "@radix-ui/primitive";
8
+ import { useComposedRefs } from "radix-ui/internal";
9
+ import { composeEventHandlers } from "radix-ui/internal";
10
10
  import type { WithRenderPropProps } from "../../utils/Primitive";
11
11
  import { DropdownMenuRenderTrigger } from "../dropdownMenuRenderPrimitives";
12
12
  import { useThreadListItemFocus } from "../threadListFocusGroup";
@@ -1,8 +1,8 @@
1
1
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
2
- import type { Scope } from "@radix-ui/react-context";
2
+ import type { Context } from "radix-ui/internal";
3
3
 
4
4
  export const useDropdownMenuScope: ReturnType<
5
5
  typeof DropdownMenuPrimitive.createDropdownMenuScope
6
6
  > = DropdownMenuPrimitive.createDropdownMenuScope();
7
7
 
8
- export type ScopedProps<P> = P & { __scopeThreadListItemMore?: Scope };
8
+ export type ScopedProps<P> = P & { __scopeThreadListItemMore?: Context.Scope };