@assistant-ui/react 0.12.19 → 0.12.21

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 (105) hide show
  1. package/README.md +1 -1
  2. package/dist/client/ExternalThread.d.ts +24 -3
  3. package/dist/client/ExternalThread.d.ts.map +1 -1
  4. package/dist/client/ExternalThread.js +106 -27
  5. package/dist/client/ExternalThread.js.map +1 -1
  6. package/dist/client/InMemoryThreadList.js +23 -30
  7. package/dist/client/InMemoryThreadList.js.map +1 -1
  8. package/dist/client/SingleThreadList.d.ts +12 -0
  9. package/dist/client/SingleThreadList.d.ts.map +1 -0
  10. package/dist/client/SingleThreadList.js +68 -0
  11. package/dist/client/SingleThreadList.js.map +1 -0
  12. package/dist/index.d.ts +8 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +6 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  17. package/dist/primitives/composer/ComposerInput.js +37 -7
  18. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  19. package/dist/primitives/composer/ComposerQueue.d.ts +2 -0
  20. package/dist/primitives/composer/ComposerQueue.d.ts.map +1 -0
  21. package/dist/primitives/composer/ComposerQueue.js +3 -0
  22. package/dist/primitives/composer/ComposerQueue.js.map +1 -0
  23. package/dist/primitives/composer/ComposerSend.d.ts.map +1 -1
  24. package/dist/primitives/composer/ComposerSend.js +3 -1
  25. package/dist/primitives/composer/ComposerSend.js.map +1 -1
  26. package/dist/primitives/composer/mention/ComposerMentionBack.d.ts +21 -0
  27. package/dist/primitives/composer/mention/ComposerMentionBack.d.ts.map +1 -0
  28. package/dist/primitives/composer/mention/ComposerMentionBack.js +28 -0
  29. package/dist/primitives/composer/mention/ComposerMentionBack.js.map +1 -0
  30. package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts +42 -0
  31. package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts.map +1 -0
  32. package/dist/primitives/composer/mention/ComposerMentionCategories.js +32 -0
  33. package/dist/primitives/composer/mention/ComposerMentionCategories.js.map +1 -0
  34. package/dist/primitives/composer/mention/ComposerMentionContext.d.ts +23 -0
  35. package/dist/primitives/composer/mention/ComposerMentionContext.d.ts.map +1 -0
  36. package/dist/primitives/composer/mention/ComposerMentionContext.js +66 -0
  37. package/dist/primitives/composer/mention/ComposerMentionContext.js.map +1 -0
  38. package/dist/primitives/composer/mention/ComposerMentionItems.d.ts +46 -0
  39. package/dist/primitives/composer/mention/ComposerMentionItems.d.ts.map +1 -0
  40. package/dist/primitives/composer/mention/ComposerMentionItems.js +30 -0
  41. package/dist/primitives/composer/mention/ComposerMentionItems.js.map +1 -0
  42. package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts +24 -0
  43. package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts.map +1 -0
  44. package/dist/primitives/composer/mention/ComposerMentionPopover.js +28 -0
  45. package/dist/primitives/composer/mention/ComposerMentionPopover.js.map +1 -0
  46. package/dist/primitives/composer/mention/MentionResource.d.ts +39 -0
  47. package/dist/primitives/composer/mention/MentionResource.d.ts.map +1 -0
  48. package/dist/primitives/composer/mention/MentionResource.js +230 -0
  49. package/dist/primitives/composer/mention/MentionResource.js.map +1 -0
  50. package/dist/primitives/composer/mention/detectMentionTrigger.d.ts +2 -0
  51. package/dist/primitives/composer/mention/detectMentionTrigger.d.ts.map +1 -0
  52. package/dist/primitives/composer/mention/detectMentionTrigger.js +26 -0
  53. package/dist/primitives/composer/mention/detectMentionTrigger.js.map +1 -0
  54. package/dist/primitives/composer/mention/index.d.ts +6 -0
  55. package/dist/primitives/composer/mention/index.d.ts.map +1 -0
  56. package/dist/primitives/composer/mention/index.js +6 -0
  57. package/dist/primitives/composer/mention/index.js.map +1 -0
  58. package/dist/primitives/composer.d.ts +10 -0
  59. package/dist/primitives/composer.d.ts.map +1 -1
  60. package/dist/primitives/composer.js +10 -0
  61. package/dist/primitives/composer.js.map +1 -1
  62. package/dist/primitives/queueItem/QueueItemRemove.d.ts +19 -0
  63. package/dist/primitives/queueItem/QueueItemRemove.d.ts.map +1 -0
  64. package/dist/primitives/queueItem/QueueItemRemove.js +21 -0
  65. package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -0
  66. package/dist/primitives/queueItem/QueueItemSteer.d.ts +19 -0
  67. package/dist/primitives/queueItem/QueueItemSteer.d.ts.map +1 -0
  68. package/dist/primitives/queueItem/QueueItemSteer.js +21 -0
  69. package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -0
  70. package/dist/primitives/queueItem/QueueItemText.d.ts +18 -0
  71. package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -0
  72. package/dist/primitives/queueItem/QueueItemText.js +19 -0
  73. package/dist/primitives/queueItem/QueueItemText.js.map +1 -0
  74. package/dist/primitives/queueItem.d.ts +4 -0
  75. package/dist/primitives/queueItem.d.ts.map +1 -0
  76. package/dist/primitives/queueItem.js +4 -0
  77. package/dist/primitives/queueItem.js.map +1 -0
  78. package/dist/unstable/useToolMentionAdapter.d.ts +42 -0
  79. package/dist/unstable/useToolMentionAdapter.d.ts.map +1 -0
  80. package/dist/unstable/useToolMentionAdapter.js +65 -0
  81. package/dist/unstable/useToolMentionAdapter.js.map +1 -0
  82. package/package.json +10 -10
  83. package/src/client/ExternalThread.ts +160 -32
  84. package/src/client/InMemoryThreadList.ts +24 -35
  85. package/src/client/SingleThreadList.ts +95 -0
  86. package/src/index.ts +26 -0
  87. package/src/primitives/composer/ComposerInput.tsx +49 -5
  88. package/src/primitives/composer/ComposerQueue.tsx +3 -0
  89. package/src/primitives/composer/ComposerSend.ts +3 -1
  90. package/src/primitives/composer/mention/ComposerMentionBack.tsx +55 -0
  91. package/src/primitives/composer/mention/ComposerMentionCategories.tsx +104 -0
  92. package/src/primitives/composer/mention/ComposerMentionContext.tsx +141 -0
  93. package/src/primitives/composer/mention/ComposerMentionItems.tsx +104 -0
  94. package/src/primitives/composer/mention/ComposerMentionPopover.tsx +52 -0
  95. package/src/primitives/composer/mention/MentionResource.ts +328 -0
  96. package/src/primitives/composer/mention/detectMentionTrigger.test.ts +78 -0
  97. package/src/primitives/composer/mention/detectMentionTrigger.ts +37 -0
  98. package/src/primitives/composer/mention/index.ts +16 -0
  99. package/src/primitives/composer.ts +10 -0
  100. package/src/primitives/queueItem/QueueItemRemove.ts +37 -0
  101. package/src/primitives/queueItem/QueueItemSteer.ts +37 -0
  102. package/src/primitives/queueItem/QueueItemText.tsx +37 -0
  103. package/src/primitives/queueItem.ts +3 -0
  104. package/src/tests/BaseComposerRuntimeCore.test.ts +3 -1
  105. package/src/unstable/useToolMentionAdapter.ts +114 -0
