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