@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,52 @@
1
+ "use client";
2
+
3
+ import { Primitive } from "@radix-ui/react-primitive";
4
+ import {
5
+ type ComponentRef,
6
+ type ComponentPropsWithoutRef,
7
+ forwardRef,
8
+ } from "react";
9
+ import { useMentionContext } from "./ComposerMentionContext";
10
+
11
+ // =============================================================================
12
+ // MentionPopover — Container that only renders when mention is active
13
+ // =============================================================================
14
+
15
+ export namespace ComposerPrimitiveMentionPopover {
16
+ export type Element = ComponentRef<typeof Primitive.div>;
17
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.div>;
18
+ }
19
+
20
+ /**
21
+ * Renders a container for the mention picker popover.
22
+ * Only renders when a `@` trigger is detected in the composer text.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <ComposerPrimitive.MentionRoot adapter={mentionAdapter}>
27
+ * <ComposerPrimitive.Input />
28
+ * <ComposerPrimitive.MentionPopover>
29
+ * <ComposerPrimitive.MentionCategories />
30
+ * </ComposerPrimitive.MentionPopover>
31
+ * </ComposerPrimitive.MentionRoot>
32
+ * ```
33
+ */
34
+ export const ComposerPrimitiveMentionPopover = forwardRef<
35
+ ComposerPrimitiveMentionPopover.Element,
36
+ ComposerPrimitiveMentionPopover.Props
37
+ >((props, forwardedRef) => {
38
+ const { open } = useMentionContext();
39
+ if (!open) return null;
40
+
41
+ return (
42
+ <Primitive.div
43
+ role="listbox"
44
+ data-state="open"
45
+ {...props}
46
+ ref={forwardedRef}
47
+ />
48
+ );
49
+ });
50
+
51
+ ComposerPrimitiveMentionPopover.displayName =
52
+ "ComposerPrimitive.MentionPopover";
@@ -0,0 +1,328 @@
1
+ import {
2
+ resource,
3
+ tapState,
4
+ tapMemo,
5
+ tapEffectEvent,
6
+ tapEffect,
7
+ tapRef,
8
+ } from "@assistant-ui/tap";
9
+ import type {
10
+ Unstable_MentionAdapter,
11
+ Unstable_MentionCategory,
12
+ Unstable_MentionItem,
13
+ Unstable_DirectiveFormatter,
14
+ } from "@assistant-ui/core";
15
+ import type { AssistantClient } from "@assistant-ui/store";
16
+ import { detectMentionTrigger } from "./detectMentionTrigger";
17
+
18
+ // =============================================================================
19
+ // Types
20
+ // =============================================================================
21
+
22
+ export type MentionKeyEvent = {
23
+ readonly key: string;
24
+ readonly shiftKey: boolean;
25
+ preventDefault(): void;
26
+ };
27
+
28
+ export type SelectItemOverride = (item: Unstable_MentionItem) => boolean;
29
+
30
+ export type MentionResourceOutput = {
31
+ // State
32
+ readonly open: boolean;
33
+ readonly query: string;
34
+ readonly activeCategoryId: string | null;
35
+ readonly categories: readonly Unstable_MentionCategory[];
36
+ readonly items: readonly Unstable_MentionItem[];
37
+ readonly highlightedIndex: number;
38
+ readonly isSearchMode: boolean;
39
+ readonly formatter: Unstable_DirectiveFormatter;
40
+
41
+ // Actions
42
+ selectCategory(categoryId: string): void;
43
+ goBack(): void;
44
+ selectItem(item: Unstable_MentionItem): void;
45
+ close(): void;
46
+ handleKeyDown(e: MentionKeyEvent): boolean;
47
+
48
+ // Internal (for ComposerInput integration)
49
+ setCursorPosition(pos: number): void;
50
+ registerSelectItemOverride(fn: SelectItemOverride): () => void;
51
+ };
52
+
53
+ // =============================================================================
54
+ // Resource
55
+ // =============================================================================
56
+
57
+ export const MentionResource = resource(
58
+ ({
59
+ adapter,
60
+ text,
61
+ triggerChar,
62
+ formatter,
63
+ aui,
64
+ }: {
65
+ adapter: Unstable_MentionAdapter | undefined;
66
+ text: string;
67
+ triggerChar: string;
68
+ formatter: Unstable_DirectiveFormatter;
69
+ aui: AssistantClient;
70
+ }): MentionResourceOutput => {
71
+ // -------------------------------------------------------------------------
72
+ // Cursor tracking + trigger detection
73
+ // -------------------------------------------------------------------------
74
+
75
+ const [cursorPosition, setCursorPosition] = tapState(text.length);
76
+
77
+ const trigger = tapMemo(() => {
78
+ const pos = Math.min(cursorPosition, text.length);
79
+ return detectMentionTrigger(text, triggerChar, pos);
80
+ }, [cursorPosition, text, triggerChar]);
81
+
82
+ const open = trigger !== null && adapter !== undefined;
83
+ const query = trigger?.query ?? "";
84
+
85
+ // -------------------------------------------------------------------------
86
+ // Category navigation
87
+ // -------------------------------------------------------------------------
88
+
89
+ const [activeCategoryId, setActiveCategoryId] = tapState<string | null>(
90
+ null,
91
+ );
92
+
93
+ // Reset when popover closes
94
+ tapEffect(() => {
95
+ if (!open) setActiveCategoryId(null);
96
+ }, [open]);
97
+
98
+ const categories = tapMemo<readonly Unstable_MentionCategory[]>(() => {
99
+ if (!open || !adapter) return [];
100
+ return adapter.categories();
101
+ }, [open, adapter]);
102
+
103
+ const effectiveActiveCategoryId = open ? activeCategoryId : null;
104
+
105
+ // -------------------------------------------------------------------------
106
+ // Items + search
107
+ // -------------------------------------------------------------------------
108
+
109
+ const allItems = tapMemo<readonly Unstable_MentionItem[]>(() => {
110
+ if (!effectiveActiveCategoryId || !adapter) return [];
111
+ return adapter.categoryItems(effectiveActiveCategoryId);
112
+ }, [effectiveActiveCategoryId, adapter]);
113
+
114
+ const searchResults = tapMemo<
115
+ readonly Unstable_MentionItem[] | null
116
+ >(() => {
117
+ if (!open || !adapter || !query || effectiveActiveCategoryId) return null;
118
+ if (adapter.search) return adapter.search(query);
119
+
120
+ const cats = adapter.categories();
121
+ const all: Unstable_MentionItem[] = [];
122
+ const lower = query.toLowerCase();
123
+ for (const cat of cats) {
124
+ for (const item of adapter.categoryItems(cat.id)) {
125
+ if (
126
+ item.id.toLowerCase().includes(lower) ||
127
+ item.label.toLowerCase().includes(lower) ||
128
+ item.description?.toLowerCase().includes(lower)
129
+ ) {
130
+ all.push(item);
131
+ }
132
+ }
133
+ }
134
+ return all;
135
+ }, [open, adapter, query, effectiveActiveCategoryId]);
136
+
137
+ const isSearchMode = searchResults !== null;
138
+
139
+ // -------------------------------------------------------------------------
140
+ // Filtering
141
+ // -------------------------------------------------------------------------
142
+
143
+ const filteredCategories = tapMemo(() => {
144
+ if (isSearchMode) return [];
145
+ if (!query) return categories;
146
+ const lower = query.toLowerCase();
147
+ return categories.filter((cat) =>
148
+ cat.label.toLowerCase().includes(lower),
149
+ );
150
+ }, [categories, query, isSearchMode]);
151
+
152
+ const filteredItems = tapMemo(() => {
153
+ if (isSearchMode) return searchResults ?? [];
154
+ if (!query) return allItems;
155
+ const lower = query.toLowerCase();
156
+ return allItems.filter(
157
+ (item) =>
158
+ item.id.toLowerCase().includes(lower) ||
159
+ item.label.toLowerCase().includes(lower) ||
160
+ item.description?.toLowerCase().includes(lower),
161
+ );
162
+ }, [allItems, query, isSearchMode, searchResults]);
163
+
164
+ // -------------------------------------------------------------------------
165
+ // Keyboard navigation
166
+ // -------------------------------------------------------------------------
167
+
168
+ const [highlightedIndex, setHighlightedIndex] = tapState(0);
169
+
170
+ const navigableList = tapMemo(() => {
171
+ if (isSearchMode) return searchResults ?? [];
172
+ if (effectiveActiveCategoryId) return filteredItems;
173
+ return filteredCategories;
174
+ }, [
175
+ isSearchMode,
176
+ searchResults,
177
+ effectiveActiveCategoryId,
178
+ filteredItems,
179
+ filteredCategories,
180
+ ]);
181
+
182
+ // Reset highlight when list changes
183
+ // biome-ignore lint/correctness/useExhaustiveDependencies: intentional reset on list change
184
+ tapEffect(() => {
185
+ setHighlightedIndex(0);
186
+ }, [navigableList]);
187
+
188
+ // -------------------------------------------------------------------------
189
+ // Lexical select-item override
190
+ // -------------------------------------------------------------------------
191
+
192
+ const selectItemOverrideRef = tapRef<SelectItemOverride | null>(null);
193
+
194
+ const registerSelectItemOverride = tapEffectEvent(
195
+ (fn: SelectItemOverride) => {
196
+ selectItemOverrideRef.current = fn;
197
+ return () => {
198
+ if (selectItemOverrideRef.current === fn) {
199
+ selectItemOverrideRef.current = null;
200
+ }
201
+ };
202
+ },
203
+ );
204
+
205
+ // -------------------------------------------------------------------------
206
+ // Actions (stable via tapEffectEvent)
207
+ // -------------------------------------------------------------------------
208
+
209
+ const selectCategory = tapEffectEvent((categoryId: string) => {
210
+ setActiveCategoryId(categoryId);
211
+ setHighlightedIndex(0);
212
+ });
213
+
214
+ const goBack = tapEffectEvent(() => {
215
+ setActiveCategoryId(null);
216
+ setHighlightedIndex(0);
217
+ });
218
+
219
+ const selectItem = tapEffectEvent((item: Unstable_MentionItem) => {
220
+ if (!trigger) return;
221
+
222
+ // Try the Lexical override first
223
+ if (selectItemOverrideRef.current?.(item)) {
224
+ setActiveCategoryId(null);
225
+ setHighlightedIndex(0);
226
+ return;
227
+ }
228
+
229
+ // Default: text-based replacement (textarea path)
230
+ const currentText = aui.composer().getState().text;
231
+ const before = currentText.slice(0, trigger.offset);
232
+ const after = currentText.slice(
233
+ trigger.offset + triggerChar.length + trigger.query.length,
234
+ );
235
+ const directive = formatter.serialize(item);
236
+ const newText =
237
+ before + directive + (after.startsWith(" ") ? after : ` ${after}`);
238
+
239
+ aui.composer().setText(newText);
240
+ setActiveCategoryId(null);
241
+ setHighlightedIndex(0);
242
+ });
243
+
244
+ const close = tapEffectEvent(() => {
245
+ setActiveCategoryId(null);
246
+ setHighlightedIndex(0);
247
+ // Move cursor before the trigger so trigger detection deactivates
248
+ if (trigger) {
249
+ setCursorPosition(trigger.offset);
250
+ }
251
+ });
252
+
253
+ const handleKeyDown = tapEffectEvent((e: MentionKeyEvent): boolean => {
254
+ if (!open) return false;
255
+
256
+ switch (e.key) {
257
+ case "ArrowDown": {
258
+ e.preventDefault();
259
+ setHighlightedIndex((prev) => {
260
+ const len = navigableList.length;
261
+ if (len === 0) return 0;
262
+ return prev < len - 1 ? prev + 1 : 0;
263
+ });
264
+ return true;
265
+ }
266
+ case "ArrowUp": {
267
+ e.preventDefault();
268
+ setHighlightedIndex((prev) => {
269
+ const len = navigableList.length;
270
+ if (len === 0) return 0;
271
+ return prev > 0 ? prev - 1 : len - 1;
272
+ });
273
+ return true;
274
+ }
275
+ case "Enter": {
276
+ if (e.shiftKey) return false;
277
+ e.preventDefault();
278
+ const item = navigableList[highlightedIndex];
279
+ if (!item) return true;
280
+
281
+ if (isSearchMode || effectiveActiveCategoryId) {
282
+ selectItem(item as Unstable_MentionItem);
283
+ } else {
284
+ selectCategory((item as Unstable_MentionCategory).id);
285
+ }
286
+ return true;
287
+ }
288
+ case "Escape": {
289
+ e.preventDefault();
290
+ close();
291
+ return true;
292
+ }
293
+ case "Backspace": {
294
+ if (effectiveActiveCategoryId && query === "") {
295
+ e.preventDefault();
296
+ goBack();
297
+ return true;
298
+ }
299
+ return false;
300
+ }
301
+ default:
302
+ return false;
303
+ }
304
+ });
305
+
306
+ // -------------------------------------------------------------------------
307
+ // Output
308
+ // -------------------------------------------------------------------------
309
+
310
+ return {
311
+ open,
312
+ query,
313
+ activeCategoryId: effectiveActiveCategoryId,
314
+ categories: filteredCategories,
315
+ items: filteredItems,
316
+ highlightedIndex,
317
+ isSearchMode,
318
+ formatter,
319
+ selectCategory,
320
+ goBack,
321
+ selectItem,
322
+ close,
323
+ handleKeyDown,
324
+ setCursorPosition,
325
+ registerSelectItemOverride,
326
+ };
327
+ },
328
+ );
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { detectMentionTrigger } from "./detectMentionTrigger";
3
+
4
+ describe("detectMentionTrigger", () => {
5
+ it("detects @query at cursor position", () => {
6
+ expect(detectMentionTrigger("hello @wea", "@", 10)).toEqual({
7
+ query: "wea",
8
+ offset: 6,
9
+ });
10
+ });
11
+
12
+ it("returns null when cursor is before the trigger", () => {
13
+ expect(detectMentionTrigger("hello @weather", "@", 5)).toBeNull();
14
+ });
15
+
16
+ it("returns null when no trigger character", () => {
17
+ expect(detectMentionTrigger("hello world", "@", 11)).toBeNull();
18
+ });
19
+
20
+ it("requires whitespace or start before trigger", () => {
21
+ expect(detectMentionTrigger("email@test", "@", 10)).toBeNull();
22
+ });
23
+
24
+ it("trigger at start of text", () => {
25
+ expect(detectMentionTrigger("@foo", "@", 4)).toEqual({
26
+ query: "foo",
27
+ offset: 0,
28
+ });
29
+ });
30
+
31
+ it("stops at whitespace in query", () => {
32
+ // "@foo bar" — space terminates the mention
33
+ expect(detectMentionTrigger("@foo bar", "@", 8)).toBeNull();
34
+ });
35
+
36
+ it("stops at newline", () => {
37
+ expect(detectMentionTrigger("@foo\nbar", "@", 8)).toBeNull();
38
+ });
39
+
40
+ it("stops at tab", () => {
41
+ expect(detectMentionTrigger("@foo\tbar", "@", 8)).toBeNull();
42
+ });
43
+
44
+ it("treats tab before trigger as valid boundary", () => {
45
+ expect(detectMentionTrigger("hello\t@foo", "@", 10)).toEqual({
46
+ query: "foo",
47
+ offset: 6,
48
+ });
49
+ });
50
+
51
+ 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
+ expect(detectMentionTrigger("hello @old text @new", "@", 20)).toEqual({
55
+ query: "new",
56
+ offset: 16,
57
+ });
58
+ });
59
+
60
+ it("ignores trigger after cursor", () => {
61
+ // Cursor at position 5, trigger at position 10
62
+ expect(detectMentionTrigger("hello text @foo", "@", 5)).toBeNull();
63
+ });
64
+
65
+ it("works with multi-char trigger", () => {
66
+ expect(detectMentionTrigger("hello @@foo", "@@", 11)).toEqual({
67
+ query: "foo",
68
+ offset: 6,
69
+ });
70
+ });
71
+
72
+ it("empty query when cursor is right after trigger", () => {
73
+ expect(detectMentionTrigger("hello @", "@", 7)).toEqual({
74
+ query: "",
75
+ offset: 6,
76
+ });
77
+ });
78
+ });
@@ -0,0 +1,37 @@
1
+ const WHITESPACE_RE = /\s/;
2
+
3
+ /**
4
+ * Detect a mention trigger in text relative to the cursor position.
5
+ *
6
+ * @internal Exported for testing and for the MentionResource.
7
+ */
8
+ export function detectMentionTrigger(
9
+ text: string,
10
+ triggerChar: string,
11
+ cursorPosition: number,
12
+ ): {
13
+ query: string;
14
+ offset: number;
15
+ } | null {
16
+ // Only consider text up to the cursor
17
+ const textUpToCursor = text.slice(0, cursorPosition);
18
+
19
+ // Search backwards from cursor for the trigger character.
20
+ // Stop at any whitespace during scan — trigger must be contiguous with cursor.
21
+ for (let i = textUpToCursor.length - 1; i >= 0; i--) {
22
+ const char = textUpToCursor[i]!;
23
+
24
+ if (WHITESPACE_RE.test(char)) return null;
25
+
26
+ if (textUpToCursor.startsWith(triggerChar, i)) {
27
+ // Trigger must be preceded by whitespace or be at start of text
28
+ if (i > 0 && !WHITESPACE_RE.test(textUpToCursor[i - 1]!)) continue;
29
+
30
+ const query = textUpToCursor.slice(i + triggerChar.length);
31
+
32
+ return { query, offset: i };
33
+ }
34
+ }
35
+
36
+ return null;
37
+ }
@@ -0,0 +1,16 @@
1
+ export {
2
+ ComposerPrimitiveMentionRoot,
3
+ useMentionContext,
4
+ useMentionContextOptional,
5
+ useMentionInternalContext,
6
+ } from "./ComposerMentionContext";
7
+ export { ComposerPrimitiveMentionPopover } from "./ComposerMentionPopover";
8
+ export {
9
+ ComposerPrimitiveMentionCategories,
10
+ ComposerPrimitiveMentionCategoryItem,
11
+ } from "./ComposerMentionCategories";
12
+ export {
13
+ ComposerPrimitiveMentionItems,
14
+ ComposerPrimitiveMentionItem,
15
+ } from "./ComposerMentionItems";
16
+ export { ComposerPrimitiveMentionBack } from "./ComposerMentionBack";
@@ -13,3 +13,13 @@ export { ComposerPrimitiveIf as If } from "./composer/ComposerIf";
13
13
  export { ComposerPrimitiveQuote as Quote } from "./composer/ComposerQuote";