@@ -0,0 +1,95 @@
1
+ import { resource, tapMemo } from "@assistant-ui/tap";
2
+ import {
3
+ type ClientElement,
4
+ type ClientOutput,
5
+ tapClientResource,
6
+ } from "@assistant-ui/store";
7
+
8
+ const THREAD_ID = "default";
9
+
10
+ const SingleThreadListItem = resource((): ClientOutput<"threadListItem"> => {
11
+ return {
12
+ getState: () => ({
13
+ id: THREAD_ID,
14
+ remoteId: undefined,
15
+ externalId: undefined,
16
+ title: undefined,
17
+ status: "regular",
18
+ }),
19
+ switchTo: () => {},
20
+ rename: () => {},
21
+ archive: () => {},
22
+ unarchive: () => {},
23
+ delete: () => {},
24
+ generateTitle: () => {},
25
+ initialize: async () => ({ remoteId: THREAD_ID, externalId: undefined }),
26
+ detach: () => {},
27
+ };
28
+ });
29
+
30
+ type SingleThreadListProps = {
31
+ thread: ClientElement<"thread">;
32
+ };
33
+
34
+ /**
35
+ * A minimal threads scope that wraps a single thread.
36
+ * Automatically provided by ExternalThread when no threads scope exists.
37
+ * Mounts the provided thread resource element.
38
+ */
39
+ export const SingleThreadList = resource(
40
+ ({ thread }: SingleThreadListProps): ClientOutput<"threads"> => {
41
+ const itemClient = tapClientResource(SingleThreadListItem());
42
+ const threadClient = tapClientResource(thread);
43
+
44
+ const state = tapMemo(
45
+ () => ({
46
+ mainThreadId: THREAD_ID,
47
+ newThreadId: null,
48
+ isLoading: false,
49
+ threadIds: [THREAD_ID],
50
+ archivedThreadIds: [],
51
+ threadItems: [itemClient.state],
52
+ main: threadClient.state,
53
+ }),
54
+ [itemClient.state, threadClient.state],
55
+ );
56
+
57
+ return {
58
+ getState: () => state,
59
+ switchToThread: () => {
60
+ throw new Error("SingleThreadList does not support switchToThread");
61
+ },
62
+ switchToNewThread: () => {
63
+ throw new Error("SingleThreadList does not support switchToNewThread");
64
+ },
65
+ item: (selector) => {
66
+ if (
67
+ selector !== "main" &&
68
+ !(
69
+ typeof selector === "object" &&
70
+ "id" in selector &&
71
+ selector.id === THREAD_ID
72
+ ) &&
73
+ !(
74
+ typeof selector === "object" &&
75
+ "index" in selector &&
76
+ selector.index === 0
77
+ )
78
+ ) {
79
+ throw new Error(
80
+ `SingleThreadList: unknown item selector ${JSON.stringify(selector)}`,
81
+ );
82
+ }
83
+ return itemClient.methods;
84
+ },
85
+ thread: (selector) => {
86
+ if (selector !== "main" && selector !== THREAD_ID) {
87
+ throw new Error(
88
+ `SingleThreadList: unknown thread selector ${JSON.stringify(selector)}`,
89
+ );
90
+ }
91
+ return threadClient.methods;
92
+ },
93
+ };
94
+ },
95
+ );
package/src/index.ts CHANGED
@@ -199,6 +199,13 @@ export {
199
199
  type ToolDefinition,
200
200
  Tools,
201
201
  DataRenderers,
202
+ Interactables,
203
+ useInteractable,
204
+ type UseInteractableConfig,
205
+ type UseInteractableMetadata,
206
+ makeInteractable,
207
+ type InteractableConfig,
208
+ type AssistantInteractable,
202
209
  } from "@assistant-ui/core/react";
