@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.
Files changed (47) hide show
  1. package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
  2. package/dist/composer/ComposerContextMenu.js +12 -3
  3. package/dist/composer/ComposerContextMenu.js.map +1 -1
  4. package/dist/composer/MentionItemMedia.d.ts +1 -1
  5. package/dist/composer/MentionItemMedia.d.ts.map +1 -1
  6. package/dist/composer/MentionItemMedia.js +10 -5
  7. package/dist/composer/MentionItemMedia.js.map +1 -1
  8. package/dist/composer/MentionPopover.d.ts +5 -0
  9. package/dist/composer/MentionPopover.d.ts.map +1 -1
  10. package/dist/composer/MentionPopover.js +17 -6
  11. package/dist/composer/MentionPopover.js.map +1 -1
  12. package/dist/composer/PromptComposer.d.ts +11 -2
  13. package/dist/composer/PromptComposer.d.ts.map +1 -1
  14. package/dist/composer/PromptComposer.js +7 -6
  15. package/dist/composer/PromptComposer.js.map +1 -1
  16. package/dist/composer/TiptapComposer.d.ts +26 -2
  17. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  18. package/dist/composer/TiptapComposer.js +262 -27
  19. package/dist/composer/TiptapComposer.js.map +1 -1
  20. package/dist/composer/TiptapComposer.spec.js +21 -1
  21. package/dist/composer/TiptapComposer.spec.js.map +1 -1
  22. package/dist/composer/extensions/MentionReference.d.ts.map +1 -1
  23. package/dist/composer/extensions/MentionReference.js +1 -1
  24. package/dist/composer/extensions/MentionReference.js.map +1 -1
  25. package/dist/composer/index.d.ts +1 -1
  26. package/dist/composer/index.d.ts.map +1 -1
  27. package/dist/composer/index.js +1 -1
  28. package/dist/composer/index.js.map +1 -1
  29. package/dist/composer/types.d.ts +6 -0
  30. package/dist/composer/types.d.ts.map +1 -1
  31. package/dist/editor/useCollabReconcile.concurrent.spec.js +106 -0
  32. package/dist/editor/useCollabReconcile.concurrent.spec.js.map +1 -1
  33. package/dist/editor/useCollabReconcile.d.ts.map +1 -1
  34. package/dist/editor/useCollabReconcile.js +42 -24
  35. package/dist/editor/useCollabReconcile.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/composer/ComposerContextMenu.tsx +15 -3
  38. package/src/composer/MentionItemMedia.tsx +13 -13
  39. package/src/composer/MentionPopover.tsx +52 -10
  40. package/src/composer/PromptComposer.tsx +34 -5
  41. package/src/composer/TiptapComposer.spec.ts +24 -0
  42. package/src/composer/TiptapComposer.tsx +346 -27
  43. package/src/composer/extensions/MentionReference.tsx +3 -0
  44. package/src/composer/index.ts +2 -0
  45. package/src/composer/types.ts +6 -0
  46. package/src/editor/useCollabReconcile.concurrent.spec.ts +124 -0
  47. 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
- () => filterMentionItemsForSlots(mentionItems, slotReferences),
2604
- [mentionItems, slotReferences],
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.state.doc.textContent.trim());
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
- onTextChange(editor.state.doc.textContent.trim());
3113
- }, [editor, onTextChange]);
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.state.doc.textContent.trim();
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 selectMention(
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
- ed.chain().focus().deleteRange({ from: deleteFrom, to: currentPos }).run();
3907
- insertReference(composerReferenceFromMentionItem(item));
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 (!isComposerEditorUsable(editor) || !popover) return;
3953
- const currentPos = editor.state.selection.from;
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
- [editor, popover, closePopover, insertReference],
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?.(editor.state.doc.textContent.trim());
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.state.doc.textContent.trim();
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}
@@ -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";
@@ -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;