@agent-native/toolkit 0.209.0 → 0.209.1-nightly-20261015005848
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/app/chat/AgentSidebar.d.ts.map +1 -1
- package/dist/app/chat/AgentSidebar.js +22 -11
- package/dist/app/chat/AgentSidebar.js.map +1 -1
- package/dist/app/review/InlineMarkdown.d.ts +2 -0
- package/dist/app/review/InlineMarkdown.d.ts.map +1 -1
- package/dist/app/review/InlineMarkdown.js +25 -7
- package/dist/app/review/InlineMarkdown.js.map +1 -1
- package/dist/composer/MentionChip.d.ts +14 -0
- package/dist/composer/MentionChip.d.ts.map +1 -0
- package/dist/composer/MentionChip.js +11 -0
- package/dist/composer/MentionChip.js.map +1 -0
- package/dist/composer/MentionPopover.d.ts +5 -0
- package/dist/composer/MentionPopover.d.ts.map +1 -1
- package/dist/composer/MentionPopover.js +55 -44
- package/dist/composer/MentionPopover.js.map +1 -1
- package/dist/composer/PromptComposer.d.ts +6 -1
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +3 -3
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts +18 -2
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +60 -12
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/extensions/MentionReference.d.ts.map +1 -1
- package/dist/composer/extensions/MentionReference.js +16 -5
- package/dist/composer/extensions/MentionReference.js.map +1 -1
- package/dist/composer/index.d.ts +2 -1
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +1 -0
- package/dist/composer/index.js.map +1 -1
- package/package.json +4 -4
- package/src/app/chat/AgentSidebar.spec.tsx +339 -2
- package/src/app/chat/AgentSidebar.tsx +25 -10
- package/src/app/review/InlineMarkdown.spec.tsx +25 -0
- package/src/app/review/InlineMarkdown.tsx +29 -13
- package/src/composer/MentionChip.tsx +46 -0
- package/src/composer/MentionPopover.tsx +49 -7
- package/src/composer/PromptComposer.text-references.spec.tsx +198 -0
- package/src/composer/PromptComposer.tsx +12 -0
- package/src/composer/TiptapComposer.tsx +100 -10
- package/src/composer/extensions/MentionReference.tsx +22 -14
- package/src/composer/index.ts +2 -0
|
@@ -44,6 +44,8 @@ export interface InlineMarkdownProtectedSpan {
|
|
|
44
44
|
label: string;
|
|
45
45
|
className?: string;
|
|
46
46
|
title?: string;
|
|
47
|
+
/** Protect only the occurrence of `source` starting at this offset. */
|
|
48
|
+
at?: number;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export function InlineMarkdown({
|
|
@@ -137,23 +139,37 @@ export function InlineMarkdown({
|
|
|
137
139
|
|
|
138
140
|
const PROTECTED_SPAN_PREFIX = "https://inline-markdown-protected.invalid/span/";
|
|
139
141
|
|
|
142
|
+
// One pass over the original text: replacing span by span would match a
|
|
143
|
+
// shorter source again inside the link label a longer one just produced.
|
|
140
144
|
function protectInlineMarkdownSpans(
|
|
141
145
|
content: string,
|
|
142
146
|
spans: readonly InlineMarkdownProtectedSpan[],
|
|
143
147
|
): string {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
content,
|
|
156
|
-
)
|
|
148
|
+
const matches: { start: number; end: number; index: number }[] = [];
|
|
149
|
+
spans.forEach(({ source, at }, index) => {
|
|
150
|
+
if (!source) return;
|
|
151
|
+
if (at !== undefined) {
|
|
152
|
+
if (content.startsWith(source, at))
|
|
153
|
+
matches.push({ start: at, end: at + source.length, index });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
for (
|
|
157
|
+
let start = content.indexOf(source);
|
|
158
|
+
start !== -1;
|
|
159
|
+
start = content.indexOf(source, start + source.length)
|
|
160
|
+
) {
|
|
161
|
+
matches.push({ start, end: start + source.length, index });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
matches.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
165
|
+
let markdown = "";
|
|
166
|
+
let cursor = 0;
|
|
167
|
+
for (const { start, end, index } of matches) {
|
|
168
|
+
if (start < cursor) continue;
|
|
169
|
+
markdown += `${content.slice(cursor, start)}[${escapeProtectedSpanLabel(spans[index]!.label)}](${PROTECTED_SPAN_PREFIX}${index})`;
|
|
170
|
+
cursor = end;
|
|
171
|
+
}
|
|
172
|
+
return markdown + content.slice(cursor);
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
function escapeProtectedSpanLabel(label: string): string {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../utils.js";
|
|
4
|
+
import { MentionItemMedia } from "./MentionItemMedia.js";
|
|
5
|
+
import type { MentionItemMedia as MentionItemMediaValue } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface MentionChipProps extends Omit<
|
|
8
|
+
ComponentPropsWithRef<"span">,
|
|
9
|
+
"children"
|
|
10
|
+
> {
|
|
11
|
+
label: ReactNode;
|
|
12
|
+
icon?: string;
|
|
13
|
+
media?: MentionItemMediaValue | null;
|
|
14
|
+
fallbackIcon?: "clipboard" | "file" | "stack";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The inline chip a mention is drawn as, in the composer and wherever a sent
|
|
19
|
+
* message shows its mentions, so the two always look the same.
|
|
20
|
+
*/
|
|
21
|
+
export function MentionChip({
|
|
22
|
+
label,
|
|
23
|
+
icon,
|
|
24
|
+
media,
|
|
25
|
+
fallbackIcon = "stack",
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
28
|
+
}: MentionChipProps) {
|
|
29
|
+
return (
|
|
30
|
+
<span
|
|
31
|
+
{...props}
|
|
32
|
+
className={cn(
|
|
33
|
+
"mx-0.5 inline-flex max-w-[200px] items-center gap-1 rounded-md border border-input bg-muted/50 px-1.5 py-0.5 align-middle text-xs font-medium text-foreground",
|
|
34
|
+
className,
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
<MentionItemMedia
|
|
38
|
+
icon={icon}
|
|
39
|
+
media={media}
|
|
40
|
+
size="sm"
|
|
41
|
+
fallbackIcon={fallbackIcon}
|
|
42
|
+
/>
|
|
43
|
+
<span className="truncate">{label}</span>
|
|
44
|
+
</span>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -33,6 +33,8 @@ export interface MentionPopoverRef {
|
|
|
33
33
|
getSelectedIndex: () => number;
|
|
34
34
|
getSelectedMention: () => MentionItem | null;
|
|
35
35
|
getSelectedCommand: () => SlashCommand | null;
|
|
36
|
+
/** Runs Retry if it is the highlighted row; false when it is not. */
|
|
37
|
+
activateRetry: () => boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface MentionPopoverProps {
|
|
@@ -43,6 +45,9 @@ interface MentionPopoverProps {
|
|
|
43
45
|
commands?: SlashCommand[];
|
|
44
46
|
hint?: string;
|
|
45
47
|
isLoading: boolean;
|
|
48
|
+
/** Why some @ items could not load; shown above the items that did. */
|
|
49
|
+
error?: string;
|
|
50
|
+
onRetry?: () => void;
|
|
46
51
|
query: string;
|
|
47
52
|
onSelectMention: (item: MentionItem) => void;
|
|
48
53
|
onSelectSkill: (skill: SkillResult) => void;
|
|
@@ -158,6 +163,8 @@ export const MentionPopover = forwardRef<
|
|
|
158
163
|
commands = [],
|
|
159
164
|
hint,
|
|
160
165
|
isLoading,
|
|
166
|
+
error,
|
|
167
|
+
onRetry,
|
|
161
168
|
query,
|
|
162
169
|
onSelectMention,
|
|
163
170
|
onSelectSkill,
|
|
@@ -168,7 +175,7 @@ export const MentionPopover = forwardRef<
|
|
|
168
175
|
const stacked = density === "stacked";
|
|
169
176
|
|
|
170
177
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
171
|
-
const
|
|
178
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
172
179
|
const t = useComposerRuntimeAdapters().translate!;
|
|
173
180
|
|
|
174
181
|
const sectionLabel = (section: string) => {
|
|
@@ -196,6 +203,10 @@ export const MentionPopover = forwardRef<
|
|
|
196
203
|
|
|
197
204
|
const itemCount =
|
|
198
205
|
type === "@" ? mentionItems.length : commands.length + skills.length;
|
|
206
|
+
// Focus stays in the editor, so Retry is a row above the items, at -1,
|
|
207
|
+
// for the arrow keys to reach.
|
|
208
|
+
const retryable = type === "@" && !!error && !!onRetry;
|
|
209
|
+
const firstIndex = retryable ? -1 : 0;
|
|
199
210
|
const itemIdentitySignature =
|
|
200
211
|
type === "@"
|
|
201
212
|
? mentionItems.map((item) => item.id).join("\0")
|
|
@@ -250,10 +261,10 @@ export const MentionPopover = forwardRef<
|
|
|
250
261
|
|
|
251
262
|
useEffect(() => {
|
|
252
263
|
setSelectedIndex((current) => (current === 0 ? current : 0));
|
|
253
|
-
}, [itemIdentitySignature, query]);
|
|
264
|
+
}, [itemIdentitySignature, query, retryable]);
|
|
254
265
|
|
|
255
266
|
useEffect(() => {
|
|
256
|
-
const container =
|
|
267
|
+
const container = containerRef.current;
|
|
257
268
|
if (!container) return;
|
|
258
269
|
const selected = container.querySelector(
|
|
259
270
|
`[data-mention-index="${selectedIndex}"]`,
|
|
@@ -266,11 +277,13 @@ export const MentionPopover = forwardRef<
|
|
|
266
277
|
useImperativeHandle(ref, () => ({
|
|
267
278
|
moveUp: () => {
|
|
268
279
|
setSelectedIndex((prev) =>
|
|
269
|
-
prev <=
|
|
280
|
+
prev <= firstIndex ? Math.max(firstIndex, itemCount - 1) : prev - 1,
|
|
270
281
|
);
|
|
271
282
|
},
|
|
272
283
|
moveDown: () => {
|
|
273
|
-
setSelectedIndex((prev) =>
|
|
284
|
+
setSelectedIndex((prev) =>
|
|
285
|
+
prev >= itemCount - 1 ? firstIndex : prev + 1,
|
|
286
|
+
);
|
|
274
287
|
},
|
|
275
288
|
getSelectedIndex: () => selectedIndex,
|
|
276
289
|
getSelectedMention: () => flatMentionItems[selectedIndex] ?? null,
|
|
@@ -278,6 +291,11 @@ export const MentionPopover = forwardRef<
|
|
|
278
291
|
if (type !== "/" || selectedIndex >= commands.length) return null;
|
|
279
292
|
return commands[selectedIndex] ?? null;
|
|
280
293
|
},
|
|
294
|
+
activateRetry: () => {
|
|
295
|
+
if (!retryable || selectedIndex !== -1) return false;
|
|
296
|
+
onRetry!();
|
|
297
|
+
return true;
|
|
298
|
+
},
|
|
281
299
|
}));
|
|
282
300
|
|
|
283
301
|
if (!position) return null;
|
|
@@ -287,6 +305,7 @@ export const MentionPopover = forwardRef<
|
|
|
287
305
|
{/* Backdrop to capture outside clicks */}
|
|
288
306
|
<div className="fixed inset-0 z-[9998]" onClick={onClose} />
|
|
289
307
|
<div
|
|
308
|
+
ref={containerRef}
|
|
290
309
|
data-agent-native-composer-popover="true"
|
|
291
310
|
data-agent-native-mention-popover="true"
|
|
292
311
|
className="fixed z-[9999] overflow-y-auto rounded-lg border border-border/80 bg-popover p-1 shadow-2xl"
|
|
@@ -308,9 +327,32 @@ export const MentionPopover = forwardRef<
|
|
|
308
327
|
),
|
|
309
328
|
}}
|
|
310
329
|
>
|
|
330
|
+
{type === "@" && error && (
|
|
331
|
+
<div
|
|
332
|
+
role="alert"
|
|
333
|
+
className="flex items-center justify-between gap-2 px-3 py-2 text-xs text-destructive"
|
|
334
|
+
>
|
|
335
|
+
<span>{error}</span>
|
|
336
|
+
{onRetry && (
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
data-mention-index={-1}
|
|
340
|
+
className={`shrink-0 rounded px-1.5 py-0.5 font-medium text-foreground ${
|
|
341
|
+
selectedIndex === -1 ? "bg-muted" : "hover:bg-accent"
|
|
342
|
+
}`}
|
|
343
|
+
onMouseEnter={() => setSelectedIndex(-1)}
|
|
344
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
345
|
+
onClick={onRetry}
|
|
346
|
+
>
|
|
347
|
+
{t("agentChat.common.retry", { defaultValue: "Retry" })}
|
|
348
|
+
</button>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
311
352
|
{isLoading && itemCount === 0 ? (
|
|
312
353
|
<LoadingSkeleton />
|
|
313
|
-
) : itemCount === 0 ?
|
|
354
|
+
) : itemCount === 0 && type === "@" && error ? null : itemCount ===
|
|
355
|
+
0 ? (
|
|
314
356
|
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
|
|
315
357
|
{type === "@" ? (
|
|
316
358
|
query ? (
|
|
@@ -331,7 +373,7 @@ export const MentionPopover = forwardRef<
|
|
|
331
373
|
)}
|
|
332
374
|
</div>
|
|
333
375
|
) : (
|
|
334
|
-
<div
|
|
376
|
+
<div>
|
|
335
377
|
{isLoading && <LoadingSkeletonRow />}
|
|
336
378
|
{type === "@"
|
|
337
379
|
? (() => {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { PromptComposer, type PromptComposerProps } from "./PromptComposer.js";
|
|
8
|
+
import type {
|
|
9
|
+
ComposerTextReference,
|
|
10
|
+
TiptapComposerHandle,
|
|
11
|
+
} from "./TiptapComposer.js";
|
|
12
|
+
import type { Reference } from "./types.js";
|
|
13
|
+
|
|
14
|
+
let container: HTMLDivElement;
|
|
15
|
+
let root: Root;
|
|
16
|
+
let composerRef: React.RefObject<TiptapComposerHandle | null>;
|
|
17
|
+
let reported: Reference[];
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
21
|
+
container = document.createElement("div");
|
|
22
|
+
document.body.appendChild(container);
|
|
23
|
+
root = createRoot(container);
|
|
24
|
+
composerRef = React.createRef<TiptapComposerHandle>();
|
|
25
|
+
reported = [];
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
act(() => root.unmount());
|
|
30
|
+
container.remove();
|
|
31
|
+
vi.unstubAllGlobals();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function person(start: number, end: number, email: string, label: string) {
|
|
35
|
+
return {
|
|
36
|
+
start,
|
|
37
|
+
end,
|
|
38
|
+
reference: { label, source: "test", refType: "person", refId: email },
|
|
39
|
+
} satisfies ComposerTextReference;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function render(props: Partial<PromptComposerProps> = {}) {
|
|
43
|
+
await act(async () => {
|
|
44
|
+
root.render(
|
|
45
|
+
<PromptComposer
|
|
46
|
+
composerRef={composerRef}
|
|
47
|
+
onSubmit={vi.fn()}
|
|
48
|
+
onReferencesChange={(references) => (reported = references)}
|
|
49
|
+
showModelSelector={false}
|
|
50
|
+
modelStatusChecksEnabled={false}
|
|
51
|
+
includeDefaultMentionSearch={false}
|
|
52
|
+
includeDefaultSlashSkills={false}
|
|
53
|
+
voiceEnabled={false}
|
|
54
|
+
{...props}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
await act(async () => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Each line, with every chip written as [refId]. */
|
|
62
|
+
function lines() {
|
|
63
|
+
return [...container.querySelectorAll(".ProseMirror > p")].map((line) =>
|
|
64
|
+
[...line.childNodes]
|
|
65
|
+
.map((node) => {
|
|
66
|
+
const chip =
|
|
67
|
+
node instanceof Element
|
|
68
|
+
? (node.closest("[data-mention-ref-id]") ??
|
|
69
|
+
node.querySelector("[data-mention-ref-id]"))
|
|
70
|
+
: null;
|
|
71
|
+
return chip
|
|
72
|
+
? `[${chip.getAttribute("data-mention-ref-id")}]`
|
|
73
|
+
: node.textContent;
|
|
74
|
+
})
|
|
75
|
+
.join(""),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("composer text with chips", () => {
|
|
80
|
+
it("seeds the initial text with each chip in place of its range", async () => {
|
|
81
|
+
await render({
|
|
82
|
+
initialText: "@Sam Lee and @Sam Lee, thanks",
|
|
83
|
+
initialTextKey: "seeded",
|
|
84
|
+
initialReferences: [
|
|
85
|
+
person(0, 8, "b@example.test", "Sam Lee"),
|
|
86
|
+
person(13, 21, "a@example.test", "Sam Lee"),
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(lines()).toEqual(["[b@example.test] and [a@example.test], thanks"]);
|
|
91
|
+
expect(reported.map((reference) => reference.refId)).toEqual([
|
|
92
|
+
"b@example.test",
|
|
93
|
+
"a@example.test",
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("replaces the draft with chips across lines, leaving an overlapping range as text and a space after a final chip", async () => {
|
|
98
|
+
await render({ initialText: "Old draft", initialTextKey: "old" });
|
|
99
|
+
|
|
100
|
+
await act(async () =>
|
|
101
|
+
composerRef.current!.setText("Hi @Ana\n@Bo and @Cy", [
|
|
102
|
+
person(3, 7, "ana@example.test", "Ana"),
|
|
103
|
+
person(16, 19, "cy@example.test", "Cy"),
|
|
104
|
+
person(8, 11, "bo@example.test", "Bo"),
|
|
105
|
+
person(9, 12, "overlap@example.test", "o a"),
|
|
106
|
+
]),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(lines()).toEqual([
|
|
110
|
+
"Hi [ana@example.test]",
|
|
111
|
+
"[bo@example.test] and [cy@example.test] ",
|
|
112
|
+
]);
|
|
113
|
+
expect(reported.map((reference) => reference.refId)).toEqual([
|
|
114
|
+
"ana@example.test",
|
|
115
|
+
"bo@example.test",
|
|
116
|
+
"cy@example.test",
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("reports each chip as its own @name, apart from the words beside it", async () => {
|
|
121
|
+
let text = "";
|
|
122
|
+
await render({ onTextChange: (next) => (text = next) });
|
|
123
|
+
|
|
124
|
+
await act(async () =>
|
|
125
|
+
composerRef.current!.setText("word@Sam@Bo and @Cyx", [
|
|
126
|
+
person(4, 8, "sam@example.test", "Sam"),
|
|
127
|
+
person(8, 11, "bo@example.test", "Bo"),
|
|
128
|
+
person(16, 19, "cy@example.test", "Cy"),
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(lines()).toEqual([
|
|
133
|
+
"word[sam@example.test][bo@example.test] and [cy@example.test]x",
|
|
134
|
+
]);
|
|
135
|
+
expect(text).toBe("word @Sam @Bo and @Cy x");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("spaces a chip from a combining mark and a letter past U+FFFF", async () => {
|
|
139
|
+
let text = "";
|
|
140
|
+
await render({ onTextChange: (next) => (text = next) });
|
|
141
|
+
|
|
142
|
+
await act(async () =>
|
|
143
|
+
composerRef.current!.setText("José@Sam\u{10400}", [
|
|
144
|
+
person(5, 9, "sam@example.test", "Sam"),
|
|
145
|
+
]),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(text).toBe("José @Sam \u{10400}");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("inserting @ at the cursor", () => {
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
delete (document as { execCommand?: unknown }).execCommand;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("filters and picks a mention without touching the text before the @", async () => {
|
|
158
|
+
await render({
|
|
159
|
+
initialText: "Ping",
|
|
160
|
+
initialTextKey: "ping",
|
|
161
|
+
mentionItems: [
|
|
162
|
+
{
|
|
163
|
+
id: "person:sam",
|
|
164
|
+
label: "Sam Lee",
|
|
165
|
+
source: "test",
|
|
166
|
+
refType: "person",
|
|
167
|
+
refId: "sam@example.test",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
const editor = () =>
|
|
172
|
+
(
|
|
173
|
+
container.querySelector(".ProseMirror") as HTMLElement & {
|
|
174
|
+
editor: { commands: { insertContent(text: string): boolean } };
|
|
175
|
+
}
|
|
176
|
+
).editor;
|
|
177
|
+
// A browser applies execCommand's edit to the DOM, and ProseMirror only
|
|
178
|
+
// reads it back after the call returns.
|
|
179
|
+
Object.defineProperty(document, "execCommand", {
|
|
180
|
+
configurable: true,
|
|
181
|
+
value: (_command: string, _showUi: boolean, text: string) => {
|
|
182
|
+
queueMicrotask(() => editor().commands.insertContent(text));
|
|
183
|
+
return true;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await act(async () => composerRef.current!.insertTextAtCursor!(" "));
|
|
188
|
+
await act(async () => composerRef.current!.insertTextAtCursor!("@"));
|
|
189
|
+
await act(async () => void editor().commands.insertContent("Sa"));
|
|
190
|
+
const option = [
|
|
191
|
+
...document.querySelectorAll<HTMLElement>("[data-mention-index]"),
|
|
192
|
+
].find((item) => item.textContent?.includes("Sam Lee"));
|
|
193
|
+
expect(option).toBeDefined();
|
|
194
|
+
await act(async () => option!.click());
|
|
195
|
+
|
|
196
|
+
expect(lines()).toEqual(["Ping [sam@example.test] "]);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
isLocalRuntimeEngine,
|
|
56
56
|
TiptapComposer,
|
|
57
57
|
type ComposerAgentOption,
|
|
58
|
+
type ComposerTextReference,
|
|
58
59
|
type ComposerTextSelection,
|
|
59
60
|
type ComposerImageModelMenu,
|
|
60
61
|
type ComposerSubmitIntent,
|
|
@@ -188,6 +189,8 @@ export interface PromptComposerProps {
|
|
|
188
189
|
initialText?: string;
|
|
189
190
|
/** Stable key used to re-apply `initialText` when the host picks a preset. */
|
|
190
191
|
initialTextKey?: string | number;
|
|
192
|
+
/** Chips placed into `initialText`, as the composer handle's `setText` takes them. */
|
|
193
|
+
initialReferences?: readonly ComposerTextReference[];
|
|
191
194
|
/** Optional host-owned control rendered directly after the "+" button. */
|
|
192
195
|
modeControl?: ReactNode;
|
|
193
196
|
/** Current agent execution mode shown in the shared composer toolbar. */
|
|
@@ -244,6 +247,9 @@ export interface PromptComposerProps {
|
|
|
244
247
|
/** Called whenever the plain editor text changes. */
|
|
245
248
|
onTextChange?: (text: string) => void;
|
|
246
249
|
mentionItems?: MentionItem[];
|
|
250
|
+
/** Why some of `mentionItems` could not load, shown in the @ popover. */
|
|
251
|
+
mentionItemsError?: string;
|
|
252
|
+
onRetryMentionItems?: () => void;
|
|
247
253
|
mentionPopoverDensity?: "default" | "stacked";
|
|
248
254
|
includeDefaultMentionSearch?: boolean;
|
|
249
255
|
onReferencesChange?: (references: Reference[]) => void;
|
|
@@ -657,6 +663,7 @@ function PromptComposerInner({
|
|
|
657
663
|
extensionTools = false,
|
|
658
664
|
initialText,
|
|
659
665
|
initialTextKey,
|
|
666
|
+
initialReferences,
|
|
660
667
|
modeControl,
|
|
661
668
|
requireAgentEngine = false,
|
|
662
669
|
execMode,
|
|
@@ -689,6 +696,8 @@ function PromptComposerInner({
|
|
|
689
696
|
modelStatusChecksEnabled,
|
|
690
697
|
onTextChange,
|
|
691
698
|
mentionItems,
|
|
699
|
+
mentionItemsError,
|
|
700
|
+
onRetryMentionItems,
|
|
692
701
|
mentionPopoverDensity,
|
|
693
702
|
includeDefaultMentionSearch,
|
|
694
703
|
onReferencesChange,
|
|
@@ -978,6 +987,7 @@ function PromptComposerInner({
|
|
|
978
987
|
placeholder={placeholder}
|
|
979
988
|
initialText={initialText}
|
|
980
989
|
initialTextKey={initialTextKey}
|
|
990
|
+
initialReferences={initialReferences}
|
|
981
991
|
onSubmit={handleSubmit}
|
|
982
992
|
onEmptySubmit={onEmptySubmit}
|
|
983
993
|
onBeforeSubmit={beforeSubmit}
|
|
@@ -1014,6 +1024,8 @@ function PromptComposerInner({
|
|
|
1014
1024
|
voiceEnabled={voiceEnabled}
|
|
1015
1025
|
onTextChange={onTextChange}
|
|
1016
1026
|
mentionItems={mentionItems}
|
|
1027
|
+
mentionItemsError={mentionItemsError}
|
|
1028
|
+
onRetryMentionItems={onRetryMentionItems}
|
|
1017
1029
|
mentionPopoverDensity={mentionPopoverDensity}
|
|
1018
1030
|
includeDefaultMentionSearch={includeDefaultMentionSearch}
|
|
1019
1031
|
onReferencesChange={onReferencesChange}
|