@assistant-ui/react 0.12.22 → 0.12.24

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 (136) hide show
  1. package/dist/client/ExternalThread.d.ts.map +1 -1
  2. package/dist/client/ExternalThread.js +1 -0
  3. package/dist/client/ExternalThread.js.map +1 -1
  4. package/dist/client/InMemoryThreadList.d.ts.map +1 -1
  5. package/dist/client/InMemoryThreadList.js +2 -0
  6. package/dist/client/InMemoryThreadList.js.map +1 -1
  7. package/dist/client/SingleThreadList.d.ts.map +1 -1
  8. package/dist/client/SingleThreadList.js +2 -0
  9. package/dist/client/SingleThreadList.js.map +1 -1
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/internal.d.ts +1 -0
  15. package/dist/internal.d.ts.map +1 -1
  16. package/dist/internal.js +2 -0
  17. package/dist/internal.js.map +1 -1
  18. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  19. package/dist/primitives/composer/ComposerInput.js +27 -12
  20. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  21. package/dist/primitives/composer/ComposerInputPluginContext.d.ts +31 -0
  22. package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -0
  23. package/dist/primitives/composer/ComposerInputPluginContext.js +32 -0
  24. package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -0
  25. package/dist/primitives/composer/mention/ComposerMentionContext.d.ts +4 -2
  26. package/dist/primitives/composer/mention/ComposerMentionContext.d.ts.map +1 -1
  27. package/dist/primitives/composer/mention/ComposerMentionContext.js +21 -13
  28. package/dist/primitives/composer/mention/ComposerMentionContext.js.map +1 -1
  29. package/dist/primitives/composer/mention/index.d.ts +4 -4
  30. package/dist/primitives/composer/mention/index.d.ts.map +1 -1
  31. package/dist/primitives/composer/mention/index.js +6 -4
  32. package/dist/primitives/composer/mention/index.js.map +1 -1
  33. package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.d.ts +36 -0
  34. package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.d.ts.map +1 -0
  35. package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.js +36 -0
  36. package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.js.map +1 -0
  37. package/dist/primitives/composer/slash-command/index.d.ts +2 -0
  38. package/dist/primitives/composer/slash-command/index.d.ts.map +1 -0
  39. package/dist/primitives/composer/slash-command/index.js +2 -0
  40. package/dist/primitives/composer/slash-command/index.js.map +1 -0
  41. package/dist/primitives/composer/{mention/ComposerMentionBack.d.ts → trigger/TriggerPopoverBack.d.ts} +3 -10
  42. package/dist/primitives/composer/trigger/TriggerPopoverBack.d.ts.map +1 -0
  43. package/dist/primitives/composer/trigger/TriggerPopoverBack.js +19 -0
  44. package/dist/primitives/composer/trigger/TriggerPopoverBack.js.map +1 -0
  45. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +38 -0
  46. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -0
  47. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +35 -0
  48. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -0
  49. package/dist/primitives/composer/trigger/TriggerPopoverContext.d.ts +37 -0
  50. package/dist/primitives/composer/trigger/TriggerPopoverContext.d.ts.map +1 -0
  51. package/dist/primitives/composer/trigger/TriggerPopoverContext.js +70 -0
  52. package/dist/primitives/composer/trigger/TriggerPopoverContext.js.map +1 -0
  53. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +40 -0
  54. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -0
  55. package/dist/primitives/composer/trigger/TriggerPopoverItems.js +35 -0
  56. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -0
  57. package/dist/primitives/composer/trigger/TriggerPopoverPopover.d.ts +26 -0
  58. package/dist/primitives/composer/trigger/TriggerPopoverPopover.d.ts.map +1 -0
  59. package/dist/primitives/composer/trigger/TriggerPopoverPopover.js +28 -0
  60. package/dist/primitives/composer/trigger/TriggerPopoverPopover.js.map +1 -0
  61. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +53 -0
  62. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -0
  63. package/dist/primitives/composer/{mention/MentionResource.js → trigger/TriggerPopoverResource.js} +50 -25
  64. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -0
  65. package/dist/primitives/composer/trigger/detectTrigger.d.ts +2 -0
  66. package/dist/primitives/composer/trigger/detectTrigger.d.ts.map +1 -0
  67. package/dist/primitives/composer/{mention/detectMentionTrigger.js → trigger/detectTrigger.js} +4 -4
  68. package/dist/primitives/composer/trigger/detectTrigger.js.map +1 -0
  69. package/dist/primitives/composer/trigger/index.d.ts +7 -0
  70. package/dist/primitives/composer/trigger/index.d.ts.map +1 -0
  71. package/dist/primitives/composer/trigger/index.js +6 -0
  72. package/dist/primitives/composer/trigger/index.js.map +1 -0
  73. package/dist/primitives/composer.d.ts +10 -0
  74. package/dist/primitives/composer.d.ts.map +1 -1
  75. package/dist/primitives/composer.js +14 -0
  76. package/dist/primitives/composer.js.map +1 -1
  77. package/dist/primitives/message/MessageRoot.d.ts +25 -3
  78. package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
  79. package/dist/primitives/message/MessageRoot.js +2 -2
  80. package/dist/primitives/message/MessageRoot.js.map +1 -1
  81. package/dist/primitives/thread/ThreadViewportSlack.d.ts +2 -2
  82. package/dist/primitives/thread/ThreadViewportSlack.d.ts.map +1 -1
  83. package/dist/unstable/useSlashCommandAdapter.d.ts +34 -0
  84. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -0
  85. package/dist/unstable/useSlashCommandAdapter.js +50 -0
  86. package/dist/unstable/useSlashCommandAdapter.js.map +1 -0
  87. package/package.json +7 -7
  88. package/src/client/ExternalThread.ts +1 -0
  89. package/src/client/InMemoryThreadList.ts +3 -0
  90. package/src/client/SingleThreadList.ts +2 -0
  91. package/src/index.ts +14 -0
  92. package/src/internal.ts +3 -0
  93. package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.test.ts +186 -3
  94. package/src/primitives/composer/ComposerInput.tsx +25 -18
  95. package/src/primitives/composer/ComposerInputPluginContext.tsx +100 -0
  96. package/src/primitives/composer/mention/ComposerMentionContext.tsx +56 -22
  97. package/src/primitives/composer/mention/index.ts +11 -8
  98. package/src/primitives/composer/slash-command/ComposerSlashCommandRoot.tsx +76 -0
  99. package/src/primitives/composer/slash-command/index.ts +1 -0
  100. package/src/primitives/composer/trigger/TriggerPopoverBack.tsx +40 -0
  101. package/src/primitives/composer/{mention/ComposerMentionCategories.tsx → trigger/TriggerPopoverCategories.tsx} +33 -28
  102. package/src/primitives/composer/trigger/TriggerPopoverContext.tsx +129 -0
  103. package/src/primitives/composer/{mention/ComposerMentionItems.tsx → trigger/TriggerPopoverItems.tsx} +34 -29
  104. package/src/primitives/composer/trigger/TriggerPopoverPopover.tsx +51 -0
  105. package/src/primitives/composer/{mention/MentionResource.ts → trigger/TriggerPopoverResource.ts} +146 -98
  106. package/src/primitives/composer/{mention/detectMentionTrigger.test.ts → trigger/detectTrigger.test.ts} +15 -15
  107. package/src/primitives/composer/{mention/detectMentionTrigger.ts → trigger/detectTrigger.ts} +3 -3
  108. package/src/primitives/composer/trigger/index.ts +16 -0
  109. package/src/primitives/composer.ts +16 -0
  110. package/src/primitives/message/MessageRoot.tsx +18 -4
  111. package/src/primitives/thread/ThreadViewportSlack.tsx +2 -2
  112. package/src/tests/BaseComposerRuntimeCore.test.ts +33 -1
  113. package/src/unstable/useSlashCommandAdapter.ts +83 -0
  114. package/dist/primitives/composer/mention/ComposerMentionBack.d.ts.map +0 -1
  115. package/dist/primitives/composer/mention/ComposerMentionBack.js +0 -28
  116. package/dist/primitives/composer/mention/ComposerMentionBack.js.map +0 -1
  117. package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts +0 -46
  118. package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts.map +0 -1
  119. package/dist/primitives/composer/mention/ComposerMentionCategories.js +0 -32
  120. package/dist/primitives/composer/mention/ComposerMentionCategories.js.map +0 -1
  121. package/dist/primitives/composer/mention/ComposerMentionItems.d.ts +0 -50
  122. package/dist/primitives/composer/mention/ComposerMentionItems.d.ts.map +0 -1
  123. package/dist/primitives/composer/mention/ComposerMentionItems.js +0 -30
  124. package/dist/primitives/composer/mention/ComposerMentionItems.js.map +0 -1
  125. package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts +0 -26
  126. package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts.map +0 -1
  127. package/dist/primitives/composer/mention/ComposerMentionPopover.js +0 -28
  128. package/dist/primitives/composer/mention/ComposerMentionPopover.js.map +0 -1
  129. package/dist/primitives/composer/mention/MentionResource.d.ts +0 -39
  130. package/dist/primitives/composer/mention/MentionResource.d.ts.map +0 -1
  131. package/dist/primitives/composer/mention/MentionResource.js.map +0 -1
  132. package/dist/primitives/composer/mention/detectMentionTrigger.d.ts +0 -2
  133. package/dist/primitives/composer/mention/detectMentionTrigger.d.ts.map +0 -1
  134. package/dist/primitives/composer/mention/detectMentionTrigger.js.map +0 -1
  135. package/src/primitives/composer/mention/ComposerMentionBack.tsx +0 -55
  136. package/src/primitives/composer/mention/ComposerMentionPopover.tsx +0 -52