203
210
 
204
211
  // Core pass-through (unchanged)
@@ -216,6 +223,11 @@ export type { Tool } from "assistant-stream";
216
223
  export { tool } from "@assistant-ui/core";
217
224
 
218
225
  export { Suggestions, type SuggestionConfig } from "@assistant-ui/core/store";
226
+ export type {
227
+ QueueItemState,
228
+ QueueItemMethods,
229
+ } from "@assistant-ui/core/store";
230
+ export type { ComposerSendOptions } from "@assistant-ui/core/store";
219
231
 
220
232
  // React-only (stays)
221
233
  export { makeAssistantVisible } from "./model-context/makeAssistantVisible";
@@ -250,6 +262,7 @@ export * as AttachmentPrimitive from "./primitives/attachment";
250
262
  export * as BranchPickerPrimitive from "./primitives/branchPicker";
251
263
  export * as ChainOfThoughtPrimitive from "./primitives/chainOfThought";
252
264
  export * as ComposerPrimitive from "./primitives/composer";
265
+ export * as QueueItemPrimitive from "./primitives/queueItem";
253
266
  export * as MessagePartPrimitive from "./primitives/messagePart";
254
267
  export * as ErrorPrimitive from "./primitives/error";
255
268
  export * as MessagePrimitive from "./primitives/message";
@@ -348,16 +361,29 @@ export {
348
361
  ExternalThread,
349
362
  type ExternalThreadProps,
350
363
  type ExternalThreadMessage,
364
+ type ExternalThreadQueueAdapter,
351
365
  } from "./client/ExternalThread";
