@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.
- package/README.md +1 -1
- package/dist/client/ExternalThread.d.ts +24 -3
- package/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +106 -27
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/client/InMemoryThreadList.js +23 -30
- package/dist/client/InMemoryThreadList.js.map +1 -1
- package/dist/client/SingleThreadList.d.ts +12 -0
- package/dist/client/SingleThreadList.d.ts.map +1 -0
- package/dist/client/SingleThreadList.js +68 -0
- package/dist/client/SingleThreadList.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +37 -7
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerQueue.d.ts +2 -0
- package/dist/primitives/composer/ComposerQueue.d.ts.map +1 -0
- package/dist/primitives/composer/ComposerQueue.js +3 -0
- package/dist/primitives/composer/ComposerQueue.js.map +1 -0
- package/dist/primitives/composer/ComposerSend.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerSend.js +3 -1
- package/dist/primitives/composer/ComposerSend.js.map +1 -1
- package/dist/primitives/composer/mention/ComposerMentionBack.d.ts +21 -0
- package/dist/primitives/composer/mention/ComposerMentionBack.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionBack.js +28 -0
- package/dist/primitives/composer/mention/ComposerMentionBack.js.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts +42 -0
- package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionCategories.js +32 -0
- package/dist/primitives/composer/mention/ComposerMentionCategories.js.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionContext.d.ts +23 -0
- package/dist/primitives/composer/mention/ComposerMentionContext.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionContext.js +66 -0
- package/dist/primitives/composer/mention/ComposerMentionContext.js.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionItems.d.ts +46 -0
- package/dist/primitives/composer/mention/ComposerMentionItems.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionItems.js +30 -0
- package/dist/primitives/composer/mention/ComposerMentionItems.js.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts +24 -0
- package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionPopover.js +28 -0
- package/dist/primitives/composer/mention/ComposerMentionPopover.js.map +1 -0
- package/dist/primitives/composer/mention/MentionResource.d.ts +39 -0
- package/dist/primitives/composer/mention/MentionResource.d.ts.map +1 -0
- package/dist/primitives/composer/mention/MentionResource.js +230 -0
- package/dist/primitives/composer/mention/MentionResource.js.map +1 -0
- package/dist/primitives/composer/mention/detectMentionTrigger.d.ts +2 -0
- package/dist/primitives/composer/mention/detectMentionTrigger.d.ts.map +1 -0
- package/dist/primitives/composer/mention/detectMentionTrigger.js +26 -0
- package/dist/primitives/composer/mention/detectMentionTrigger.js.map +1 -0
- package/dist/primitives/composer/mention/index.d.ts +6 -0
- package/dist/primitives/composer/mention/index.d.ts.map +1 -0
- package/dist/primitives/composer/mention/index.js +6 -0
- package/dist/primitives/composer/mention/index.js.map +1 -0
- package/dist/primitives/composer.d.ts +10 -0
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/composer.js +10 -0
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemRemove.d.ts +19 -0
- package/dist/primitives/queueItem/QueueItemRemove.d.ts.map +1 -0
- package/dist/primitives/queueItem/QueueItemRemove.js +21 -0
- package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -0
- package/dist/primitives/queueItem/QueueItemSteer.d.ts +19 -0
- package/dist/primitives/queueItem/QueueItemSteer.d.ts.map +1 -0
- package/dist/primitives/queueItem/QueueItemSteer.js +21 -0
- package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -0
- package/dist/primitives/queueItem/QueueItemText.d.ts +18 -0
- package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -0
- package/dist/primitives/queueItem/QueueItemText.js +19 -0
- package/dist/primitives/queueItem/QueueItemText.js.map +1 -0
- package/dist/primitives/queueItem.d.ts +4 -0
- package/dist/primitives/queueItem.d.ts.map +1 -0
- package/dist/primitives/queueItem.js +4 -0
- package/dist/primitives/queueItem.js.map +1 -0
- package/dist/unstable/useToolMentionAdapter.d.ts +42 -0
- package/dist/unstable/useToolMentionAdapter.d.ts.map +1 -0
- package/dist/unstable/useToolMentionAdapter.js +65 -0
- package/dist/unstable/useToolMentionAdapter.js.map +1 -0
- package/package.json +10 -10
- package/src/client/ExternalThread.ts +160 -32
- package/src/client/InMemoryThreadList.ts +24 -35
- package/src/client/SingleThreadList.ts +95 -0
- package/src/index.ts +26 -0
- package/src/primitives/composer/ComposerInput.tsx +49 -5
- package/src/primitives/composer/ComposerQueue.tsx +3 -0
- package/src/primitives/composer/ComposerSend.ts +3 -1
- package/src/primitives/composer/mention/ComposerMentionBack.tsx +55 -0
- package/src/primitives/composer/mention/ComposerMentionCategories.tsx +104 -0
- package/src/primitives/composer/mention/ComposerMentionContext.tsx +141 -0
- package/src/primitives/composer/mention/ComposerMentionItems.tsx +104 -0
- package/src/primitives/composer/mention/ComposerMentionPopover.tsx +52 -0
- package/src/primitives/composer/mention/MentionResource.ts +328 -0
- package/src/primitives/composer/mention/detectMentionTrigger.test.ts +78 -0
- package/src/primitives/composer/mention/detectMentionTrigger.ts +37 -0
- package/src/primitives/composer/mention/index.ts +16 -0
- package/src/primitives/composer.ts +10 -0
- package/src/primitives/queueItem/QueueItemRemove.ts +37 -0
- package/src/primitives/queueItem/QueueItemSteer.ts +37 -0
- package/src/primitives/queueItem/QueueItemText.tsx +37 -0
- package/src/primitives/queueItem.ts +3 -0
- package/src/tests/BaseComposerRuntimeCore.test.ts +3 -1
- 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";
|
|
@@ -204,7 +204,9 @@ describe("BaseComposerRuntimeCore", () => {
|
|
|
204
204
|
};
|
|
205
205
|
composer.setAttachmentAdapter(adapter);
|
|
206
206
|
|
|
207
|
-
await composer.addAttachment(
|
|
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");
|