14
14
  export { ComposerPrimitiveQuoteText as QuoteText } from "./composer/ComposerQuote";
15
15
  export { ComposerPrimitiveQuoteDismiss as QuoteDismiss } from "./composer/ComposerQuote";
16
+ export { ComposerPrimitiveQueue as Queue } from "./composer/ComposerQueue";
17
+ export { ComposerPrimitiveMentionRoot as Unstable_MentionRoot } from "./composer/mention";
18
+ export { ComposerPrimitiveMentionPopover as Unstable_MentionPopover } from "./composer/mention";
19
+ export { ComposerPrimitiveMentionCategories as Unstable_MentionCategories } from "./composer/mention";
20
+ export { ComposerPrimitiveMentionCategoryItem as Unstable_MentionCategoryItem } from "./composer/mention";
21
+ export { ComposerPrimitiveMentionItems as Unstable_MentionItems } from "./composer/mention";
22
+ export { ComposerPrimitiveMentionItem as Unstable_MentionItem } from "./composer/mention";
23
+ export { ComposerPrimitiveMentionBack as Unstable_MentionBack } from "./composer/mention";
24
+ export { useMentionContext as unstable_useMentionContext } from "./composer/mention";
25
+ export { useMentionContextOptional as unstable_useMentionContextOptional } from "./composer/mention";
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import {
4
+ ActionButtonElement,
5
+ ActionButtonProps,
6
+ createActionButton,
7
+ } from "../../utils/createActionButton";
8
+ import { useAui } from "@assistant-ui/store";
9
+ import { useCallback } from "react";
10
+
11
+ const useQueueItemRemove = () => {
12
+ const aui = useAui();
13
+
14
+ const callback = useCallback(() => {
15
+ aui.queueItem().remove();
16
+ }, [aui]);
17
+
18
+ return callback;
19
+ };
20
+
21
+ export namespace QueueItemPrimitiveRemove {
22
+ export type Element = ActionButtonElement;
23
+ export type Props = ActionButtonProps<typeof useQueueItemRemove>;
24
+ }
25
+
26
+ /**
27
+ * A button that removes this item from the queue.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <QueueItemPrimitive.Remove>×</QueueItemPrimitive.Remove>
32
+ * ```
33
+ */
34
+ export const QueueItemPrimitiveRemove = createActionButton(
35
+ "QueueItemPrimitive.Remove",
36
+ useQueueItemRemove,
37
+ );
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import {
4
+ ActionButtonElement,
5
+ ActionButtonProps,
6
+ createActionButton,
7
+ } from "../../utils/createActionButton";
8
+ import { useAui } from "@assistant-ui/store";
9
+ import { useCallback } from "react";
10
+
11
+ const useQueueItemSteer = () => {
12
+ const aui = useAui();
13
+
14
+ const callback = useCallback(() => {
15
+ aui.queueItem().steer();
16
+ }, [aui]);
17
+
18
+ return callback;
19
+ };
20
+
21
+ export namespace QueueItemPrimitiveSteer {
22
+ export type Element = ActionButtonElement;
23
+ export type Props = ActionButtonProps<typeof useQueueItemSteer>;
24
+ }
25
+
26
+ /**
27
+ * A button that steers the current run to process this queue item immediately.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <QueueItemPrimitive.Steer>Run Now</QueueItemPrimitive.Steer>
32
+ * ```
33
+ */
34
+ export const QueueItemPrimitiveSteer = createActionButton(
35
+ "QueueItemPrimitive.Steer",
36
+ useQueueItemSteer,
37
+ );
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { Primitive } from "@radix-ui/react-primitive";
4
+ import {
5
+ type ComponentRef,
6
+ type ComponentPropsWithoutRef,
7
+ forwardRef,
8
+ } from "react";
9
+ import { useAuiState } from "@assistant-ui/store";
10
+
11
+ export namespace QueueItemPrimitiveText {
12
+ export type Element = ComponentRef<typeof Primitive.span>;
13
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.span>;
14
+ }
15
+
16
+ /**
17
+ * Renders the prompt text of a queue item.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <QueueItemPrimitive.Text />
22
+ * ```
23
+ */
24
+ export const QueueItemPrimitiveText = forwardRef<
25
+ QueueItemPrimitiveText.Element,
26
+ QueueItemPrimitiveText.Props
27
+ >((props, ref) => {
28
+ const prompt = useAuiState((s) => s.queueItem.prompt);
29
+
30
+ return (
31
+ <Primitive.span {...props} ref={ref}>
32
+ {props.children ?? prompt}
33
+ </Primitive.span>
34
+ );
35
+ });
36
+
37
+ QueueItemPrimitiveText.displayName = "QueueItemPrimitive.Text";
@@ -0,0 +1,3 @@
1
+ export { QueueItemPrimitiveText as Text } from "./queueItem/QueueItemText";
2
+ export { QueueItemPrimitiveSteer as Steer } from "./queueItem/QueueItemSteer";
3
+ export { QueueItemPrimitiveRemove as Remove } from "./queueItem/QueueItemRemove";
@@ -204,7 +204,9 @@ describe("BaseComposerRuntimeCore", () => {
204
204
  };
205
205
  composer.setAttachmentAdapter(adapter);
206
206
 
207
- await composer.addAttachment(new File(["data"], "test.txt"));
207
+ await composer.addAttachment(
208
+ new File(["data"], "test.txt", { type: "text/plain" }),
209
+ );
208
210
 
209
211
  expect(composer.attachments).toHaveLength(1);
210
212
  expect(composer.attachments[0]!.id).toBe("att-1");