352
366
  export {
353
367
  InMemoryThreadList,
354
368
  type InMemoryThreadListProps,
355
369
  } from "./client/InMemoryThreadList";
370
+ export { SingleThreadList } from "./client/SingleThreadList";
356
371
 
357
372
  // ============================================================================
358
373
  // internal & augmentations
359
374
  // ============================================================================
360
375
  export * as INTERNAL from "./internal";
376
+
377
+ // Unstable - mention
378
+ export {
379
+ unstable_useToolMentionAdapter,
380
+ type Unstable_ToolMentionAdapterOptions,
381
+ } from "./unstable/useToolMentionAdapter";
382
+ export {
383
+ useMentionContext as unstable_useMentionContext,
384
+ useMentionContextOptional as unstable_useMentionContextOptional,
385
+ useMentionInternalContext as unstable_useMentionInternalContext,
386
+ } from "./primitives/composer/mention";
361
387
  export type { ToolExecutionStatus } from "./internal";
362
388
 
363
389
  export type { Assistant } from "./augmentations";
@@ -18,6 +18,10 @@ import { useEscapeKeydown } from "@radix-ui/react-use-escape-keydown";
18
18
  import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom";
19
19
  import { useAuiState, useAui } from "@assistant-ui/store";
20
20
  import { flushResourcesSync } from "@assistant-ui/tap";
21
+ import {
22
+ useMentionContextOptional,
23
+ useMentionInternalContext,
24
+ } from "./mention/ComposerMentionContext";
21
25
 
