@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
@@ -7,43 +7,70 @@ import {
7
7
  tapRef,
8
8
  } from "@assistant-ui/tap";
9
9
  import type {
10
- Unstable_MentionAdapter,
11
- Unstable_MentionCategory,
12
- Unstable_MentionItem,
10
+ Unstable_TriggerAdapter,
11
+ Unstable_TriggerCategory,
12
+ Unstable_TriggerItem,
13
13
  Unstable_DirectiveFormatter,
14
14
  } from "@assistant-ui/core";
15
15
  import type { AssistantClient } from "@assistant-ui/store";
16
- import { detectMentionTrigger } from "./detectMentionTrigger";
16
+ import { detectTrigger } from "./detectTrigger";
17
+
18
+ function isTriggerItem(
19
+ x: Unstable_TriggerItem | Unstable_TriggerCategory,
20
+ ): x is Unstable_TriggerItem {
21
+ return "type" in x;
22
+ }
23
+
24
+ function matchesQuery(item: Unstable_TriggerItem, lower: string): boolean {
25
+ return (
26
+ item.id.toLowerCase().includes(lower) ||
27
+ item.label.toLowerCase().includes(lower) ||
28
+ (item.description?.toLowerCase().includes(lower) ?? false)
29
+ );
30
+ }
17
31
 
18
32
  // =============================================================================
19
33
  // Types
20
34
  // =============================================================================
21
35
 
