@agent-native/toolkit 0.22.2 → 0.22.3-nightly-20260927133429
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/composer/ComposerContextMenu.d.ts.map +1 -1
- package/dist/composer/ComposerContextMenu.js +12 -3
- package/dist/composer/ComposerContextMenu.js.map +1 -1
- package/dist/composer/MentionItemMedia.d.ts +1 -1
- package/dist/composer/MentionItemMedia.d.ts.map +1 -1
- package/dist/composer/MentionItemMedia.js +10 -5
- package/dist/composer/MentionItemMedia.js.map +1 -1
- package/dist/composer/MentionPopover.d.ts +5 -0
- package/dist/composer/MentionPopover.d.ts.map +1 -1
- package/dist/composer/MentionPopover.js +17 -6
- package/dist/composer/MentionPopover.js.map +1 -1
- package/dist/composer/PromptComposer.d.ts +11 -2
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +7 -6
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts +26 -2
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +262 -27
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/TiptapComposer.spec.js +21 -1
- package/dist/composer/TiptapComposer.spec.js.map +1 -1
- package/dist/composer/extensions/MentionReference.d.ts.map +1 -1
- package/dist/composer/extensions/MentionReference.js +1 -1
- package/dist/composer/extensions/MentionReference.js.map +1 -1
- package/dist/composer/index.d.ts +1 -1
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +1 -1
- package/dist/composer/index.js.map +1 -1
- package/dist/composer/types.d.ts +6 -0
- package/dist/composer/types.d.ts.map +1 -1
- package/dist/editor/useCollabReconcile.concurrent.spec.js +106 -0
- package/dist/editor/useCollabReconcile.concurrent.spec.js.map +1 -1
- package/dist/editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/editor/useCollabReconcile.js +42 -24
- package/dist/editor/useCollabReconcile.js.map +1 -1
- package/package.json +1 -1
- package/src/composer/ComposerContextMenu.tsx +15 -3
- package/src/composer/MentionItemMedia.tsx +13 -13
- package/src/composer/MentionPopover.tsx +52 -10
- package/src/composer/PromptComposer.tsx +34 -5
- package/src/composer/TiptapComposer.spec.ts +24 -0
- package/src/composer/TiptapComposer.tsx +346 -27
- package/src/composer/extensions/MentionReference.tsx +3 -0
- package/src/composer/index.ts +2 -0
- package/src/composer/types.ts +6 -0
- package/src/editor/useCollabReconcile.concurrent.spec.ts +124 -0
- package/src/editor/useCollabReconcile.ts +43 -24
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
IconLoader2,
|
|
21
21
|
} from "@tabler/icons-react";
|
|
22
22
|
import Placeholder from "@tiptap/extension-placeholder";
|
|
23
|
+
import { TextSelection } from "@tiptap/pm/state";
|
|
23
24
|
import type { EditorView } from "@tiptap/pm/view";
|
|
24
25
|
import { useEditor, EditorContent } from "@tiptap/react";
|
|
25
26
|
import StarterKit from "@tiptap/starter-kit";
|
|
@@ -107,10 +108,29 @@ export interface TiptapComposerHandle {
|
|
|
107
108
|
addAttachment(file: File): Promise<unknown>;
|
|
108
109
|
/** Insert text through the editor's normal input path. */
|
|
109
110
|
insertText(text: string): void;
|
|
111
|
+
/**
|
|
112
|
+
* Insert text at the current selection, keeping the existing draft. Typed
|
|
113
|
+
* triggers such as `@` open their menus as if the person typed them.
|
|
114
|
+
*/
|
|
115
|
+
insertTextAtCursor?(text: string): void;
|
|
110
116
|
setText(text: string): void;
|
|
111
117
|
/** Submit replacement text with the current attachments and context, without editing the draft on failure. */
|
|
112
118
|
submitWithText(text: string): Promise<boolean>;
|
|
113
119
|
insertReference(ref: AgentComposerReference): void;
|
|
120
|
+
replaceReference(refType: string, ref: AgentComposerReference | null): void;
|
|
121
|
+
getSelection(): ComposerTextSelection | null;
|
|
122
|
+
setSelection(
|
|
123
|
+
start: number,
|
|
124
|
+
end?: number,
|
|
125
|
+
direction?: ComposerTextSelection["direction"],
|
|
126
|
+
): void;
|
|
127
|
+
dismissPopover(): boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ComposerTextSelection {
|
|
131
|
+
start: number;
|
|
132
|
+
end: number;
|
|
133
|
+
direction: "forward" | "backward" | "none";
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
export type ComposerSubmitIntent = "immediate" | "queued";
|
|
@@ -199,7 +219,7 @@ function composerReferenceFromMentionItem(
|
|
|
199
219
|
item: MentionItem,
|
|
200
220
|
): AgentComposerReference {
|
|
201
221
|
return {
|
|
202
|
-
label: item.label,
|
|
222
|
+
label: item.referenceLabel ?? item.label,
|
|
203
223
|
icon: item.icon || "file",
|
|
204
224
|
media: item.media,
|
|
205
225
|
source: item.source,
|
|
@@ -214,6 +234,29 @@ function composerReferenceFromMentionItem(
|
|
|
214
234
|
};
|
|
215
235
|
}
|
|
216
236
|
|
|
237
|
+
export function mentionItemMatchesQuery(
|
|
238
|
+
item: MentionItem,
|
|
239
|
+
query: string,
|
|
240
|
+
): boolean {
|
|
241
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
242
|
+
if (!normalizedQuery) return true;
|
|
243
|
+
return [item.label, ...(item.aliases ?? []), item.description ?? ""].some(
|
|
244
|
+
(candidate) => candidate.toLowerCase().includes(normalizedQuery),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function findExactMentionItem(
|
|
249
|
+
items: MentionItem[],
|
|
250
|
+
query: string,
|
|
251
|
+
): MentionItem | undefined {
|
|
252
|
+
const normalizedQuery = query.toLowerCase();
|
|
253
|
+
return items.find((item) =>
|
|
254
|
+
[item.label, ...(item.aliases ?? [])].some(
|
|
255
|
+
(candidate) => candidate.toLowerCase() === normalizedQuery,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
217
260
|
function mentionReferenceAttrs(ref: AgentComposerReference) {
|
|
218
261
|
return {
|
|
219
262
|
label: ref.label,
|
|
@@ -825,6 +868,14 @@ export interface TiptapComposerProps {
|
|
|
825
868
|
clearOnSubmit?: boolean;
|
|
826
869
|
/** Called whenever the plain editor text changes. */
|
|
827
870
|
onTextChange?: (text: string) => void;
|
|
871
|
+
mentionItems?: MentionItem[];
|
|
872
|
+
mentionPopoverDensity?: "default" | "stacked";
|
|
873
|
+
includeDefaultMentionSearch?: boolean;
|
|
874
|
+
onReferencesChange?: (references: Reference[]) => void;
|
|
875
|
+
onEscape?: () => void;
|
|
876
|
+
onFocus?: () => void;
|
|
877
|
+
onBlur?: () => void;
|
|
878
|
+
onSelectionChange?: (selection: ComposerTextSelection) => void;
|
|
828
879
|
/** Custom action button (e.g. stop button) to render instead of the default send button. */
|
|
829
880
|
actionButton?: React.ReactNode;
|
|
830
881
|
/** Whether the default send action will wait behind existing work. */
|
|
@@ -2536,6 +2587,14 @@ export function TiptapComposer({
|
|
|
2536
2587
|
extensionTools = false,
|
|
2537
2588
|
interceptBuildRequestsForBuilder = false,
|
|
2538
2589
|
onAttachmentError,
|
|
2590
|
+
mentionItems: hostMentionItems = [],
|
|
2591
|
+
mentionPopoverDensity = "default",
|
|
2592
|
+
includeDefaultMentionSearch = true,
|
|
2593
|
+
onReferencesChange,
|
|
2594
|
+
onEscape,
|
|
2595
|
+
onFocus,
|
|
2596
|
+
onBlur,
|
|
2597
|
+
onSelectionChange,
|
|
2539
2598
|
}: TiptapComposerProps) {
|
|
2540
2599
|
const contextItems = providedContextItems ?? [];
|
|
2541
2600
|
const adapters = useComposerRuntimeAdapters();
|
|
@@ -2552,6 +2611,7 @@ export function TiptapComposer({
|
|
|
2552
2611
|
} | null>(null);
|
|
2553
2612
|
const submitInFlightRef = useRef(false);
|
|
2554
2613
|
const [editorHasText, setEditorHasText] = useState(false);
|
|
2614
|
+
const [referenceRevision, setReferenceRevision] = useState(0);
|
|
2555
2615
|
const [slotReferences, setSlotReferences] = useState<
|
|
2556
2616
|
AgentComposerReference[]
|
|
2557
2617
|
>([]);
|
|
@@ -2586,6 +2646,7 @@ export function TiptapComposer({
|
|
|
2586
2646
|
|
|
2587
2647
|
// Refs for values accessed in handleKeyDown (ProseMirror doesn't re-bind)
|
|
2588
2648
|
const popoverStateRef = useRef<PopoverState>(null);
|
|
2649
|
+
const composingRef = useRef(false);
|
|
2589
2650
|
const onAttachmentErrorRef = useRef(onAttachmentError);
|
|
2590
2651
|
onAttachmentErrorRef.current = onAttachmentError;
|
|
2591
2652
|
const execModeRef = useRef(execMode);
|
|
@@ -2597,11 +2658,25 @@ export function TiptapComposer({
|
|
|
2597
2658
|
|
|
2598
2659
|
const { items: mentionItems, isLoading: mentionsLoading } = useMentionSearch(
|
|
2599
2660
|
popover?.type === "@" ? popover.query : "",
|
|
2600
|
-
popover?.type === "@",
|
|
2661
|
+
includeDefaultMentionSearch && popover?.type === "@",
|
|
2601
2662
|
);
|
|
2663
|
+
const mentionQuery = popover?.type === "@" ? popover.query : "";
|
|
2602
2664
|
const filteredMentionItems = useMemo(
|
|
2603
|
-
() =>
|
|
2604
|
-
|
|
2665
|
+
() =>
|
|
2666
|
+
filterMentionItemsForSlots(
|
|
2667
|
+
[
|
|
2668
|
+
// Host items arrive unfiltered; the default search filters itself.
|
|
2669
|
+
...hostMentionItems.filter((item) =>
|
|
2670
|
+
mentionItemMatchesQuery(item, mentionQuery),
|
|
2671
|
+
),
|
|
2672
|
+
...mentionItems,
|
|
2673
|
+
].filter(
|
|
2674
|
+
(item, index, items) =>
|
|
2675
|
+
items.findIndex((candidate) => candidate.id === item.id) === index,
|
|
2676
|
+
),
|
|
2677
|
+
slotReferences,
|
|
2678
|
+
),
|
|
2679
|
+
[hostMentionItems, mentionItems, mentionQuery, slotReferences],
|
|
2605
2680
|
);
|
|
2606
2681
|
|
|
2607
2682
|
const {
|
|
@@ -2775,17 +2850,27 @@ export function TiptapComposer({
|
|
|
2775
2850
|
// Drive the send button's enabled state from the actual editor contents;
|
|
2776
2851
|
// the composer runtime is only synced on submit, so its isEmpty lags.
|
|
2777
2852
|
setEditorHasText(composerDocumentHasContent(ed.state.doc));
|
|
2778
|
-
onTextChangeRef.current?.(ed.
|
|
2853
|
+
onTextChangeRef.current?.(ed.getText({ blockSeparator: "\n" }).trim());
|
|
2854
|
+
setReferenceRevision((revision) => revision + 1);
|
|
2779
2855
|
|
|
2780
2856
|
scheduleComposerDraftPersist(ed);
|
|
2781
2857
|
},
|
|
2782
2858
|
onSelectionUpdate: ({ editor: ed }) => {
|
|
2783
|
-
const { from, to } = ed.state.selection;
|
|
2859
|
+
const { from, to, anchor, head } = ed.state.selection;
|
|
2860
|
+
if (ed.isFocused)
|
|
2861
|
+
onSelectionChange?.({
|
|
2862
|
+
start: from,
|
|
2863
|
+
end: to,
|
|
2864
|
+
direction:
|
|
2865
|
+
anchor === head ? "none" : anchor > head ? "backward" : "forward",
|
|
2866
|
+
});
|
|
2784
2867
|
if (selectedContextItemKeyRef.current && (from !== to || from > 1)) {
|
|
2785
2868
|
selectedContextItemKeyRef.current = null;
|
|
2786
2869
|
setSelectedContextItemKey(null);
|
|
2787
2870
|
}
|
|
2788
2871
|
},
|
|
2872
|
+
onFocus,
|
|
2873
|
+
onBlur,
|
|
2789
2874
|
editorProps: {
|
|
2790
2875
|
attributes: {
|
|
2791
2876
|
"aria-label": ariaLabel ?? resolvedPlaceholder,
|
|
@@ -2796,6 +2881,34 @@ export function TiptapComposer({
|
|
|
2796
2881
|
class:
|
|
2797
2882
|
"agent-composer-prosemirror flex-1 resize-none bg-transparent text-sm text-foreground outline-none leading-[1.625rem] min-h-[3.25rem] max-h-[10rem] overflow-y-auto",
|
|
2798
2883
|
},
|
|
2884
|
+
handleDOMEvents: {
|
|
2885
|
+
compositionstart: () => {
|
|
2886
|
+
composingRef.current = true;
|
|
2887
|
+
return false;
|
|
2888
|
+
},
|
|
2889
|
+
compositionend: () => {
|
|
2890
|
+
composingRef.current = false;
|
|
2891
|
+
return false;
|
|
2892
|
+
},
|
|
2893
|
+
keydown: (_view, event) => {
|
|
2894
|
+
if (event.key !== "Escape" || !event.defaultPrevented) return false;
|
|
2895
|
+
if (
|
|
2896
|
+
event.isComposing ||
|
|
2897
|
+
event.keyCode === 229 ||
|
|
2898
|
+
composingRef.current
|
|
2899
|
+
) {
|
|
2900
|
+
event.stopPropagation();
|
|
2901
|
+
return true;
|
|
2902
|
+
}
|
|
2903
|
+
if (popoverStateRef.current) {
|
|
2904
|
+
closePopover();
|
|
2905
|
+
} else {
|
|
2906
|
+
onEscape?.();
|
|
2907
|
+
}
|
|
2908
|
+
event.stopPropagation();
|
|
2909
|
+
return true;
|
|
2910
|
+
},
|
|
2911
|
+
},
|
|
2799
2912
|
handlePaste: (view, event) => {
|
|
2800
2913
|
if (disabled) {
|
|
2801
2914
|
if (event.clipboardData?.files.length) {
|
|
@@ -2907,10 +3020,25 @@ export function TiptapComposer({
|
|
|
2907
3020
|
});
|
|
2908
3021
|
},
|
|
2909
3022
|
handleKeyDown: (view, event) => {
|
|
3023
|
+
if (event.isComposing || event.keyCode === 229) {
|
|
3024
|
+
event.stopPropagation();
|
|
3025
|
+
return false;
|
|
3026
|
+
}
|
|
2910
3027
|
const pop = popoverStateRef.current;
|
|
2911
3028
|
|
|
2912
3029
|
// Handle popover keyboard nav
|
|
2913
3030
|
if (pop) {
|
|
3031
|
+
if (event.key === " " && pop.type === "@" && pop.query) {
|
|
3032
|
+
const exact = findExactMentionItem(
|
|
3033
|
+
mentionItemsRef.current,
|
|
3034
|
+
pop.query,
|
|
3035
|
+
);
|
|
3036
|
+
if (exact) {
|
|
3037
|
+
event.preventDefault();
|
|
3038
|
+
selectMention(view, pop, exact);
|
|
3039
|
+
return true;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
2914
3042
|
if (event.key === "ArrowUp") {
|
|
2915
3043
|
event.preventDefault();
|
|
2916
3044
|
popoverRef.current?.moveUp();
|
|
@@ -3025,6 +3153,13 @@ export function TiptapComposer({
|
|
|
3025
3153
|
return true;
|
|
3026
3154
|
}
|
|
3027
3155
|
|
|
3156
|
+
if (event.key === "Escape" && onEscape) {
|
|
3157
|
+
event.preventDefault();
|
|
3158
|
+
event.stopPropagation();
|
|
3159
|
+
onEscape();
|
|
3160
|
+
return true;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3028
3163
|
// Detect @ trigger — only when preceded by start-of-text, space, or newline
|
|
3029
3164
|
// (not after alphanumeric chars, which would indicate an email address)
|
|
3030
3165
|
if (event.key === "@") {
|
|
@@ -3109,8 +3244,9 @@ export function TiptapComposer({
|
|
|
3109
3244
|
// correct immediately after a tab switch, not only after the next keystroke.
|
|
3110
3245
|
useEffect(() => {
|
|
3111
3246
|
if (!isComposerEditorUsable(editor) || !onTextChange) return;
|
|
3112
|
-
|
|
3113
|
-
|
|
3247
|
+
const currentText = editor.getText({ blockSeparator: "\n" }).trim();
|
|
3248
|
+
if (initialText === undefined) onTextChange(currentText);
|
|
3249
|
+
}, [editor, initialText, onTextChange]);
|
|
3114
3250
|
|
|
3115
3251
|
const insertReference = useCallback(
|
|
3116
3252
|
(ref: AgentComposerReference) => {
|
|
@@ -3241,11 +3377,46 @@ export function TiptapComposer({
|
|
|
3241
3377
|
editor.commands.insertContent(text);
|
|
3242
3378
|
}
|
|
3243
3379
|
},
|
|
3380
|
+
insertTextAtCursor(text: string) {
|
|
3381
|
+
if (!isComposerEditorUsable(editor)) return;
|
|
3382
|
+
editor.commands.focus();
|
|
3383
|
+
// An inserted "@" (an @ toolbar button) opens the mention menu just as
|
|
3384
|
+
// typing it does; after a word it needs a space to count as a trigger.
|
|
3385
|
+
const mention = text === "@";
|
|
3386
|
+
let inserted = text;
|
|
3387
|
+
if (mention) {
|
|
3388
|
+
const { from } = editor.state.selection;
|
|
3389
|
+
const before = editor.state.doc.textBetween(
|
|
3390
|
+
Math.max(0, from - 1),
|
|
3391
|
+
from,
|
|
3392
|
+
);
|
|
3393
|
+
if (from > 1 && before !== "" && !/\s/.test(before)) inserted = ` @`;
|
|
3394
|
+
}
|
|
3395
|
+
if (
|
|
3396
|
+
typeof document.execCommand !== "function" ||
|
|
3397
|
+
!document.execCommand("insertText", false, inserted)
|
|
3398
|
+
) {
|
|
3399
|
+
editor.commands.insertContent(inserted);
|
|
3400
|
+
}
|
|
3401
|
+
if (!mention) return;
|
|
3402
|
+
const view = editor.view;
|
|
3403
|
+
const startPos = view.state.selection.from;
|
|
3404
|
+
const position = getComposerPopoverAnchorPosition(view, startPos - 1);
|
|
3405
|
+
if (!position) return;
|
|
3406
|
+
const state: PopoverState = {
|
|
3407
|
+
type: "@",
|
|
3408
|
+
position,
|
|
3409
|
+
startPos,
|
|
3410
|
+
query: "",
|
|
3411
|
+
};
|
|
3412
|
+
popoverStateRef.current = state;
|
|
3413
|
+
setPopover(state);
|
|
3414
|
+
},
|
|
3244
3415
|
setText(text: string) {
|
|
3245
3416
|
if (!isComposerEditorUsable(editor)) return;
|
|
3246
3417
|
editor.commands.setContent(plainTextToDoc(text));
|
|
3247
3418
|
editor.commands.focus("end");
|
|
3248
|
-
const trimmed = editor.
|
|
3419
|
+
const trimmed = editor.getText({ blockSeparator: "\n" }).trim();
|
|
3249
3420
|
setEditorHasText(trimmed.length > 0);
|
|
3250
3421
|
setSlotReferences([]);
|
|
3251
3422
|
composerRuntime.setText(trimmed);
|
|
@@ -3254,6 +3425,84 @@ export function TiptapComposer({
|
|
|
3254
3425
|
},
|
|
3255
3426
|
submitWithText: (text: string) => submitComposer("immediate", text),
|
|
3256
3427
|
insertReference,
|
|
3428
|
+
replaceReference(refType, ref) {
|
|
3429
|
+
if (!isComposerEditorUsable(editor)) return;
|
|
3430
|
+
const positions: number[] = [];
|
|
3431
|
+
editor.state.doc.descendants((node: any, pos: number) => {
|
|
3432
|
+
if (
|
|
3433
|
+
node.type.name === "mentionReference" &&
|
|
3434
|
+
node.attrs.refType === refType
|
|
3435
|
+
) {
|
|
3436
|
+
positions.push(pos);
|
|
3437
|
+
}
|
|
3438
|
+
});
|
|
3439
|
+
if (positions.length === 0) {
|
|
3440
|
+
if (ref) insertReference(ref);
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
const referencePosition = positions[0]!;
|
|
3444
|
+
const node = editor.state.doc.nodeAt(referencePosition);
|
|
3445
|
+
if (!node) return;
|
|
3446
|
+
const normalized = ref
|
|
3447
|
+
? (adapters.agentChat!.normalizeReference!(
|
|
3448
|
+
ref,
|
|
3449
|
+
) as AgentComposerReference)
|
|
3450
|
+
: null;
|
|
3451
|
+
editor
|
|
3452
|
+
.chain()
|
|
3453
|
+
.focus()
|
|
3454
|
+
.command(({ tr }) => {
|
|
3455
|
+
for (const duplicatePosition of positions.slice(1).reverse()) {
|
|
3456
|
+
const duplicate = tr.doc.nodeAt(duplicatePosition);
|
|
3457
|
+
if (duplicate) {
|
|
3458
|
+
tr.delete(
|
|
3459
|
+
duplicatePosition,
|
|
3460
|
+
duplicatePosition + duplicate.nodeSize,
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
if (normalized) {
|
|
3465
|
+
tr.setNodeMarkup(
|
|
3466
|
+
referencePosition,
|
|
3467
|
+
undefined,
|
|
3468
|
+
mentionReferenceAttrs(normalized),
|
|
3469
|
+
);
|
|
3470
|
+
} else {
|
|
3471
|
+
tr.delete(referencePosition, referencePosition + node.nodeSize);
|
|
3472
|
+
}
|
|
3473
|
+
return true;
|
|
3474
|
+
})
|
|
3475
|
+
.run();
|
|
3476
|
+
},
|
|
3477
|
+
getSelection() {
|
|
3478
|
+
if (!isComposerEditorUsable(editor)) return null;
|
|
3479
|
+
const { from, to, anchor, head } = editor.state.selection;
|
|
3480
|
+
return {
|
|
3481
|
+
start: from,
|
|
3482
|
+
end: to,
|
|
3483
|
+
direction:
|
|
3484
|
+
anchor === head ? "none" : anchor > head ? "backward" : "forward",
|
|
3485
|
+
};
|
|
3486
|
+
},
|
|
3487
|
+
setSelection(start, end = start, direction = "none") {
|
|
3488
|
+
if (!isComposerEditorUsable(editor)) return;
|
|
3489
|
+
const maxPosition = editor.state.doc.content.size;
|
|
3490
|
+
const boundedStart = Math.max(1, Math.min(start, maxPosition));
|
|
3491
|
+
const boundedEnd = Math.max(1, Math.min(end, maxPosition));
|
|
3492
|
+
const anchor = direction === "backward" ? boundedEnd : boundedStart;
|
|
3493
|
+
const head = direction === "backward" ? boundedStart : boundedEnd;
|
|
3494
|
+
editor.commands.focus();
|
|
3495
|
+
editor.view.dispatch(
|
|
3496
|
+
editor.state.tr.setSelection(
|
|
3497
|
+
TextSelection.create(editor.state.doc, anchor, head),
|
|
3498
|
+
),
|
|
3499
|
+
);
|
|
3500
|
+
},
|
|
3501
|
+
dismissPopover() {
|
|
3502
|
+
if (!popoverStateRef.current) return false;
|
|
3503
|
+
closePopover();
|
|
3504
|
+
return true;
|
|
3505
|
+
},
|
|
3257
3506
|
}));
|
|
3258
3507
|
|
|
3259
3508
|
const handleSelectMode = useCallback(
|
|
@@ -3568,6 +3817,16 @@ export function TiptapComposer({
|
|
|
3568
3817
|
return { text, references };
|
|
3569
3818
|
}, [editor, slotReferences]);
|
|
3570
3819
|
|
|
3820
|
+
const referencesSignatureRef = useRef("");
|
|
3821
|
+
useEffect(() => {
|
|
3822
|
+
if (!onReferencesChange) return;
|
|
3823
|
+
const references = extractComposerPayload().references;
|
|
3824
|
+
const signature = JSON.stringify(references);
|
|
3825
|
+
if (signature === referencesSignatureRef.current) return;
|
|
3826
|
+
referencesSignatureRef.current = signature;
|
|
3827
|
+
onReferencesChange(references);
|
|
3828
|
+
}, [referenceRevision, extractComposerPayload, onReferencesChange]);
|
|
3829
|
+
|
|
3571
3830
|
const syncComposerRuntimeState = useCallback(
|
|
3572
3831
|
(text: string, references: Reference[]) => {
|
|
3573
3832
|
const requestMode =
|
|
@@ -3893,8 +4152,7 @@ export function TiptapComposer({
|
|
|
3893
4152
|
|
|
3894
4153
|
// Helper functions that operate on the editor view directly
|
|
3895
4154
|
// These are called from handleKeyDown which can't use React state
|
|
3896
|
-
function
|
|
3897
|
-
_view: any,
|
|
4155
|
+
function insertSelectedMention(
|
|
3898
4156
|
pop: NonNullable<PopoverState>,
|
|
3899
4157
|
item: MentionItem,
|
|
3900
4158
|
) {
|
|
@@ -3903,8 +4161,72 @@ export function TiptapComposer({
|
|
|
3903
4161
|
const currentPos = ed.state.selection.from;
|
|
3904
4162
|
// startPos is after the trigger char, so -1 to include the @ or /
|
|
3905
4163
|
const deleteFrom = Math.max(0, pop.startPos - 1);
|
|
3906
|
-
|
|
3907
|
-
|
|
4164
|
+
const normalized = adapters.agentChat!.normalizeReference!(
|
|
4165
|
+
composerReferenceFromMentionItem(item),
|
|
4166
|
+
) as AgentComposerReference | null;
|
|
4167
|
+
if (!normalized) return;
|
|
4168
|
+
if (normalized.slotKey) {
|
|
4169
|
+
ed.chain()
|
|
4170
|
+
.focus()
|
|
4171
|
+
.deleteRange({ from: deleteFrom, to: currentPos })
|
|
4172
|
+
.run();
|
|
4173
|
+
insertReference(normalized);
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
if (normalized.relatedReferences?.some((reference) => reference.slotKey)) {
|
|
4177
|
+
setSlotReferences((current) =>
|
|
4178
|
+
applySlotReferenceChanges(current, normalized.relatedReferences ?? []),
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
4181
|
+
if (item.replaceExisting) {
|
|
4182
|
+
let existingPosition: number | null = null;
|
|
4183
|
+
ed.state.doc.descendants((node: any, pos: number) => {
|
|
4184
|
+
if (
|
|
4185
|
+
existingPosition === null &&
|
|
4186
|
+
node.type.name === "mentionReference" &&
|
|
4187
|
+
node.attrs.refType === normalized.refType
|
|
4188
|
+
) {
|
|
4189
|
+
existingPosition = pos;
|
|
4190
|
+
return false;
|
|
4191
|
+
}
|
|
4192
|
+
});
|
|
4193
|
+
if (existingPosition !== null) {
|
|
4194
|
+
const position = existingPosition;
|
|
4195
|
+
ed.chain()
|
|
4196
|
+
.focus()
|
|
4197
|
+
.command(({ tr }) => {
|
|
4198
|
+
tr.delete(deleteFrom, currentPos);
|
|
4199
|
+
tr.setNodeMarkup(
|
|
4200
|
+
tr.mapping.map(position),
|
|
4201
|
+
undefined,
|
|
4202
|
+
mentionReferenceAttrs(normalized),
|
|
4203
|
+
);
|
|
4204
|
+
tr.insertText(" ", tr.selection.from);
|
|
4205
|
+
return true;
|
|
4206
|
+
})
|
|
4207
|
+
.run();
|
|
4208
|
+
setEditorHasText(true);
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
ed.chain()
|
|
4213
|
+
.focus()
|
|
4214
|
+
.deleteRange({ from: deleteFrom, to: currentPos })
|
|
4215
|
+
.insertContent({
|
|
4216
|
+
type: "mentionReference",
|
|
4217
|
+
attrs: mentionReferenceAttrs(normalized),
|
|
4218
|
+
})
|
|
4219
|
+
.insertContent(" ")
|
|
4220
|
+
.run();
|
|
4221
|
+
setEditorHasText(true);
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
function selectMention(
|
|
4225
|
+
_view: any,
|
|
4226
|
+
pop: NonNullable<PopoverState>,
|
|
4227
|
+
item: MentionItem,
|
|
4228
|
+
) {
|
|
4229
|
+
insertSelectedMention(pop, item);
|
|
3908
4230
|
popoverStateRef.current = null;
|
|
3909
4231
|
setPopover(null);
|
|
3910
4232
|
}
|
|
@@ -3949,18 +4271,11 @@ export function TiptapComposer({
|
|
|
3949
4271
|
// Popover select handlers for click-based selection (from MentionPopover)
|
|
3950
4272
|
const handleSelectMention = useCallback(
|
|
3951
4273
|
(item: MentionItem) => {
|
|
3952
|
-
if (!
|
|
3953
|
-
|
|
3954
|
-
const deleteFrom = Math.max(0, popover.startPos - 1);
|
|
3955
|
-
editor
|
|
3956
|
-
.chain()
|
|
3957
|
-
.focus()
|
|
3958
|
-
.deleteRange({ from: deleteFrom, to: currentPos })
|
|
3959
|
-
.run();
|
|
3960
|
-
insertReference(composerReferenceFromMentionItem(item));
|
|
4274
|
+
if (!popover) return;
|
|
4275
|
+
insertSelectedMention(popover, item);
|
|
3961
4276
|
closePopover();
|
|
3962
4277
|
},
|
|
3963
|
-
[
|
|
4278
|
+
[popover, closePopover, insertReference],
|
|
3964
4279
|
);
|
|
3965
4280
|
|
|
3966
4281
|
const handleSelectCommand = useCallback(
|
|
@@ -4004,6 +4319,7 @@ export function TiptapComposer({
|
|
|
4004
4319
|
if (!isComposerEditorUsable(editor) || !popover) return;
|
|
4005
4320
|
|
|
4006
4321
|
const updateHandler = () => {
|
|
4322
|
+
if (composingRef.current) return;
|
|
4007
4323
|
const pop = popoverStateRef.current;
|
|
4008
4324
|
if (!pop) return;
|
|
4009
4325
|
const { from } = editor.state.selection;
|
|
@@ -4049,11 +4365,12 @@ export function TiptapComposer({
|
|
|
4049
4365
|
|
|
4050
4366
|
useEffect(() => {
|
|
4051
4367
|
if (!isComposerEditorUsable(editor)) return;
|
|
4368
|
+
if (initialText !== undefined) return;
|
|
4052
4369
|
if (previousDraftKeyRef.current !== draftKey) return;
|
|
4053
4370
|
if (composerText !== "") return;
|
|
4054
4371
|
if (editor.isEmpty) return;
|
|
4055
4372
|
editor.commands.clearContent();
|
|
4056
|
-
}, [composerText, draftKey, editor]);
|
|
4373
|
+
}, [composerText, draftKey, editor, initialText]);
|
|
4057
4374
|
|
|
4058
4375
|
useEffect(() => {
|
|
4059
4376
|
if (!isComposerEditorUsable(editor)) return;
|
|
@@ -4100,16 +4417,17 @@ export function TiptapComposer({
|
|
|
4100
4417
|
editor.commands.focus("end");
|
|
4101
4418
|
if (initialText !== undefined) initialTextKeyRef.current = key;
|
|
4102
4419
|
} else if (initialText === undefined) {
|
|
4103
|
-
onTextChangeRef.current?.(
|
|
4420
|
+
onTextChangeRef.current?.(
|
|
4421
|
+
editor.getText({ blockSeparator: "\n" }).trim(),
|
|
4422
|
+
);
|
|
4104
4423
|
return;
|
|
4105
4424
|
} else if (initialTextKeyRef.current !== key) {
|
|
4106
4425
|
initialTextKeyRef.current = key;
|
|
4107
4426
|
editor.commands.setContent(plainTextToDoc(initialText));
|
|
4108
|
-
editor.commands.focus("end");
|
|
4109
4427
|
} else {
|
|
4110
4428
|
return;
|
|
4111
4429
|
}
|
|
4112
|
-
const trimmed = editor.
|
|
4430
|
+
const trimmed = editor.getText({ blockSeparator: "\n" }).trim();
|
|
4113
4431
|
setEditorHasText(composerDocumentHasContent(editor.state.doc));
|
|
4114
4432
|
composerRuntime.setText(trimmed);
|
|
4115
4433
|
onTextChangeRef.current?.(trimmed);
|
|
@@ -4422,6 +4740,7 @@ export function TiptapComposer({
|
|
|
4422
4740
|
</div>
|
|
4423
4741
|
<MentionPopover
|
|
4424
4742
|
ref={popoverRef}
|
|
4743
|
+
density={mentionPopoverDensity}
|
|
4425
4744
|
type={popover?.type ?? "@"}
|
|
4426
4745
|
position={popover?.position ?? null}
|
|
4427
4746
|
mentionItems={filteredMentionItems}
|
|
@@ -49,6 +49,9 @@ const MentionReferenceComponent = ({ node }: { node: any }) => {
|
|
|
49
49
|
<span
|
|
50
50
|
className="inline-flex items-center gap-1 rounded-md border border-input bg-muted/50 px-1.5 py-0.5 text-xs font-medium text-foreground align-middle mx-0.5 max-w-[200px] select-none"
|
|
51
51
|
title={node.attrs.refPath || node.attrs.refId || node.attrs.label}
|
|
52
|
+
data-mention-reference=""
|
|
53
|
+
data-mention-ref-type={node.attrs.refType || undefined}
|
|
54
|
+
data-mention-ref-id={node.attrs.refId || undefined}
|
|
52
55
|
>
|
|
53
56
|
<MentionItemMedia
|
|
54
57
|
icon={node.attrs.icon}
|
package/src/composer/index.ts
CHANGED
|
@@ -44,12 +44,14 @@ export {
|
|
|
44
44
|
canSubmitComposerContent,
|
|
45
45
|
displayableComposerModeMessage,
|
|
46
46
|
getComposerSubmitIntentForEnterKey,
|
|
47
|
+
findExactMentionItem,
|
|
47
48
|
handleComposerFileDrop,
|
|
48
49
|
isLocalRuntimeEngine,
|
|
49
50
|
getOversizedDocumentAttachmentError,
|
|
50
51
|
insertComposerHardBreakAndScrollIntoView,
|
|
51
52
|
type ComposerSubmitIntent,
|
|
52
53
|
type TiptapComposerHandle,
|
|
54
|
+
type ComposerTextSelection,
|
|
53
55
|
type TiptapComposerProps,
|
|
54
56
|
type TiptapComposerSubmitOptions,
|
|
55
57
|
} from "./TiptapComposer.js";
|
package/src/composer/types.ts
CHANGED
|
@@ -29,6 +29,12 @@ export type MentionItemMedia =
|
|
|
29
29
|
export interface MentionItem {
|
|
30
30
|
id: string;
|
|
31
31
|
label: string;
|
|
32
|
+
/** Label stored in the inserted reference when it differs from the menu row. */
|
|
33
|
+
referenceLabel?: string;
|
|
34
|
+
/** Exact case-insensitive names that may commit this item with Space. */
|
|
35
|
+
aliases?: string[];
|
|
36
|
+
/** Replace the existing inline reference of this type instead of adding another. */
|
|
37
|
+
replaceExisting?: boolean;
|
|
32
38
|
description?: string;
|
|
33
39
|
icon?: string;
|
|
34
40
|
media?: MentionItemMedia;
|