22
26
  export namespace ComposerPrimitiveInput {
23
27
  export type Element = HTMLTextAreaElement;
@@ -118,6 +122,7 @@ export const ComposerPrimitiveInput = forwardRef<
118
122
  onChange,
119
123
  onKeyDown,
120
124
  onPaste,
125
+ onSelect,
121
126
  submitOnEnter,
122
127
  submitMode,
123
128
  cancelOnEscape = true,
@@ -130,6 +135,8 @@ export const ComposerPrimitiveInput = forwardRef<
130
135
  forwardedRef,
131
136
  ) => {
132
137
  const aui = useAui();
138
+ const mentionContext = useMentionContextOptional();
139
+ const mentionInternalContext = useMentionInternalContext();
133
140
 
134
141
  const effectiveSubmitMode =
135
142
  submitMode ?? (submitOnEnter === false ? "none" : "enter");
@@ -149,11 +156,17 @@ export const ComposerPrimitiveInput = forwardRef<
149
156
  const ref = useComposedRefs(forwardedRef, textareaRef);
150
157
 
151
158
  useEscapeKeydown((e) => {
152
- if (!cancelOnEscape) return;
153
-
154
159
  // Only handle ESC if it originated from within this input
155
160
  if (!textareaRef.current?.contains(e.target as Node)) return;
156
161
 
162
+ // Let mention popover handle Escape first
163
+ if (mentionContext?.open) {
164
+ mentionContext.handleKeyDown(e);
165
+ return;
166
+ }
167
+
168
+ if (!cancelOnEscape) return;
169
+
157
170
  const composer = aui.composer();
158
171
  if (composer.getState().canCancel) {
159
172
  composer.cancel();
@@ -167,9 +180,31 @@ export const ComposerPrimitiveInput = forwardRef<
167
180
  // ignore IME composition events
168
181
  if (e.nativeEvent.isComposing) return;
169
182
 
170
- if (e.key === "Enter" && !e.shiftKey) {
171
- const isRunning = aui.thread().getState().isRunning;
172
- if (isRunning) return;
183
+ // Let the mention popover handle keyboard events first
184
+ if (mentionContext?.handleKeyDown(e)) return;
185
+
186
+ if (e.key === "Enter") {
187
+ const threadState = aui.thread().getState();
188
+ const hasQueue = threadState.capabilities.queue;
189
+
190
+ // Steer hotkey: Cmd/Ctrl+Shift+Enter (respects submitMode="none" and isEmpty)
191
+ if (
192
+ e.shiftKey &&
193
+ (e.ctrlKey || e.metaKey) &&
194
+ hasQueue &&
195
+ effectiveSubmitMode !== "none" &&
196
+ !aui.composer().getState().isEmpty
197
+ ) {
198
+ e.preventDefault();
199
+ aui.composer().send({ steer: true });
200
+ return;
201
+ }
202
+
203
+ // Regular newline: Shift+Enter
204
+ if (e.shiftKey) return;
205
+
206
+ // Block submission when running unless queue is supported
207
+ if (threadState.isRunning && !hasQueue) return;
173
208
 
174
209
  let shouldSubmit = false;
175
210
  if (effectiveSubmitMode === "ctrlEnter") {
@@ -254,8 +289,17 @@ export const ComposerPrimitiveInput = forwardRef<
254
289
  flushResourcesSync(() => {
255
290
  aui.composer().setText(e.target.value);
256
291
  });
292
+ mentionInternalContext?.setCursorPosition(
293
+ e.target.selectionStart ?? e.target.value.length,
294
+ );
257
295
  })}
258
296
  onKeyDown={composeEventHandlers(onKeyDown, handleKeyPress)}
297
+ onSelect={composeEventHandlers(onSelect, (e) => {
298
+ const target = e.target as HTMLTextAreaElement;
299
+ mentionInternalContext?.setCursorPosition(
300
+ target.selectionStart ?? target.value.length,
301
+ );
302
+ })}
259
303
  onPaste={composeEventHandlers(onPaste, handlePaste)}
260
304
  />
261
305
  );
@@ -0,0 +1,3 @@
1
+ "use client";
2
+
3
+ export { ComposerPrimitiveQueue } from "@assistant-ui/core/react";
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useCallback } from "react";
3
4
  import {
4
5
  ActionButtonElement,
5
6
  ActionButtonProps,
@@ -9,8 +10,9 @@ import { useComposerSend as useComposerSendBehavior } from "@assistant-ui/core/r
9
10
 
10
11
  export const useComposerSend = () => {
11
12
  const { disabled, send } = useComposerSendBehavior();
13
+ const callback = useCallback(() => send(), [send]);
12
14
  if (disabled) return null;
13
- return send;
15
+ return callback;
14
16
  };
15
17
 
16
18
  export namespace ComposerPrimitiveSend {
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { Primitive } from "@radix-ui/react-primitive";
4
+ import {
5
+ type ComponentRef,
6
+ type ComponentPropsWithoutRef,
7
+ forwardRef,
8
+ useCallback,
9
+ } from "react";
10
+ import { composeEventHandlers } from "@radix-ui/primitive";
11
+ import { useMentionContext } from "./ComposerMentionContext";
12
+
13
+ // =============================================================================
14
+ // MentionBack — Button to navigate back from items to categories
15
+ // =============================================================================
16
+
17
+ export namespace ComposerPrimitiveMentionBack {
18
+ export type Element = ComponentRef<typeof Primitive.button>;
19
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.button>;
20
+ }
21
+
22
+ /**
23
+ * A button that navigates back from category items to the category list.
24
+ * Only renders when a category is active (drill-down view).
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <ComposerPrimitive.MentionBack>
29
+ * &larr; Back
30
+ * </ComposerPrimitive.MentionBack>
31
+ * ```
32
+ */
33
+ export const ComposerPrimitiveMentionBack = forwardRef<
34
+ ComposerPrimitiveMentionBack.Element,
35
+ ComposerPrimitiveMentionBack.Props
36
+ >(({ onClick, ...props }, forwardedRef) => {
37
+ const { activeCategoryId, isSearchMode, goBack } = useMentionContext();
38
+
39
+ const handleClick = useCallback(() => {
40
+ goBack();
41
+ }, [goBack]);
42
+
43
+ if (!activeCategoryId || isSearchMode) return null;
44
+
45
+ return (
46
+ <Primitive.button
47
+ type="button"
48
+ {...props}
49
+ ref={forwardedRef}
50
+ onClick={composeEventHandlers(onClick, handleClick)}
51
+ />
52
+ );
53
+ });
54
+
55
+ ComposerPrimitiveMentionBack.displayName = "ComposerPrimitive.MentionBack";
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { Primitive } from "@radix-ui/react-primitive";
4
+ import {
5
+ type ComponentRef,
6
+ type ComponentPropsWithoutRef,
7
+ type ReactNode,
8
+ forwardRef,
9
+ useCallback,
10
+ } from "react";
11
+ import { composeEventHandlers } from "@radix-ui/primitive";
12
+ import { useMentionContext } from "./ComposerMentionContext";
13
+ import type { Unstable_MentionCategory } from "@assistant-ui/core";
14
+
15
+ // =============================================================================
16
+ // MentionCategories — Renders the list of categories
17
+ // =============================================================================
18
+
19
+ export namespace ComposerPrimitiveMentionCategories {
20
+ export type Element = ComponentRef<typeof Primitive.div>;
21
+ export type Props = Omit<
22
+ ComponentPropsWithoutRef<typeof Primitive.div>,
23
+ "children"
24
+ > & {
25
+ /**
26
+ * Render function that receives the filtered categories and returns
27
+ * the UI. A render-function pattern is used here (instead of a
28
+ * `components` prop) to give consumers full control over list layout,
29
+ * ordering, grouping, and empty states.
30
+ */
31
+ children: (categories: readonly Unstable_MentionCategory[]) => ReactNode;
32
+ };
33
+ }
34
+
35
+ export const ComposerPrimitiveMentionCategories = forwardRef<
36
+ ComposerPrimitiveMentionCategories.Element,
37
+ ComposerPrimitiveMentionCategories.Props
38
+ >(({ children, ...props }, forwardedRef) => {
39
+ const { categories, activeCategoryId, isSearchMode } = useMentionContext();
40
+
41
+ if (activeCategoryId || isSearchMode) return null;
42
+
43
+ return (
44
+ <Primitive.div role="group" {...props} ref={forwardedRef}>
45
+ {children(categories)}
46
+ </Primitive.div>
47
+ );
48
+ });
49
+
50
+ ComposerPrimitiveMentionCategories.displayName =
51
+ "ComposerPrimitive.MentionCategories";
52
+
53
+ // =============================================================================
54
+ // MentionCategoryItem — A single category row (clickable to drill-down)
55
+ // =============================================================================
56
+
57
+ export namespace ComposerPrimitiveMentionCategoryItem {
58
+ export type Element = ComponentRef<typeof Primitive.button>;
59
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.button> & {
60
+ categoryId: string;
61
+ };
62
+ }
63
+
64
+ /**
65
+ * A button that selects a category and triggers drill-down navigation.
66
+ * Automatically receives `data-highlighted` when keyboard-navigated.
67
+ */
68
+ export const ComposerPrimitiveMentionCategoryItem = forwardRef<
69
+ ComposerPrimitiveMentionCategoryItem.Element,
70
+ ComposerPrimitiveMentionCategoryItem.Props
71
+ >(({ categoryId, onClick, ...props }, forwardedRef) => {
72
+ const {
73
+ selectCategory,
74
+ categories,
75
+ highlightedIndex,
76
+ activeCategoryId,
77
+ isSearchMode,
78
+ } = useMentionContext();
79
+
80
+ const handleClick = useCallback(() => {
81
+ selectCategory(categoryId);
82
+ }, [selectCategory, categoryId]);
83
+
84
+ // Derive highlighted state from context — no manual wiring needed
85
+ const isHighlighted =
86
+ !activeCategoryId &&
87
+ !isSearchMode &&
88
+ categories.findIndex((c) => c.id === categoryId) === highlightedIndex;
89
+
90
+ return (
91
+ <Primitive.button
92
+ type="button"
93
+ role="option"
94
+ aria-selected={isHighlighted}
95
+ data-highlighted={isHighlighted ? "" : undefined}
96
+ {...props}
97
+ ref={forwardedRef}
98
+ onClick={composeEventHandlers(onClick, handleClick)}
99
+ />
100
+ );
101
+ });
102
+
103
+ ComposerPrimitiveMentionCategoryItem.displayName =
104
+ "ComposerPrimitive.MentionCategoryItem";
@@ -0,0 +1,141 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ useCallback,
8
+ useMemo,
9
+ useEffect,
10
+ type ReactNode,
11
+ type FC,
12
+ } from "react";
13
+ import { useResource } from "@assistant-ui/tap/react";
14
+ import { useAui, useAuiState } from "@assistant-ui/store";
15
+ import type {
16
+ Unstable_MentionAdapter,
17
+ Unstable_DirectiveFormatter,
18
+ } from "@assistant-ui/core";
19
+ import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
20
+ import {
21
+ MentionResource,
22
+ type MentionResourceOutput,
23
+ type SelectItemOverride,
24
+ } from "./MentionResource";
25
+
26
+ // =============================================================================
27
+ // Context — public (popover components read state + actions from here)
28
+ // =============================================================================
29
+
30
+ const MentionContext = createContext<MentionResourceOutput | null>(null);
31
+
32
+ export const useMentionContext = () => {
33
+ const ctx = useContext(MentionContext);
34
+ if (!ctx)
35
+ throw new Error(
36
+ "useMentionContext must be used within ComposerPrimitive.MentionRoot",
37
+ );
38
+ return ctx;
39
+ };
40
+
41
+ export const useMentionContextOptional = () => {
42
+ return useContext(MentionContext);
43
+ };
44
+
45
+ // =============================================================================
46
+ // Internal context — ComposerInput → MentionRoot communication
47
+ // =============================================================================
48
+
49
+ type MentionInternalContextValue = {
50
+ setCursorPosition(pos: number): void;
51
+ registerSelectItemOverride(fn: SelectItemOverride): () => void;
52
+ };
53
+
54
+ const MentionInternalContext =
55
+ createContext<MentionInternalContextValue | null>(null);
56
+
57
+ export const useMentionInternalContext = () => {
58
+ return useContext(MentionInternalContext);
59
+ };
60
+
61
+ // =============================================================================
62
+ // Provider Component
63
+ // =============================================================================
64
+
65
+ export namespace ComposerPrimitiveMentionRoot {
66
+ export type Props = {
67
+ children: ReactNode;
68
+ adapter?: Unstable_MentionAdapter | undefined;
69
+ /** Character(s) that trigger the mention popover. @default "@" */
70
+ trigger?: string | undefined;
71
+ /** Custom formatter for serializing/parsing mention directives. */
72
+ formatter?: Unstable_DirectiveFormatter | undefined;
73
+ };
74
+ }
75
+
76
+ export const ComposerPrimitiveMentionRoot: FC<
77
+ ComposerPrimitiveMentionRoot.Props
78
+ > = ({
79
+ children,
80
+ adapter: adapterProp,
81
+ trigger: triggerChar = "@",
82
+ formatter: formatterProp,
83
+ }) => {
84
+ const aui = useAui();
85
+ const text = useAuiState((s) => s.composer.text);
86
+ const formatter = formatterProp ?? unstable_defaultDirectiveFormatter;
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Runtime adapter (subscribe to state changes instead of useAuiState to avoid
90
+ // infinite loop — getModelContext() returns a new object on every call)
91
+ // ---------------------------------------------------------------------------
92
+
93
+ const getRuntimeAdapter = useCallback(() => {
94
+ try {
95
+ const runtime = aui.composer().__internal_getRuntime?.();
96
+ return (runtime as any)?._core?.getState()?.getMentionAdapter?.();
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ }, [aui]);
101
+ const [runtimeAdapter, setRuntimeAdapter] = useState(getRuntimeAdapter);
102
+ useEffect(() => {
103
+ return aui.subscribe(() => {
104
+ setRuntimeAdapter((prev: unknown) => {
105
+ const next = getRuntimeAdapter();
106
+ return prev === next ? prev : next;
107
+ });
108
+ });
109
+ }, [aui, getRuntimeAdapter]);
110
+ const adapter = adapterProp ?? runtimeAdapter;
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Mention resource (all state + logic managed via tap primitives)
114
+ // ---------------------------------------------------------------------------
115
+
116
+ const mention = useResource(
117
+ MentionResource({ adapter, text, triggerChar, formatter, aui }),
118
+ );
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Internal context (stable — methods come from tapEffectEvent)
122
+ // ---------------------------------------------------------------------------
123
+
124
+ const internalContextValue = useMemo<MentionInternalContextValue>(
125
+ () => ({
126
+ setCursorPosition: mention.setCursorPosition,
127
+ registerSelectItemOverride: mention.registerSelectItemOverride,
128
+ }),
129
+ [mention.setCursorPosition, mention.registerSelectItemOverride],
130
+ );
131
+
132
+ return (
133
+ <MentionContext.Provider value={mention}>
134
+ <MentionInternalContext.Provider value={internalContextValue}>
135
+ {children}
136
+ </MentionInternalContext.Provider>
137
+ </MentionContext.Provider>
138
+ );
139
+ };
140
+
141
+ ComposerPrimitiveMentionRoot.displayName = "ComposerPrimitive.MentionRoot";
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { Primitive } from "@radix-ui/react-primitive";
4
+ import {
5
+ type ComponentRef,
6
+ type ComponentPropsWithoutRef,
7
+ type ReactNode,
8
+ forwardRef,
9
+ useCallback,
10
+ } from "react";
11
+ import { composeEventHandlers } from "@radix-ui/primitive";
12
+ import { useMentionContext } from "./ComposerMentionContext";
13
+ import type { Unstable_MentionItem } from "@assistant-ui/core";
14
+
15
+ // =============================================================================
16
+ // MentionItems — Renders the list of items within a category
17
+ // =============================================================================
18
+
19
+ export namespace ComposerPrimitiveMentionItems {
20
+ export type Element = ComponentRef<typeof Primitive.div>;
21
+ export type Props = Omit<
22
+ ComponentPropsWithoutRef<typeof Primitive.div>,
23
+ "children"
24
+ > & {
25
+ /**
26
+ * Render function that receives the filtered items and returns
27
+ * the UI. A render-function pattern is used here (instead of a
28
+ * `components` prop) to give consumers full control over list layout,
29
+ * ordering, grouping, and empty states.
30
+ */
31
+ children: (items: readonly Unstable_MentionItem[]) => ReactNode;
32
+ };
33
+ }
34
+
35
+ export const ComposerPrimitiveMentionItems = forwardRef<
36
+ ComposerPrimitiveMentionItems.Element,
37
+ ComposerPrimitiveMentionItems.Props
38
+ >(({ children, ...props }, forwardedRef) => {
39
+ const { items, activeCategoryId, isSearchMode } = useMentionContext();
40
+
41
+ if (!activeCategoryId && !isSearchMode) return null;
42
+
43
+ return (
44
+ <Primitive.div role="group" {...props} ref={forwardedRef}>
45
+ {children(items)}
46
+ </Primitive.div>
47
+ );
48
+ });
49
+
50
+ ComposerPrimitiveMentionItems.displayName = "ComposerPrimitive.MentionItems";
51
+
52
+ // =============================================================================
53
+ // MentionItem — A single selectable mention item
54
+ // =============================================================================
55
+
56
+ export namespace ComposerPrimitiveMentionItem {
57
+ export type Element = ComponentRef<typeof Primitive.button>;
58
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.button> & {
59
+ item: Unstable_MentionItem;
60
+ /** Position in the items list. Used for keyboard highlight matching. Falls back to findIndex by id. */
61
+ index?: number | undefined;
62
+ };
63
+ }
64
+
65
+ /**
66
+ * A button that inserts the mention item into the composer.
67
+ * Automatically receives `data-highlighted` when keyboard-navigated.
68
+ */
69
+ export const ComposerPrimitiveMentionItem = forwardRef<
70
+ ComposerPrimitiveMentionItem.Element,
71
+ ComposerPrimitiveMentionItem.Props
72
+ >(({ item, index: indexProp, onClick, ...props }, forwardedRef) => {
73
+ const {
74
+ selectItem,
75
+ items,
76
+ highlightedIndex,
77
+ activeCategoryId,
78
+ isSearchMode,
79
+ } = useMentionContext();
80
+
81
+ const handleClick = useCallback(() => {
82
+ selectItem(item);
83
+ }, [selectItem, item]);
84
+
85
+ // Use explicit index prop if provided, fall back to findIndex
86
+ const itemIndex = indexProp ?? items.findIndex((i) => i.id === item.id);
87
+ const isHighlighted =
88
+ (isSearchMode || activeCategoryId !== null) &&
89
+ itemIndex === highlightedIndex;
90
+
91
+ return (
92
+ <Primitive.button
93
+ type="button"
94
+ role="option"
95
+ aria-selected={isHighlighted}
96
+ data-highlighted={isHighlighted ? "" : undefined}
97
+ {...props}
98
+ ref={forwardedRef}
99
+ onClick={composeEventHandlers(onClick, handleClick)}
100
+ />
101
+ );
102
+ });
103
+
104
+ ComposerPrimitiveMentionItem.displayName = "ComposerPrimitive.MentionItem";