22
- export type MentionKeyEvent = {
36
+ export type TriggerPopoverKeyEvent = {
23
37
  readonly key: string;
24
38
  readonly shiftKey: boolean;
25
39
  preventDefault(): void;
26
40
  };
27
41
 
28
- export type SelectItemOverride = (item: Unstable_MentionItem) => boolean;
42
+ export type SelectItemOverride = (item: Unstable_TriggerItem) => boolean;
29
43
 
30
- export type MentionResourceOutput = {
44
+ export type OnSelectBehavior =
45
+ | {
46
+ type: "insertDirective";
47
+ formatter: Unstable_DirectiveFormatter;
48
+ }
49
+ | {
50
+ type: "action";
51
+ handler: (item: Unstable_TriggerItem) => void;
52
+ };
53
+
54
+ export type TriggerPopoverResourceOutput = {
31
55
  // State
32
56
  readonly open: boolean;
33
57
  readonly query: string;
34
58
  readonly activeCategoryId: string | null;
35
- readonly categories: readonly Unstable_MentionCategory[];
36
- readonly items: readonly Unstable_MentionItem[];
59
+ readonly categories: readonly Unstable_TriggerCategory[];
60
+ readonly items: readonly Unstable_TriggerItem[];
37
61
  readonly highlightedIndex: number;
38
62
  readonly isSearchMode: boolean;
39
- readonly formatter: Unstable_DirectiveFormatter;
63
+ /** Stable ID prefix for generating accessible element IDs. */
64
+ readonly popoverId: string;
65
+ /** ID of the currently highlighted item (for aria-activedescendant). */
66
+ readonly highlightedItemId: string | undefined;
40
67
 
41
68
  // Actions
42
69
  selectCategory(categoryId: string): void;
43
70
  goBack(): void;
44
- selectItem(item: Unstable_MentionItem): void;
71
+ selectItem(item: Unstable_TriggerItem): void;
45
72
  close(): void;
46
- handleKeyDown(e: MentionKeyEvent): boolean;
73
+ handleKeyDown(e: TriggerPopoverKeyEvent): boolean;
47
74
 
48
75
  // Internal (for ComposerInput integration)
49
76
  setCursorPosition(pos: number): void;
@@ -54,20 +81,23 @@ export type MentionResourceOutput = {
54
81
  // Resource
55
82
  // =============================================================================
56
83
 
57
- export const MentionResource = resource(
84
+ export const TriggerPopoverResource = resource(
58
85
  ({
59
86
  adapter,
60
87
  text,
61
88
  triggerChar,
62
- formatter,
89
+ onSelect,
63
90
  aui,
91
+ popoverId,
64
92
  }: {
65
- adapter: Unstable_MentionAdapter | undefined;
93
+ adapter: Unstable_TriggerAdapter | undefined;
66
94
  text: string;
67
95
  triggerChar: string;
68
- formatter: Unstable_DirectiveFormatter;
96
+ onSelect: OnSelectBehavior;
69
97
  aui: AssistantClient;
70
- }): MentionResourceOutput => {
98
+ /** Stable ID for accessible element IDs (pass React's useId() from component layer). */
99
+ popoverId: string;
100
+ }): TriggerPopoverResourceOutput => {
71
101
  // -------------------------------------------------------------------------
72
102
  // Cursor tracking + trigger detection
73
103
  // -------------------------------------------------------------------------
@@ -76,7 +106,7 @@ export const MentionResource = resource(
76
106
 
77
107
  const trigger = tapMemo(() => {
78
108
  const pos = Math.min(cursorPosition, text.length);
79
- return detectMentionTrigger(text, triggerChar, pos);
109
+ return detectTrigger(text, triggerChar, pos);
80
110
  }, [cursorPosition, text, triggerChar]);
81
111
 
82
112
  const open = trigger !== null && adapter !== undefined;
@@ -95,7 +125,7 @@ export const MentionResource = resource(
95
125
  if (!open) setActiveCategoryId(null);
96
126
  }, [open]);
97
127
 
98
- const categories = tapMemo<readonly Unstable_MentionCategory[]>(() => {
128
+ const categories = tapMemo<readonly Unstable_TriggerCategory[]>(() => {
99
129
  if (!open || !adapter) return [];
100
130
  return adapter.categories();
101
131
  }, [open, adapter]);
@@ -106,33 +136,31 @@ export const MentionResource = resource(
106
136
  // Items + search
107
137
  // -------------------------------------------------------------------------
108
138
 
109
- const allItems = tapMemo<readonly Unstable_MentionItem[]>(() => {
139
+ const allItems = tapMemo<readonly Unstable_TriggerItem[]>(() => {
110
140
  if (!effectiveActiveCategoryId || !adapter) return [];
111
141
  return adapter.categoryItems(effectiveActiveCategoryId);
112
142
  }, [effectiveActiveCategoryId, adapter]);
113
143
 
114
144
  const searchResults = tapMemo<
115
- readonly Unstable_MentionItem[] | null
145
+ readonly Unstable_TriggerItem[] | null
116
146
  >(() => {
117
- if (!open || !adapter || !query || effectiveActiveCategoryId) return null;
147
+ if (!open || !adapter || effectiveActiveCategoryId) return null;
148
+ // If categories exist and query is empty, show categories first (not search)
149
+ if (!query && categories.length > 0) return null;
118
150
  if (adapter.search) return adapter.search(query);
119
151
 
120
- const cats = adapter.categories();
121
- const all: Unstable_MentionItem[] = [];
152
+ // Fallback: search all categories manually (reuse already-computed list)
153
+ const all: Unstable_TriggerItem[] = [];
122
154
  const lower = query.toLowerCase();
123
- for (const cat of cats) {
155
+ for (const cat of categories) {
124
156
  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
- ) {
157
+ if (matchesQuery(item, lower)) {
130
158
  all.push(item);
131
159
  }
132
160
  }
133
161
  }
134
162
  return all;
135
- }, [open, adapter, query, effectiveActiveCategoryId]);
163
+ }, [open, adapter, query, effectiveActiveCategoryId, categories]);
136
164
 
137
165
  const isSearchMode = searchResults !== null;
138
166
 
@@ -153,12 +181,7 @@ export const MentionResource = resource(
153
181
  if (isSearchMode) return searchResults ?? [];
154
182
  if (!query) return allItems;
155
183
  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
- );
184
+ return allItems.filter((item) => matchesQuery(item, lower));
162
185
  }, [allItems, query, isSearchMode, searchResults]);
163
186
 
164
187
  // -------------------------------------------------------------------------
@@ -186,7 +209,7 @@ export const MentionResource = resource(
186
209
  }, [navigableList]);
187
210
 
188
211
  // -------------------------------------------------------------------------
189
- // Lexical select-item override
212
+ // Select-item override (for Lexical integration)
190
213
  // -------------------------------------------------------------------------
191
214
 
192
215
  const selectItemOverrideRef = tapRef<SelectItemOverride | null>(null);
@@ -216,27 +239,42 @@ export const MentionResource = resource(
216
239
  setHighlightedIndex(0);
217
240
  });
218
241
 
219
- const selectItem = tapEffectEvent((item: Unstable_MentionItem) => {
242
+ const selectItem = tapEffectEvent((item: Unstable_TriggerItem) => {
220
243
  if (!trigger) return;
221
244
 
222
- // Try the Lexical override first
245
+ // Try the override first (e.g. Lexical MentionPlugin)
223
246
  if (selectItemOverrideRef.current?.(item)) {
224
247
  setActiveCategoryId(null);
225
248
  setHighlightedIndex(0);
226
249
  return;
227
250
  }
228
251
 
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}`);
252
+ // Behavior depends on the onSelect configuration
253
+ if (onSelect.type === "insertDirective") {
254
+ // Insert directive text (mention path)
255
+ const currentText = aui.composer().getState().text;
256
+ const before = currentText.slice(0, trigger.offset);
257
+ const after = currentText.slice(
258
+ trigger.offset + triggerChar.length + trigger.query.length,
259
+ );
260
+ const directive = onSelect.formatter.serialize(item);
261
+ const newText =
262
+ before + directive + (after.startsWith(" ") ? after : ` ${after}`);
263
+
264
+ aui.composer().setText(newText);
265
+ } else if (onSelect.type === "action") {
266
+ // Execute action + clear trigger text (slash command path)
267
+ const currentText = aui.composer().getState().text;
268
+ const before = currentText.slice(0, trigger.offset);
269
+ const after = currentText.slice(
270
+ trigger.offset + triggerChar.length + trigger.query.length,
271
+ );
272
+ const newText = before + after.trimStart();
273
+ aui.composer().setText(newText);
274
+
275
+ onSelect.handler(item);
276
+ }
238
277
 
239
- aui.composer().setText(newText);
240
278
  setActiveCategoryId(null);
241
279
  setHighlightedIndex(0);
242
280
  });
@@ -250,63 +288,72 @@ export const MentionResource = resource(
250
288
  }
251
289
  });
252
290
 
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);
291
+ const handleKeyDown = tapEffectEvent(
292
+ (e: TriggerPopoverKeyEvent): boolean => {
293
+ if (!open) return false;
294
+
295
+ switch (e.key) {
296
+ case "ArrowDown": {
297
+ e.preventDefault();
298
+ setHighlightedIndex((prev) => {
299
+ const len = navigableList.length;
300
+ if (len === 0) return 0;
301
+ return prev < len - 1 ? prev + 1 : 0;
302
+ });
303
+ return true;
285
304
  }
286
- return true;
287
- }
288
- case "Escape": {
289
- e.preventDefault();
290
- close();
291
- return true;
292
- }
293
- case "Backspace": {
294
- if (effectiveActiveCategoryId && query === "") {
305
+ case "ArrowUp": {
295
306
  e.preventDefault();
296
- goBack();
307
+ setHighlightedIndex((prev) => {
308
+ const len = navigableList.length;
309
+ if (len === 0) return 0;
310
+ return prev > 0 ? prev - 1 : len - 1;
311
+ });
297
312
  return true;
298
313
  }
299
- return false;
314
+ case "Enter": {
315
+ if (e.shiftKey) return false;
316
+ e.preventDefault();
317
+ const item = navigableList[highlightedIndex];
318
+ if (!item) return true;
319
+
320
+ if (isTriggerItem(item)) {
321
+ selectItem(item);
322
+ } else {
323
+ selectCategory(item.id);
324
+ }
325
+ return true;
326
+ }
327
+ case "Escape": {
328
+ e.preventDefault();
329
+ close();
330
+ return true;
331
+ }
332
+ case "Backspace": {
333
+ if (effectiveActiveCategoryId && query === "") {
334
+ e.preventDefault();
335
+ goBack();
336
+ return true;
337
+ }
338
+ return false;
339
+ }
340
+ default:
341
+ return false;
300
342
  }
301
- default:
302
- return false;
303
- }
304
- });
343
+ },
344
+ );
305
345
 
306
346
  // -------------------------------------------------------------------------
307
347
  // Output
308
348
  // -------------------------------------------------------------------------
309
349
 
350
+ // Compute highlighted item ID for aria-activedescendant
351
+ const highlightedEntry = navigableList[highlightedIndex];
352
+ const highlightedItemId =
353
+ open && highlightedEntry
354
+ ? `${popoverId}-option-${highlightedEntry.id}`
355
+ : undefined;
356
+
310
357
  return {
311
358
  open,
312
359
  query,
@@ -315,7 +362,8 @@ export const MentionResource = resource(
315
362
  items: filteredItems,
316
363
  highlightedIndex,
317
364
  isSearchMode,
318
- formatter,
365
+ popoverId,
366
+ highlightedItemId,
319
367
  selectCategory,
320
368
  goBack,
321
369
  selectItem,
@@ -1,28 +1,28 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { detectMentionTrigger } from "./detectMentionTrigger";
2
+ import { detectTrigger } from "./detectTrigger";
3
3
 
4
- describe("detectMentionTrigger", () => {
4
+ describe("detectTrigger", () => {
5
5
  it("detects @query at cursor position", () => {
6
- expect(detectMentionTrigger("hello @wea", "@", 10)).toEqual({
6
+ expect(detectTrigger("hello @wea", "@", 10)).toEqual({
7
7
  query: "wea",
8
8
  offset: 6,
9
9
  });
10
10
  });
11
11
 
12
12
  it("returns null when cursor is before the trigger", () => {
13
- expect(detectMentionTrigger("hello @weather", "@", 5)).toBeNull();
13
+ expect(detectTrigger("hello @weather", "@", 5)).toBeNull();
14
14
  });
15
15
 
16
16
  it("returns null when no trigger character", () => {
17
- expect(detectMentionTrigger("hello world", "@", 11)).toBeNull();
17
+ expect(detectTrigger("hello world", "@", 11)).toBeNull();
18
18
  });
19
19
 
20
20
  it("requires whitespace or start before trigger", () => {
21
- expect(detectMentionTrigger("email@test", "@", 10)).toBeNull();
21
+ expect(detectTrigger("email@test", "@", 10)).toBeNull();
22
22
  });
23
23
 
24
24
  it("trigger at start of text", () => {
25
- expect(detectMentionTrigger("@foo", "@", 4)).toEqual({
25
+ expect(detectTrigger("@foo", "@", 4)).toEqual({
26
26
  query: "foo",
27
27
  offset: 0,
28
28
  });
@@ -30,19 +30,19 @@ describe("detectMentionTrigger", () => {
30
30
 
31
31
  it("stops at whitespace in query", () => {
32
32
  // "@foo bar" — space terminates the mention
33
- expect(detectMentionTrigger("@foo bar", "@", 8)).toBeNull();
33
+ expect(detectTrigger("@foo bar", "@", 8)).toBeNull();
34
34
  });
35
35
 
36
36
  it("stops at newline", () => {
37
- expect(detectMentionTrigger("@foo\nbar", "@", 8)).toBeNull();
37
+ expect(detectTrigger("@foo\nbar", "@", 8)).toBeNull();
38
38
  });
39
39
 
40
40
  it("stops at tab", () => {
41
- expect(detectMentionTrigger("@foo\tbar", "@", 8)).toBeNull();
41
+ expect(detectTrigger("@foo\tbar", "@", 8)).toBeNull();
42
42
  });
43
43
 
44
44
  it("treats tab before trigger as valid boundary", () => {
45
- expect(detectMentionTrigger("hello\t@foo", "@", 10)).toEqual({
45
+ expect(detectTrigger("hello\t@foo", "@", 10)).toEqual({
46
46
  query: "foo",
47
47
  offset: 6,
48
48
  });
@@ -51,7 +51,7 @@ describe("detectMentionTrigger", () => {
51
51
  it("finds trigger closest to cursor, not earlier ones", () => {
52
52
  // Text has two @: "hello @old text @new"
53
53
  // Cursor at end → should find @new
54
- expect(detectMentionTrigger("hello @old text @new", "@", 20)).toEqual({
54
+ expect(detectTrigger("hello @old text @new", "@", 20)).toEqual({
55
55
  query: "new",
56
56
  offset: 16,
57
57
  });
@@ -59,18 +59,18 @@ describe("detectMentionTrigger", () => {
59
59
 
60
60
  it("ignores trigger after cursor", () => {
61
61
  // Cursor at position 5, trigger at position 10
62
- expect(detectMentionTrigger("hello text @foo", "@", 5)).toBeNull();
62
+ expect(detectTrigger("hello text @foo", "@", 5)).toBeNull();
63
63
  });
64
64
 
65
65
  it("works with multi-char trigger", () => {
66
- expect(detectMentionTrigger("hello @@foo", "@@", 11)).toEqual({
66
+ expect(detectTrigger("hello @@foo", "@@", 11)).toEqual({
67
67
  query: "foo",
68
68
  offset: 6,
69
69
  });
70
70
  });
71
71
 
72
72
  it("empty query when cursor is right after trigger", () => {
73
- expect(detectMentionTrigger("hello @", "@", 7)).toEqual({
73
+ expect(detectTrigger("hello @", "@", 7)).toEqual({
74
74
  query: "",
75
75
  offset: 6,
76
76
  });
@@ -1,11 +1,11 @@
1
1
  const WHITESPACE_RE = /\s/;
2
2
 
3
3
  /**
4
- * Detect a mention trigger in text relative to the cursor position.
4
+ * Detect a trigger character in text relative to the cursor position.
5
5
  *
6
- * @internal Exported for testing and for the MentionResource.
6
+ * @internal Exported for testing and for trigger resources.
7
7
  */
8
- export function detectMentionTrigger(
8
+ export function detectTrigger(
9
9
  text: string,
10
10
  triggerChar: string,
11
11
  cursorPosition: number,
@@ -0,0 +1,16 @@
1
+ export {
2
+ ComposerPrimitiveTriggerPopoverRoot,
3
+ useTriggerPopoverContext,
4
+ useTriggerPopoverContextOptional,
5
+ } from "./TriggerPopoverContext";
6
+ export { ComposerPrimitiveTriggerPopoverPopover } from "./TriggerPopoverPopover";
7
+ export {
8
+ ComposerPrimitiveTriggerPopoverCategories,
9
+ ComposerPrimitiveTriggerPopoverCategoryItem,
10
+ } from "./TriggerPopoverCategories";
11
+ export {
12
+ ComposerPrimitiveTriggerPopoverItems,
13
+ ComposerPrimitiveTriggerPopoverItem,
14
+ } from "./TriggerPopoverItems";
15
+ export { ComposerPrimitiveTriggerPopoverBack } from "./TriggerPopoverBack";
16
+ export type { OnSelectBehavior } from "./TriggerPopoverResource";
@@ -23,3 +23,19 @@ export { ComposerPrimitiveMentionItem as Unstable_MentionItem } from "./composer
23
23
  export { ComposerPrimitiveMentionBack as Unstable_MentionBack } from "./composer/mention";
24
24
  export { useMentionContext as unstable_useMentionContext } from "./composer/mention";
25
25
  export { useMentionContextOptional as unstable_useMentionContextOptional } from "./composer/mention";
26
+
27
+ // --- Generic Trigger Popover primitives (unstable) ---
28
+ export { ComposerPrimitiveTriggerPopoverRoot as Unstable_TriggerPopoverRoot } from "./composer/trigger";
29
+ export { ComposerPrimitiveTriggerPopoverPopover as Unstable_TriggerPopoverPopover } from "./composer/trigger";
30
+ export { ComposerPrimitiveTriggerPopoverCategories as Unstable_TriggerPopoverCategories } from "./composer/trigger";
31
+ export { ComposerPrimitiveTriggerPopoverCategoryItem as Unstable_TriggerPopoverCategoryItem } from "./composer/trigger";
32
+ export { ComposerPrimitiveTriggerPopoverItems as Unstable_TriggerPopoverItems } from "./composer/trigger";
33
+ export { ComposerPrimitiveTriggerPopoverItem as Unstable_TriggerPopoverItem } from "./composer/trigger";
34
+ export { ComposerPrimitiveTriggerPopoverBack as Unstable_TriggerPopoverBack } from "./composer/trigger";
35
+ export { useTriggerPopoverContext as unstable_useTriggerPopoverContext } from "./composer/trigger";
36
+ export { useTriggerPopoverContextOptional as unstable_useTriggerPopoverContextOptional } from "./composer/trigger";
37
+
38
+ // --- Slash Command primitives (unstable) ---
39
+ // SlashCommandRoot is the only slash-specific primitive; UI primitives
40
+ // (Popover, Items, Categories, Back) are the shared TriggerPopover* set above.
41
+ export { ComposerPrimitiveSlashCommandRoot as Unstable_SlashCommandRoot } from "./composer/slash-command";
@@ -79,9 +79,20 @@ export namespace MessagePrimitiveRoot {
79
79
  export type Element = ComponentRef<typeof Primitive.div>;
80
80
  /**
81
81
  * Props for the MessagePrimitive.Root component.
82
- * Accepts all standard div element props.
82
+ * Accepts all standard div element props plus optional viewport slack tuning.
83
83
  */
84
- export type Props = ComponentPropsWithoutRef<typeof Primitive.div>;
84
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.div> & {
85
+ /**
86
+ * Threshold at which the user message height clamps to the offset.
87
+ * @default "10em"
88
+ */
89
+ fillClampThreshold?: string | undefined;
90
+ /**
91
+ * Offset used when clamping large user messages.
92
+ * @default "6em"
93
+ */
94
+ fillClampOffset?: string | undefined;
95
+ };
85
96
  }
86
97
 
87
98
  /**
@@ -108,7 +119,7 @@ export namespace MessagePrimitiveRoot {
108
119
  export const MessagePrimitiveRoot = forwardRef<
109
120
  MessagePrimitiveRoot.Element,
110
121
  MessagePrimitiveRoot.Props
111
- >((props, forwardRef) => {
122
+ >(({ fillClampThreshold, fillClampOffset, ...props }, forwardRef) => {
112
123
  const isHoveringRef = useIsHoveringRef();
113
124
  const anchorUserMessageRef = useMessageViewportRef();
114
125
  const ref = useComposedRefs<HTMLDivElement>(
@@ -119,7 +130,10 @@ export const MessagePrimitiveRoot = forwardRef<
119
130
  const messageId = useAuiState((s) => s.message.id);
120
131
 
121
132
  return (
122
- <ThreadPrimitiveViewportSlack>
133
+ <ThreadPrimitiveViewportSlack
134
+ fillClampThreshold={fillClampThreshold}
135
+ fillClampOffset={fillClampOffset}
136
+ >
123
137
  <Primitive.div {...props} ref={ref} data-message-id={messageId} />
124
138
  </ThreadPrimitiveViewportSlack>
125
139
  );
@@ -36,9 +36,9 @@ const parseCssLength = (value: string, element: HTMLElement): number => {
36
36
 
37
37
  export type ThreadViewportSlackProps = {
38
38
  /** Threshold at which the user message height clamps to the offset */
39
- fillClampThreshold?: string;
39
+ fillClampThreshold?: string | undefined;
40
40
  /** Offset used when clamping large user messages */
41
- fillClampOffset?: string;
41
+ fillClampOffset?: string | undefined;
42
42
  children: ReactNode;
43
43
  };
44
44
 
@@ -6,12 +6,14 @@ import type {
6
6
  AppendMessage,
7
7
  CreateAttachment,
8
8
  PendingAttachment,
9
+ SendOptions,
9
10
  } from "@assistant-ui/core";
10
11
 
11
12
  class TestComposerCore extends BaseComposerRuntimeCore {
12
13
  private _attachmentAdapter: AttachmentAdapter | undefined;
13
14
  private _dictationAdapter: DictationAdapter | undefined;
14
15
  public sentMessages: Array<Omit<AppendMessage, "parentId" | "sourceId">> = [];
16
+ public sentOptions: Array<SendOptions | undefined> = [];
15
17
  public cancelCalled = false;
16
18
 
17
19
  protected getAttachmentAdapter() {
@@ -33,8 +35,12 @@ class TestComposerCore extends BaseComposerRuntimeCore {
33
35
  return false;
34
36
  }
35
37
 
36
- protected handleSend(message: Omit<AppendMessage, "parentId" | "sourceId">) {
38
+ protected handleSend(
39
+ message: Omit<AppendMessage, "parentId" | "sourceId">,
40
+ options?: SendOptions,
41
+ ) {
37
42
  this.sentMessages.push(message);
43
+ this.sentOptions.push(options);
38
44
  }
39
45
 
40
46
  protected handleCancel() {
@@ -421,4 +427,30 @@ describe("BaseComposerRuntimeCore", () => {
421
427
  expect(adapter.send).toHaveBeenCalledTimes(1);
422
428
  });
423
429
  });
430
+
431
+ describe("send options", () => {
432
+ it("send() passes undefined options by default", async () => {
433
+ composer.setText("hello");
434
+ await composer.send();
435
+
436
+ expect(composer.sentOptions).toHaveLength(1);
437
+ expect(composer.sentOptions[0]).toBeUndefined();
438
+ });
439
+
440
+ it("send({ startRun: true }) forwards options to handleSend", async () => {
441
+ composer.setText("hello");
442
+ await composer.send({ startRun: true });
443
+
444
+ expect(composer.sentOptions).toHaveLength(1);
445
+ expect(composer.sentOptions[0]).toEqual({ startRun: true });
446
+ });
447
+
448
+ it("send({ startRun: false }) forwards options to handleSend", async () => {
449
+ composer.setText("hello");
450
+ await composer.send({ startRun: false });
451
+
452
+ expect(composer.sentOptions).toHaveLength(1);
453
+ expect(composer.sentOptions[0]).toEqual({ startRun: false });
454
+ });
455
+ });
424
456
  });