@@ -19,7 +19,13 @@ const createState = (
19
19
  const createAssistantMessage = (
20
20
  argsText: string,
21
21
  args: Record<string, unknown>,
22
- options?: { result?: ReadonlyJSONValue; isError?: boolean },
22
+ options?: {
23
+ result?: ReadonlyJSONValue;
24
+ isError?: boolean;
25
+ toolCallId?: string;
26
+ toolName?: string;
27
+ nestedMessages?: ThreadAssistantMessage[];
28
+ },
23
29
  ): ThreadAssistantMessage => ({
24
30
  id: "m-1",
25
31
  role: "assistant",
@@ -35,12 +41,13 @@ const createAssistantMessage = (
35
41
  content: [
36
42
  {
37
43
  type: "tool-call",
38
- toolCallId: "tool-1",
39
- toolName: "weatherSearch",
44
+ toolCallId: options?.toolCallId ?? "tool-1",
45
+ toolName: options?.toolName ?? "weatherSearch",
40
46
  args: args as ReadonlyJSONObject,
41
47
  argsText,
42
48
  ...(options?.result !== undefined && { result: options.result }),
43
49
  ...(options?.isError !== undefined && { isError: options.isError }),
50
+ ...(options?.nestedMessages && { messages: options.nestedMessages }),
44
51
  },
45
52
  ],
46
53
  });
@@ -228,6 +235,182 @@ describe("useToolInvocations", () => {
228
235
  expect(Object.keys(statuses)).not.toContain("tool-1:rewrite:0");
229
236
  });
230
237
 
238
+ it("does not execute tool calls loaded asynchronously with existing results", async () => {
239
+ const execute = vi.fn(async () => ({ forecast: "ok" }));
240
+ const getTools = () => ({
241
+ weatherSearch: {
242
+ parameters: { type: "object", properties: {} },
243
+ execute,
244
+ } satisfies Tool,
245
+ });
246
+ const onResult = vi.fn();
247
+ const setToolStatuses = vi.fn();
248
+
249
+ const { rerender } = renderHook(
250
+ ({ state }: { state: AssistantTransportState }) =>
251
+ useToolInvocations({
252
+ state,
253
+ getTools,
254
+ onResult,
255
+ setToolStatuses,
256
+ }),
257
+ {
258
+ initialProps: {
259
+ state: createState([]),
260
+ },
261
+ },
262
+ );
263
+
264
+ act(() => {
265
+ rerender({
266
+ state: createState([
267
+ createAssistantMessage(
268
+ '{"query":"London"}',
269
+ { query: "London" },
270
+ { result: { source: "history" } },
271
+ ),
272
+ ]),
273
+ });
274
+ });
275
+
276
+ await waitFor(() => {
277
+ expect(execute).not.toHaveBeenCalled();
278
+ expect(onResult).not.toHaveBeenCalled();
279
+ });
280
+ });
281
+
282
+ it("does not re-execute asynchronously loaded resolved tool calls after reset", async () => {
283
+ const execute = vi.fn(async () => ({ forecast: "ok" }));
284
+ const getTools = () => ({
285
+ weatherSearch: {
286
+ parameters: { type: "object", properties: {} },
287
+ execute,
288
+ } satisfies Tool,
289
+ });
290
+ const onResult = vi.fn();
291
+ const setToolStatuses = vi.fn();
292
+
293
+ const { result, rerender } = renderHook(
294
+ ({ state }: { state: AssistantTransportState }) =>
295
+ useToolInvocations({
296
+ state,
297
+ getTools,
298
+ onResult,
299
+ setToolStatuses,
300
+ }),
301
+ {
302
+ initialProps: {
303
+ state: createState([]),
304
+ },
305
+ },
306
+ );
307
+
308
+ act(() => {
309
+ rerender({
310
+ state: createState([
311
+ createAssistantMessage('{"query":"London"}', { query: "London" }),
312
+ ]),
313
+ });
314
+ });
315
+
316
+ await waitFor(() => {
317
+ expect(execute).toHaveBeenCalledTimes(1);
318
+ });
319
+
320
+ act(() => {
321
+ result.current.reset();
322
+ });
323
+
324
+ await act(async () => {
325
+ await Promise.resolve();
326
+ });
327
+
328
+ act(() => {
329
+ rerender({
330
+ state: createState([]),
331
+ });
332
+ });
333
+
334
+ act(() => {
335
+ rerender({
336
+ state: createState([
337
+ createAssistantMessage(
338
+ '{"query":"London"}',
339
+ { query: "London" },
340
+ { result: { source: "history" } },
341
+ ),
342
+ ]),
343
+ });
344
+ });
345
+
346
+ await waitFor(() => {
347
+ expect(execute).toHaveBeenCalledTimes(1);
348
+ expect(onResult).toHaveBeenCalledTimes(1);
349
+ });
350
+ });
351
+
352
+ it("still processes nested unresolved tool calls when the parent tool call is already resolved", async () => {
353
+ const executeParent = vi.fn(async () => ({ scope: "parent" }));
354
+ const executeChild = vi.fn(async () => ({ scope: "child" }));
355
+ const getTools = () => ({
356
+ resolvedOnly: {
357
+ parameters: { type: "object", properties: {} },
358
+ execute: executeParent,
359
+ } satisfies Tool,
360
+ childTool: {
361
+ parameters: { type: "object", properties: {} },
362
+ execute: executeChild,
363
+ } satisfies Tool,
364
+ });
365
+ const onResult = vi.fn();
366
+ const setToolStatuses = vi.fn();
367
+
368
+ const nestedMessage = createAssistantMessage(
369
+ '{"query":"nested"}',
370
+ { query: "nested" },
371
+ {
372
+ toolCallId: "tool-child",
373
+ toolName: "childTool",
374
+ },
375
+ );
376
+
377
+ const { rerender } = renderHook(
378
+ ({ state }: { state: AssistantTransportState }) =>
379
+ useToolInvocations({
380
+ state,
381
+ getTools,
382
+ onResult,
383
+ setToolStatuses,
384
+ }),
385
+ {
386
+ initialProps: {
387
+ state: createState([]),
388
+ },
389
+ },
390
+ );
391
+
392
+ act(() => {
393
+ rerender({
394
+ state: createState([
395
+ createAssistantMessage(
396
+ '{"query":"parent"}',
397
+ { query: "parent" },
398
+ {
399
+ result: { source: "history" },
400
+ toolName: "resolvedOnly",
401
+ nestedMessages: [nestedMessage],
402
+ },
403
+ ),
404
+ ]),
405
+ });
406
+ });
407
+
408
+ await waitFor(() => {
409
+ expect(executeParent).not.toHaveBeenCalled();
410
+ expect(executeChild).toHaveBeenCalledTimes(1);
411
+ });
412
+ });
413
+
231
414
  it("does not close args stream early for non-executable tool snapshots", () => {
232
415
  const getTools = () => ({
233
416
  weatherSearch: {
@@ -22,10 +22,7 @@ import { useEscapeKeydown } from "@radix-ui/react-use-escape-keydown";
22
22
  import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom";
23
23
  import { useAuiState, useAui } from "@assistant-ui/store";
24
24
  import { flushResourcesSync } from "@assistant-ui/tap";
25
- import {
26
- useMentionContextOptional,
27
- useMentionInternalContext,
28
- } from "./mention/ComposerMentionContext";
25
+ import { useComposerInputPluginRegistryOptional } from "./ComposerInputPluginContext";
29
26
 
30
27
  export namespace ComposerPrimitiveInput {
31
28
  export type Element = HTMLTextAreaElement;
@@ -144,8 +141,7 @@ export const ComposerPrimitiveInput = forwardRef<
144
141
  forwardedRef,
145
142
  ) => {
146
143
  const aui = useAui();
147
- const mentionContext = useMentionContextOptional();
148
- const mentionInternalContext = useMentionInternalContext();
144
+ const pluginRegistry = useComposerInputPluginRegistryOptional();
149
145
 
150
146
  const effectiveSubmitMode =
151
147
  submitMode ?? (submitOnEnter === false ? "none" : "enter");
@@ -166,10 +162,11 @@ export const ComposerPrimitiveInput = forwardRef<
166
162
  // Only handle ESC if it originated from within this input
167
163
  if (!textareaRef.current?.contains(e.target as Node)) return;
168
164
 
169
- // Let mention popover handle Escape first
170
- if (mentionContext?.open) {
171
- mentionContext.handleKeyDown(e);
172
- return;
165
+ // Let registered plugins (mention, slash command, etc.) handle Escape first
166
+ if (pluginRegistry) {
167
+ for (const plugin of pluginRegistry.getPlugins()) {
168
+ if (plugin.handleKeyDown(e)) return;
169
+ }
173
170
  }
174
171
 
175
172
  if (!cancelOnEscape) return;
@@ -187,8 +184,12 @@ export const ComposerPrimitiveInput = forwardRef<
187
184
  // ignore IME composition events
188
185
  if (e.nativeEvent.isComposing) return;
189
186
 
190
- // Let the mention popover handle keyboard events first
191
- if (mentionContext?.handleKeyDown(e)) return;
187
+ // Let registered plugins (mention, slash command, etc.) handle keyboard events first
188
+ if (pluginRegistry) {
189
+ for (const plugin of pluginRegistry.getPlugins()) {
190
+ if (plugin.handleKeyDown(e)) return;
191
+ }
192
+ }
192
193
 
193
194
  if (e.key === "Enter") {
194
195
  const threadState = aui.thread().getState();
@@ -297,9 +298,12 @@ export const ComposerPrimitiveInput = forwardRef<
297
298
  flushResourcesSync(() => {
298
299
  aui.composer().setText(e.target.value);
299
300
  });
300
- mentionInternalContext?.setCursorPosition(
301
- e.target.selectionStart ?? e.target.value.length,
302
- );
301
+ const pos = e.target.selectionStart ?? e.target.value.length;
302
+ if (pluginRegistry) {
303
+ for (const plugin of pluginRegistry.getPlugins()) {
304
+ plugin.setCursorPosition(pos);
305
+ }
306
+ }
303
307
  },
304
308
  ),
305
309
  onKeyDown: composeEventHandlers(onKeyDown, handleKeyPress),
@@ -307,9 +311,12 @@ export const ComposerPrimitiveInput = forwardRef<
307
311
  onSelect,
308
312
  (e: React.SyntheticEvent<HTMLTextAreaElement>) => {
309
313
  const target = e.target as HTMLTextAreaElement;
310
- mentionInternalContext?.setCursorPosition(
311
- target.selectionStart ?? target.value.length,
312
- );
314
+ const pos = target.selectionStart ?? target.value.length;
315
+ if (pluginRegistry) {
316
+ for (const plugin of pluginRegistry.getPlugins()) {
317
+ plugin.setCursorPosition(pos);
318
+ }
319
+ }
313
320
  },
314
321
  ),
315
322
  onPaste: composeEventHandlers(onPaste, handlePaste),
@@ -0,0 +1,100 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useRef,
7
+ useCallback,
8
+ useMemo,
9
+ type ReactNode,
10
+ type FC,
11
+ } from "react";
12
+
13
+ // =============================================================================
14
+ // Plugin interface — any trigger system (mention, slash command, emoji, etc.)
15
+ // registers one of these with the input.
16
+ // =============================================================================
17
+
18
+ /**
19
+ * A plugin that intercepts keyboard events and cursor changes in the composer
20
+ * input. Used by trigger roots (MentionRoot, SlashCommandRoot, etc.) to handle
21
+ * popover navigation without ComposerInput knowing about specific triggers.
22
+ */
23
+ export type ComposerInputPlugin = {
24
+ /** Handle a key event. Return true if consumed (stops propagation to other plugins and default behavior). */
25
+ handleKeyDown(e: {
26
+ readonly key: string;
27
+ readonly shiftKey: boolean;
28
+ readonly ctrlKey?: boolean;
29
+ readonly metaKey?: boolean;
30
+ readonly nativeEvent?: { isComposing?: boolean };
31
+ preventDefault(): void;
32
+ }): boolean;
33
+
34
+ /** Called on every cursor position change (selection change / text change). */
35
+ setCursorPosition(pos: number): void;
36
+ };
37
+
38
+ // =============================================================================
39
+ // Registry — mutable, ref-based. No re-renders on register/unregister because
40
+ // plugins are read imperatively at event time.
41
+ // =============================================================================
42
+
43
+ export type ComposerInputPluginRegistry = {
44
+ register(plugin: ComposerInputPlugin): () => void;
45
+ getPlugins(): readonly ComposerInputPlugin[];
46
+ };
47
+
48
+ const ComposerInputPluginRegistryContext =
49
+ createContext<ComposerInputPluginRegistry | null>(null);
50
+
51
+ export const useComposerInputPluginRegistry =
52
+ (): ComposerInputPluginRegistry => {
53
+ const ctx = useContext(ComposerInputPluginRegistryContext);
54
+ if (!ctx)
55
+ throw new Error(
56
+ "useComposerInputPluginRegistry must be used within a ComposerInputPluginProvider",
57
+ );
58
+ return ctx;
59
+ };
60
+
61
+ export const useComposerInputPluginRegistryOptional =
62
+ (): ComposerInputPluginRegistry | null => {
63
+ return useContext(ComposerInputPluginRegistryContext);
64
+ };
65
+
66
+ // =============================================================================
67
+ // Provider
68
+ // =============================================================================
69
+
70
+ export const ComposerInputPluginProvider: FC<{ children: ReactNode }> = ({
71
+ children,
72
+ }) => {
73
+ const pluginsRef = useRef<Set<ComposerInputPlugin>>(new Set());
74
+ const snapshotRef = useRef<readonly ComposerInputPlugin[]>([]);
75
+
76
+ const register = useCallback((plugin: ComposerInputPlugin) => {
77
+ pluginsRef.current.add(plugin);
78
+ snapshotRef.current = Array.from(pluginsRef.current);
79
+ return () => {
80
+ pluginsRef.current.delete(plugin);
81
+ snapshotRef.current = Array.from(pluginsRef.current);
82
+ };
83
+ }, []);
84
+
85
+ const getPlugins = useCallback(
86
+ (): readonly ComposerInputPlugin[] => snapshotRef.current,
87
+ [],
88
+ );
89
+
90
+ const registry = useMemo<ComposerInputPluginRegistry>(
91
+ () => ({ register, getPlugins }),
92
+ [register, getPlugins],
93
+ );
94
+
95
+ return (
96
+ <ComposerInputPluginRegistryContext.Provider value={registry}>
97
+ {children}
98
+ </ComposerInputPluginRegistryContext.Provider>
99
+ );
100
+ };
@@ -10,21 +10,25 @@ import {
10
10
  type ReactNode,
11
11
  type FC,
12
12
  } from "react";
13
- import { useResource } from "@assistant-ui/tap/react";
14
- import { useAui, useAuiState } from "@assistant-ui/store";
13
+ import { useAui } from "@assistant-ui/store";
15
14
  import type {
16
15
  Unstable_MentionAdapter,
17
16
  Unstable_DirectiveFormatter,
18
17
  } from "@assistant-ui/core";
19
18
  import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
20
- import {
21
- MentionResource,
22
- type MentionResourceOutput,
23
- type SelectItemOverride,
24
- } from "./MentionResource";
19
+ import { ComposerPrimitiveTriggerPopoverRoot } from "../trigger/TriggerPopoverContext";
20
+ import type {
21
+ TriggerPopoverResourceOutput,
22
+ SelectItemOverride,
23
+ OnSelectBehavior,
24
+ } from "../trigger/TriggerPopoverResource";
25
+
26
+ type MentionResourceOutput = TriggerPopoverResourceOutput & {
27
+ readonly formatter: Unstable_DirectiveFormatter;
28
+ };
25
29
 
26
30
  // =============================================================================
27
- // Context — public (popover components read state + actions from here)
31
+ // Context — public (provides formatter on top of TriggerPopoverContext)
28
32
  // =============================================================================
29
33
 
30
34
  const MentionContext = createContext<MentionResourceOutput | null>(null);
@@ -43,11 +47,10 @@ export const useMentionContextOptional = () => {
43
47
  };
44
48
 
45
49
  // =============================================================================
46
- // Internal context — ComposerInput MentionRoot communication
50
+ // Internal context — only registerSelectItemOverride for Lexical integration
47
51
  // =============================================================================
48
52
 
49
53
  type MentionInternalContextValue = {
50
- setCursorPosition(pos: number): void;
51
54
  registerSelectItemOverride(fn: SelectItemOverride): () => void;
52
55
  };
53
56
 
@@ -59,7 +62,7 @@ export const useMentionInternalContext = () => {
59
62
  };
60
63
 
61
64
  // =============================================================================
62
- // Provider Component
65
+ // Provider Component — delegates to TriggerPopoverRoot internally
63
66
  // =============================================================================
64
67
 
65
68
  export namespace ComposerPrimitiveMentionRoot {
@@ -82,7 +85,6 @@ export const ComposerPrimitiveMentionRoot: FC<
82
85
  formatter: formatterProp,
83
86
  }) => {
84
87
  const aui = useAui();
85
- const text = useAuiState((s) => s.composer.text);
86
88
  const formatter = formatterProp ?? unstable_defaultDirectiveFormatter;
87
89
 
88
90
  // ---------------------------------------------------------------------------
@@ -110,32 +112,64 @@ export const ComposerPrimitiveMentionRoot: FC<
110
112
  const adapter = adapterProp ?? runtimeAdapter;
111
113
 
112
114
  // ---------------------------------------------------------------------------
113
- // Mention resource (all state + logic managed via tap primitives)
115
+ // onSelect behavior for mentions: insert directive text
114
116
  // ---------------------------------------------------------------------------
115
117
 
116
- const mention = useResource(
117
- MentionResource({ adapter, text, triggerChar, formatter, aui }),
118
+ const onSelect = useMemo<OnSelectBehavior>(
119
+ () => ({ type: "insertDirective", formatter }),
120
+ [formatter],
118
121
  );
119
122
 
120
123
  // ---------------------------------------------------------------------------
121
- // Internal context (stable methods come from tapEffectEvent)
124
+ // MentionContext provides formatter + delegates state to TriggerPopoverContext
125
+ // We use useAuiState to read trigger popover state via the inner context.
126
+ // For backward compat, MentionContext wraps TriggerPopoverContext output.
122
127
  // ---------------------------------------------------------------------------
123
128
 
129
+ return (
130
+ <ComposerPrimitiveTriggerPopoverRoot
131
+ adapter={adapter}
132
+ trigger={triggerChar}
133
+ onSelect={onSelect}
134
+ >
135
+ <MentionContextBridge formatter={formatter}>
136
+ {children}
137
+ </MentionContextBridge>
138
+ </ComposerPrimitiveTriggerPopoverRoot>
139
+ );
140
+ };
141
+
142
+ ComposerPrimitiveMentionRoot.displayName = "ComposerPrimitive.MentionRoot";
143
+
144
+ // =============================================================================
145
+ // Bridge — reads TriggerPopoverContext, wraps it as MentionContext
146
+ // =============================================================================
147
+
148
+ import { useTriggerPopoverContext } from "../trigger/TriggerPopoverContext";
149
+
150
+ const MentionContextBridge: FC<{
151
+ formatter: Unstable_DirectiveFormatter;
152
+ children: ReactNode;
153
+ }> = ({ formatter, children }) => {
154
+ const triggerCtx = useTriggerPopoverContext();
155
+
156
+ const mentionValue = useMemo<MentionResourceOutput>(
157
+ () => ({ ...triggerCtx, formatter }),
158
+ [triggerCtx, formatter],
159
+ );
160
+
124
161
  const internalContextValue = useMemo<MentionInternalContextValue>(
125
162
  () => ({
126
- setCursorPosition: mention.setCursorPosition,
127
- registerSelectItemOverride: mention.registerSelectItemOverride,
163
+ registerSelectItemOverride: triggerCtx.registerSelectItemOverride,
128
164
  }),
129
- [mention.setCursorPosition, mention.registerSelectItemOverride],
165
+ [triggerCtx.registerSelectItemOverride],
130
166
  );
131
167
 
132
168
  return (
133
- <MentionContext.Provider value={mention}>
169
+ <MentionContext.Provider value={mentionValue}>
134
170
  <MentionInternalContext.Provider value={internalContextValue}>
135
171
  {children}
136
172
  </MentionInternalContext.Provider>
137
173
  </MentionContext.Provider>
138
174
  );
139
175
  };
140
-
141
- ComposerPrimitiveMentionRoot.displayName = "ComposerPrimitive.MentionRoot";
@@ -4,13 +4,16 @@ export {
4
4
  useMentionContextOptional,
5
5
  useMentionInternalContext,
6
6
  } from "./ComposerMentionContext";
7
- export { ComposerPrimitiveMentionPopover } from "./ComposerMentionPopover";
7
+
8
+ // UI primitives — re-exported from the shared trigger popover implementation.
9
+ // MentionRoot internally renders TriggerPopoverRoot, so these work within it.
10
+ export { ComposerPrimitiveTriggerPopoverPopover as ComposerPrimitiveMentionPopover } from "../trigger/TriggerPopoverPopover";
8
11
  export {
9
- ComposerPrimitiveMentionCategories,
10
- ComposerPrimitiveMentionCategoryItem,
11
- } from "./ComposerMentionCategories";
12
+ ComposerPrimitiveTriggerPopoverCategories as ComposerPrimitiveMentionCategories,
13
+ ComposerPrimitiveTriggerPopoverCategoryItem as ComposerPrimitiveMentionCategoryItem,
14
+ } from "../trigger/TriggerPopoverCategories";
12
15
  export {
13
- ComposerPrimitiveMentionItems,
14
- ComposerPrimitiveMentionItem,
15
- } from "./ComposerMentionItems";
16
- export { ComposerPrimitiveMentionBack } from "./ComposerMentionBack";
16
+ ComposerPrimitiveTriggerPopoverItems as ComposerPrimitiveMentionItems,
17
+ ComposerPrimitiveTriggerPopoverItem as ComposerPrimitiveMentionItem,
18
+ } from "../trigger/TriggerPopoverItems";
19
+ export { ComposerPrimitiveTriggerPopoverBack as ComposerPrimitiveMentionBack } from "../trigger/TriggerPopoverBack";
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { type ReactNode, type FC, useCallback, useMemo } from "react";
4
+ import type {
5
+ Unstable_SlashCommandAdapter,
6
+ Unstable_SlashCommandItem,
7
+ } from "@assistant-ui/core";
8
+ import { ComposerPrimitiveTriggerPopoverRoot } from "../trigger/TriggerPopoverContext";
9
+ import type { OnSelectBehavior } from "../trigger/TriggerPopoverResource";
10
+
11
+ // =============================================================================
12
+ // SlashCommandRoot — convenience wrapper around TriggerPopoverRoot
13
+ // =============================================================================
14
+
15
+ export namespace ComposerPrimitiveSlashCommandRoot {
16
+ export type Props = {
17
+ children: ReactNode;
18
+ /** The adapter providing slash command categories and items. */
19
+ adapter: Unstable_SlashCommandAdapter;
20
+ /** Character(s) that trigger the popover. @default "/" */
21
+ trigger?: string | undefined;
22
+ /** Callback when a slash command is selected. */
23
+ onSelect?: ((item: Unstable_SlashCommandItem) => void) | undefined;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Convenience wrapper around `TriggerPopoverRoot` pre-configured for `/` slash commands.
29
+ * When a user selects a command, the `/command` text is removed from the composer
30
+ * and the item's `execute` callback (if any) and `onSelect` prop are called.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * <ComposerPrimitive.Unstable_SlashCommandRoot adapter={slashAdapter}>
35
+ * <ComposerPrimitive.Input />
36
+ * <ComposerPrimitive.Unstable_TriggerPopoverPopover>
37
+ * <ComposerPrimitive.Unstable_TriggerPopoverItems>
38
+ * {(items) => items.map(item => (
39
+ * <ComposerPrimitive.Unstable_TriggerPopoverItem key={item.id} item={item}>
40
+ * {item.label}
41
+ * </ComposerPrimitive.Unstable_TriggerPopoverItem>
42
+ * ))}
43
+ * </ComposerPrimitive.Unstable_TriggerPopoverItems>
44
+ * </ComposerPrimitive.Unstable_TriggerPopoverPopover>
45
+ * </ComposerPrimitive.Unstable_SlashCommandRoot>
46
+ * ```
47
+ */
48
+ export const ComposerPrimitiveSlashCommandRoot: FC<
49
+ ComposerPrimitiveSlashCommandRoot.Props
50
+ > = ({ children, adapter, trigger = "/", onSelect: onSelectProp }) => {
51
+ const handler = useCallback(
52
+ (item: Unstable_SlashCommandItem) => {
53
+ item.execute?.();
54
+ onSelectProp?.(item);
55
+ },
56
+ [onSelectProp],
57
+ );
58
+
59
+ const onSelect = useMemo<OnSelectBehavior>(
60
+ () => ({ type: "action", handler }),
61
+ [handler],
62
+ );
63
+
64
+ return (
65
+ <ComposerPrimitiveTriggerPopoverRoot
66
+ adapter={adapter}
67
+ trigger={trigger}
68
+ onSelect={onSelect}
69
+ >
70
+ {children}
71
+ </ComposerPrimitiveTriggerPopoverRoot>
72
+ );
73
+ };
74
+
75
+ ComposerPrimitiveSlashCommandRoot.displayName =
76
+ "ComposerPrimitive.SlashCommandRoot";
@@ -0,0 +1 @@
1
+ export { ComposerPrimitiveSlashCommandRoot } from "./ComposerSlashCommandRoot";
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { Primitive } from "../../../utils/Primitive";
4
+ import {
5
+ type ComponentRef,
6
+ type ComponentPropsWithoutRef,
7
+ forwardRef,
8
+ } from "react";
9
+ import { composeEventHandlers } from "@radix-ui/primitive";
10
+ import { useTriggerPopoverContext } from "./TriggerPopoverContext";
11
+
12
+ export namespace ComposerPrimitiveTriggerPopoverBack {
13
+ export type Element = ComponentRef<typeof Primitive.button>;
14
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.button>;
15
+ }
16
+
17
+ /**
18
+ * A button that navigates back from category items to the category list.
19
+ * Only renders when a category is active (drill-down view).
20
+ */
21
+ export const ComposerPrimitiveTriggerPopoverBack = forwardRef<
22
+ ComposerPrimitiveTriggerPopoverBack.Element,
23
+ ComposerPrimitiveTriggerPopoverBack.Props
24
+ >(({ onClick, ...props }, forwardedRef) => {
25
+ const { activeCategoryId, isSearchMode, goBack } = useTriggerPopoverContext();
26
+
27
+ if (!activeCategoryId || isSearchMode) return null;
28
+
29
+ return (
30
+ <Primitive.button
31
+ type="button"
32
+ {...props}
33
+ ref={forwardedRef}
34
+ onClick={composeEventHandlers(onClick, goBack)}
35
+ />
36
+ );
37
+ });
38
+
39
+ ComposerPrimitiveTriggerPopoverBack.displayName =
40
+ "ComposerPrimitive.TriggerPopoverBack";