@wix/himalaya-cli 0.1036.0 → 0.1038.0

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 (117) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/genie-chat/src/index.d.ts +119 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/portfolio/src/index.d.ts +46 -9
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-genie-chat.mjs +51 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-portfolio.mjs +14 -2
  5. package/dist/cli.mjs +1323 -492
  6. package/dist/preview-assets.generated.bin +0 -0
  7. package/fixtures/wix-canned.json +51 -2
  8. package/guides/headless-loop.md +7 -0
  9. package/guides/himi-test.md +66 -1
  10. package/guides/sdk-reference.json +90 -12
  11. package/guides/trim-a-template.md +5 -0
  12. package/guides/wix-business-context.md +13 -1
  13. package/guides/wix-site-to-himi-app.md +10 -1
  14. package/package.json +2 -2
  15. package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +14 -8
  16. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +12 -1
  17. package/templates/beauty-salon-owner/dev/index.ts +2148 -1262
  18. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +338 -26
  19. package/templates/beauty-salon-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  20. package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  21. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  22. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  23. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  24. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  25. package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +14 -8
  26. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +12 -1
  27. package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
  28. package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
  29. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +12 -1
  30. package/templates/community-club-owner/dev/index.ts +2151 -1265
  31. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +338 -26
  32. package/templates/community-club-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  33. package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  34. package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  35. package/templates/community-club-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  36. package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  37. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +12 -1
  38. package/templates/events-meetups-owner/dev/index.ts +2151 -1265
  39. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +338 -26
  40. package/templates/events-meetups-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  41. package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  42. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  43. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  44. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  45. package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +14 -8
  46. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +12 -1
  47. package/templates/gym-fitness-owner/dev/index.ts +2148 -1262
  48. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +338 -26
  49. package/templates/gym-fitness-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  50. package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  51. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  52. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  53. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  54. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  55. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +12 -1
  56. package/templates/nonprofit-cause-owner/dev/index.ts +2148 -1262
  57. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +338 -26
  58. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  59. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  60. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  61. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  62. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  63. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  64. package/templates/owner-lite/dev/index.ts +2145 -1259
  65. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +338 -26
  66. package/templates/owner-lite/tier5-src/_shared/aria-mentions.ts +202 -0
  67. package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +2 -2
  68. package/templates/owner-lite/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  69. package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  70. package/templates/owner-lite/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  71. package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  72. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +12 -1
  73. package/templates/podcast-creator-owner/dev/index.ts +2151 -1265
  74. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +338 -26
  75. package/templates/podcast-creator-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  76. package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  77. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  78. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  79. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  80. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +12 -1
  81. package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
  82. package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
  83. package/templates/portfolio-creative-owner/dev/index.ts +2148 -1262
  84. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +338 -26
  85. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  86. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  87. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  88. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  89. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  90. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  91. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +12 -1
  92. package/templates/restaurant-owner/dev/index.ts +2148 -1262
  93. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +338 -26
  94. package/templates/restaurant-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  95. package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  96. package/templates/restaurant-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  97. package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  98. package/templates/restaurant-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  99. package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  100. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +12 -1
  101. package/templates/retail-store-owner/dev/index.ts +2151 -1265
  102. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +338 -26
  103. package/templates/retail-store-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  104. package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  105. package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  106. package/templates/retail-store-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  107. package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  108. package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +14 -8
  109. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +12 -1
  110. package/templates/yoga-studio-owner/dev/index.ts +2148 -1262
  111. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +338 -26
  112. package/templates/yoga-studio-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  113. package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  114. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  115. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  116. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  117. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
@@ -37,8 +37,13 @@ import {
37
37
  functionCallsOf,
38
38
  getConversationMessages,
39
39
  getConversations,
40
+ itemsOf,
41
+ listInstalledProviders,
42
+ mentionReference,
40
43
  messageText,
41
44
  messagesOf,
45
+ providersOf,
46
+ queryItems,
42
47
  sendFunctionCallResults,
43
48
  sendMessage,
44
49
  triggerAssistantMessage,
@@ -47,6 +52,8 @@ import {
47
52
  type Attachment,
48
53
  type ConversationMessage,
49
54
  type ConversationSummary,
55
+ type ItemsQuery,
56
+ type MessageReference,
50
57
  type SendMessageResponse,
51
58
  } from "@himalaya/wix-genie-chat";
52
59
  import {
@@ -68,6 +75,8 @@ import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
68
75
  import { NO_WIDGET, activeWidgetOf, asyncAdvanced, type SubtreeSlot, type WidgetOptionRow, type WidgetView }
69
76
  from "./aria-widget-view.js";
70
77
  import { substituteSelection } from "./aria-widget-model.js";
78
+ import { MENTION_PAGE_SIZE, allowlisted, emptyMentionModel, mentionView,
79
+ type MentionItemRow, type MentionLabels, type MentionModel, type MentionProvider, type MentionView } from "./aria-mentions.js";
71
80
  import { isTransportFailure, ownerTransportFailureText } from "./owner-load-error.js";
72
81
  import { LOCAL_ID_PREFIX, createRowMemo, mintLocalId, transcriptKeys, type RowMemo } from "./aria-hot-path.js";
73
82
 
@@ -102,6 +111,11 @@ export interface AriaBubble {
102
111
  * that right-aligns the owner renders two templates and collapses one. */
103
112
  notUser: boolean;
104
113
  notAria: boolean;
114
+ /** What the turn MENTIONED — "@Blue Dress · @Maya Chen" — read off the
115
+ * references stored beside the text. Empty (and `refsHidden`) on every turn
116
+ * that mentioned nothing, which is every assistant turn. */
117
+ refs: string;
118
+ refsHidden: boolean;
105
119
  /** What a screen reader says for the row: the author, then the words. A
106
120
  * screen that draws the speaker only as a bubble and a side of the screen
107
121
  * (`messageStyle: 'userBubble'`) has no visible author line to read, and
@@ -161,6 +175,10 @@ export interface AriaChatState {
161
175
  /** Text of the optimistic user bubble awaiting reconciliation with the server
162
176
  * transcript. Empty when nothing is in flight. */
163
177
  _pending: string;
178
+ /** The optimistic bubble's mention line, so a tick that re-appends the
179
+ * bubble before the server echoes the turn does not drop what it mentioned.
180
+ * Read only while `_pending` is set. */
181
+ _pendingRefs: string;
164
182
  /** The file names the in-flight message was sent with, so the optimistic
165
183
  * bubble keeps its attachment line across every tick until the server's own
166
184
  * copy of the turn replaces it. */
@@ -223,6 +241,10 @@ export interface AriaChatState {
223
241
  * every time the transcript is, which is what makes "one active widget"
224
242
  * true by construction rather than by remembering to clear it. */
225
243
  widget: WidgetView;
244
+ /** The mention picker and the picks waiting above the composer, flattened
245
+ * for binding (`aria-mentions.ts`). Derived from the core's mention model on
246
+ * the way out of EVERY action — see `gated`. */
247
+ mention: MentionView;
226
248
  /** The photos waiting in the composer to go with the next message.
227
249
  *
228
250
  * A PROJECTION, not a field the reducers maintain: the worker holds the chips
@@ -295,6 +317,18 @@ export interface AriaChatLabels {
295
317
  * all — it carries no Wix app binding to sign the chat-service call with.
296
318
  * See `isWixAccountUnreachable`, and note what it is NOT: a signed-out user. */
297
319
  unavailable?: Resolvable<string>;
320
+ /** The mention picker's copy. See `MentionLabels`. */
321
+ mentionTitle?: Resolvable<string>;
322
+ mentionNoProviders?: Resolvable<string>;
323
+ mentionNoItems?: Resolvable<string>;
324
+ mentionFailed?: Resolvable<string>;
325
+ /** Shown when THIS host cannot reach the provider service at all — the
326
+ * service is native-only (see the chat client), so on the web preview the
327
+ * picker says so instead of pretending the site has nothing to mention. */
328
+ mentionUnavailable?: Resolvable<string>;
329
+ /** Shown when one provider's items could not be fetched. Takes the
330
+ * category's label. */
331
+ mentionItemsFailed?: (category: string) => string;
298
332
  /** Attachment copy — see `AriaChatOptions.attachments`. Chip states: */
299
333
  attachUploading?: Resolvable<string>;
300
334
  attachFailed?: Resolvable<string>;
@@ -674,6 +708,20 @@ export function withSpoken(bubbles: AriaBubble[]): AriaBubble[] {
674
708
  : bubbles.map((b) => (typeof b.spoken === "string" ? b : { ...b, spoken: spokenOf(b.who, b.content) }));
675
709
  }
676
710
 
711
+ /** The line a bubble shows for what its turn mentioned: each MENTION
712
+ * reference's label, `@`-prefixed. Only `source: "mention"` counts — the
713
+ * service also stores references of other kinds (a reply-to, page context),
714
+ * and those are not something the owner picked. */
715
+ export function refsLine(refs: MessageReference[] | undefined): string {
716
+ const labels: string[] = [];
717
+ for (const r of refs ?? []) {
718
+ if (r?.source !== "mention") continue;
719
+ const label = typeof r.label === "string" ? r.label.trim() : "";
720
+ if (label) labels.push(`@${label}`);
721
+ }
722
+ return labels.join(" · ");
723
+ }
724
+
677
725
  /** A bubble's words and its attachment line, with its collapse flag.
678
726
  *
679
727
  * A turn that is ONLY attachments carries the line as its content instead of
@@ -688,10 +736,14 @@ function withAttachments(content: string, names: readonly string[]): { content:
688
736
  }
689
737
 
690
738
  /** A greeting or optimistic bubble: no links, all slots defaulted. `attached`
691
- * names the files an optimistic USER bubble is being sent with. */
692
- export function plainBubble(id: string, role: string, who: string, content: string, attached: readonly string[] = []): AriaBubble {
739
+ * names the files an optimistic USER bubble is being sent with, and `refs` is
740
+ * its mention line, already formatted. */
741
+ export function plainBubble(
742
+ id: string, role: string, who: string, content: string, attached: readonly string[] = [], refs = "",
743
+ ): AriaBubble {
693
744
  const a = withAttachments(content, attached);
694
- return { id, role, who, notUser: role !== "USER", notAria: role === "USER", spoken: spokenOf(who, a.content), ...linkFieldsOf("", role), ...a };
745
+ return { id, role, who, notUser: role !== "USER", notAria: role === "USER", spoken: spokenOf(who, a.content),
746
+ refs, refsHidden: !refs, ...linkFieldsOf("", role), ...a };
695
747
  }
696
748
 
697
749
  /** Server transcript → bubbles. USER text is literal; ASSISTANT replies are
@@ -732,11 +784,14 @@ export function toBubbles(
732
784
  // not something the service is known to send, and naming files on it
733
785
  // would claim Aria sent them.
734
786
  const attached = role === "USER" ? attachmentsOf(m).map((a) => a.fileName) : [];
735
- const build = () => bubbleOf(id, role, who, raw, writing, attached);
736
- // Attachments are part of the key: they are not in the text, so a row
737
- // keyed on text alone could serve a bubble naming the wrong files.
787
+ // A USER turn only: an owner mentions things, the assistant does not.
788
+ const refs = role === "USER" ? refsLine(m.references) : "";
789
+ const build = () => bubbleOf(id, role, who, raw, writing, attached, refs);
790
+ // Attachments and mentions are part of the key: neither is in the text, so
791
+ // a row keyed on text alone could serve a bubble naming the wrong files or
792
+ // the wrong picks.
738
793
  const b = memo
739
- ? memo.row(id, raw, `${role}\u0000${who}\u0000${writing ? "w" : ""}\u0000${attached.join("\u0001")}`, build)
794
+ ? memo.row(id, raw, `${role}\u0000${who}\u0000${writing ? "w" : ""}\u0000${attached.join("\u0001")}\u0000${refs}`, build)
740
795
  : build();
741
796
  if (b) out.push(b);
742
797
  }
@@ -761,7 +816,7 @@ export type BubbleMemo = RowMemo<AriaBubble | null>;
761
816
  export const createBubbleMemo = (): BubbleMemo => createRowMemo<AriaBubble | null>();
762
817
 
763
818
  function bubbleOf(
764
- id: string, role: string, who: string, raw: string, writing: boolean, attached: readonly string[] = [],
819
+ id: string, role: string, who: string, raw: string, writing: boolean, attached: readonly string[] = [], refs = "",
765
820
  ): AriaBubble | null {
766
821
  // Only assistant turns carry envelopes; a user's own text is literal, and
767
822
  // scanning it would let an owner who typed `<<…>>` erase their own message.
@@ -786,6 +841,8 @@ function bubbleOf(
786
841
  notUser: role !== "USER",
787
842
  notAria: role === "USER",
788
843
  spoken: spokenOf(who, a.content),
844
+ refs,
845
+ refsHidden: !refs,
789
846
  ...linkFieldsOf(prose, role),
790
847
  ...a,
791
848
  };
@@ -838,14 +895,14 @@ export function argText(arg: AriaActionArg): string {
838
895
  * vanish mid-turn. */
839
896
  export function reconcile(
840
897
  server: AriaBubble[], pending: string, pendingAfter: number, youName = "You", pendingId = "",
841
- pendingAttached: readonly string[] = [],
898
+ pendingAttached: readonly string[] = [], pendingRefs = "",
842
899
  ): { messages: AriaBubble[]; pending: string } {
843
900
  if (!pending) return { messages: server, pending: "" };
844
901
  if (countRole(server, "USER") > pendingAfter) return { messages: server, pending: "" };
845
902
  // `pendingId` is empty only for state persisted before ids were minted; a
846
903
  // fixed fallback still keeps that one row stable across ticks.
847
904
  const id = pendingId || `${LOCAL_ID_PREFIX}pending`;
848
- return { messages: [...server, plainBubble(id, "USER", youName, pending, pendingAttached)], pending };
905
+ return { messages: [...server, plainBubble(id, "USER", youName, pending, pendingAttached, pendingRefs)], pending };
849
906
  }
850
907
 
851
908
  /** How many turns of one role the transcript holds. Counted over BUBBLES rather
@@ -1178,6 +1235,23 @@ export interface AriaChatCore {
1178
1235
  widgetPick(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
1179
1236
  /** Answer one QUESTIONNAIRE question. */
1180
1237
  widgetAnswer(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
1238
+ /** Open the mention picker at the category level, listing the site's
1239
+ * installed providers the first time (per business). `undefined` when the
1240
+ * business switched while the list was being fetched. */
1241
+ mentionOpen(s: AriaChatState): Promise<AriaChatState | undefined>;
1242
+ /** Close it. Picks already made stay above the composer. */
1243
+ mentionClose(s: AriaChatState): AriaChatState;
1244
+ /** From a provider's items back to the categories. */
1245
+ mentionBack(s: AriaChatState): AriaChatState;
1246
+ /** Browse one provider (`arg.id` / `arg.value` is its key). Resolves to
1247
+ * `undefined` when a later picker action superseded it — see `mentionSeq`. */
1248
+ mentionProvider(s: AriaChatState, arg?: AriaActionArg): Promise<AriaChatState | undefined>;
1249
+ /** Search the provider being browsed. Same `undefined` rule. */
1250
+ mentionQuery(s: AriaChatState, arg?: AriaActionArg): Promise<AriaChatState | undefined>;
1251
+ /** Pick one item (`arg.id` is its row id) — a chip above the composer. */
1252
+ mentionPick(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
1253
+ /** Drop one chip (`arg.id` is the chip id). */
1254
+ mentionRemove(s: AriaChatState, arg?: AriaActionArg): AriaChatState;
1181
1255
  /** Pick (or take) a photo and start uploading it. Returns as soon as the chip
1182
1256
  * exists — the upload reports back through `attachmentSettled`. The source is
1183
1257
  * the arg (`"camera"`, or `{ source: "camera" }`); anything else is the
@@ -1255,12 +1329,112 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1255
1329
  const historyKeyFor = (site: string) => scopedKey(o.historyKey, site);
1256
1330
  const localHistory = o.localHistory !== false;
1257
1331
 
1332
+ // -------------------------------------------------------------------------
1333
+ // Mentions
1334
+ // -------------------------------------------------------------------------
1335
+
1336
+ const mentionLabels = (): MentionLabels => ({
1337
+ title: res(o.labels?.mentionTitle ?? "Mention"),
1338
+ noProviders: res(o.labels?.mentionNoProviders ?? "There's nothing on this site to mention yet."),
1339
+ noItems: res(o.labels?.mentionNoItems ?? "Nothing found."),
1340
+ providersFailed: res(o.labels?.mentionFailed ?? "Couldn't load what you can mention."),
1341
+ unavailable: res(o.labels?.mentionUnavailable ?? "Mentions aren't available here."),
1342
+ });
1343
+ const itemsFailedText = (category: string): string =>
1344
+ o.labels?.mentionItemsFailed?.(category) ?? `Couldn't load ${category}.`;
1345
+
1346
+ /** The picker, and the picks waiting to be sent — kept HERE, outside the
1347
+ * reducer state, and painted onto every action's result by `gated`.
1348
+ *
1349
+ * A latch for the reason `draft` is one. Reducers are pure functions of the
1350
+ * state handed to them, and the poll ladder keeps a `tick` in the air for
1351
+ * much of a screen's life: a tick that read its snapshot BEFORE the owner
1352
+ * opened the picker would, on resolving, write the closed picker back —
1353
+ * the panel snapping shut under their finger. Painting the model on the way
1354
+ * out of every action, the tick included, makes the latest pick win no
1355
+ * matter which action carries it.
1356
+ *
1357
+ * What that costs: the model is not persisted, so a worker that is torn
1358
+ * down and re-booted comes back with the picker closed and no chips. A pick
1359
+ * is a draft the owner has not sent, exactly like the composer text. */
1360
+ let mentionModel: MentionModel = emptyMentionModel();
1361
+ let mentionViewNow: MentionView = mentionView(mentionModel, mentionLabels());
1362
+ const setMention = (next: MentionModel): void => {
1363
+ mentionModel = next;
1364
+ mentionViewNow = mentionView(next, mentionLabels());
1365
+ };
1366
+ /** Bumped by every picker transition that makes an in-flight item query
1367
+ * stale: a newer search, another provider, Back, Close, a pick, a new chat,
1368
+ * a business switch. A query compares on resolving and stands down. */
1369
+ let mentionSeq = 0;
1370
+ /** Chip ids. Minted rather than derived from the provider key and item id,
1371
+ * neither of which is guaranteed to be safe inside a component id. */
1372
+ let pickSeq = 0;
1373
+
1374
+ /** A business switch drops the providers AND the picks: a product of the
1375
+ * previous business is not something to mention to this one. */
1376
+ const mentionSite = (): void => {
1377
+ const site = siteOf();
1378
+ if (mentionModel.site === site) return;
1379
+ mentionSeq += 1;
1380
+ setMention({ ...emptyMentionModel(), site });
1381
+ };
1382
+
1383
+ /** Which sentence a failed provider call gets. The service is native-only, so
1384
+ * a host that cannot reach it is told so plainly — never shown an empty
1385
+ * list, which would read as "this site has nothing to mention". */
1386
+ const mentionFailure = (e: unknown, fallback: string): string => {
1387
+ const msg = String((e as Error)?.message ?? e ?? "");
1388
+ return isWixAccountUnreachable(e) || msg.includes("absolute_urls_not_allowed")
1389
+ ? mentionLabels().unavailable
1390
+ : fallback;
1391
+ };
1392
+
1393
+ const mentionArg = (arg?: AriaActionArg): string =>
1394
+ typeof arg === "string" ? arg.trim() : firstString(arg?.id, arg?.optionValue, arg?.value, arg?.text);
1395
+
1396
+ /** One provider page into rows. Row ids are positional — the item id is
1397
+ * kept beside them for the pick — and a picture is only shown from a Wix
1398
+ * media host, the same rule the widget images follow. */
1399
+ const itemRows = (provider: MentionProvider, items: ReturnType<typeof itemsOf>): MentionItemRow[] => {
1400
+ const seen = new Set<string>();
1401
+ const rows: MentionItemRow[] = [];
1402
+ for (const it of items) {
1403
+ if (seen.has(it.id)) continue;
1404
+ seen.add(it.id);
1405
+ const pic = provider.showsImage && isWixMediaHost(hostOf(it.imageUrl)) ? it.imageUrl : "";
1406
+ rows.push({ id: `i${rows.length}`, itemId: it.id, title: it.title, subtitle: it.subtitle,
1407
+ subtitleHidden: !it.subtitle, pictureUrl: pic, pictureHidden: !pic });
1408
+ }
1409
+ return rows;
1410
+ };
1411
+
1412
+ /** Ask one provider for a page. Resolves to `undefined` — leave the state
1413
+ * alone — when a later picker transition superseded it. */
1414
+ const fetchItems = async (s: AriaChatState, provider: MentionProvider, q: string, seq: number): Promise<AriaChatState | undefined> => {
1415
+ const query: ItemsQuery = {
1416
+ ...(q ? { search: q } : {}),
1417
+ ...(provider.paging === "CURSOR" ? { cursorPaging: { limit: MENTION_PAGE_SIZE } }
1418
+ : provider.paging === "OFFSET" ? { paging: { limit: MENTION_PAGE_SIZE, offset: 0 } } : {}),
1419
+ };
1420
+ let items: MentionItemRow[] = [];
1421
+ let failed = "";
1422
+ try {
1423
+ items = itemRows(provider, itemsOf(await queryItems(provider.key, query)));
1424
+ } catch (e) {
1425
+ failed = mentionFailure(e, itemsFailedText(provider.label));
1426
+ }
1427
+ if (seq !== mentionSeq) return undefined;
1428
+ setMention({ ...mentionModel, items, itemsLoading: false, itemsFailed: failed });
1429
+ return committed ?? s;
1430
+ };
1431
+
1258
1432
  const initialState: AriaChatState = {
1259
1433
  status: "loading", statusText: "", error: "", input: "", copyNote: "", replyActionsHidden: true,
1260
- messages: [], prompts: [], _convId: "", _siteId: "", _pending: "", _pendingAttach: [],
1434
+ messages: [], prompts: [], _convId: "", _siteId: "", _pending: "", _pendingRefs: "", _pendingAttach: [],
1261
1435
  _pendingAfter: 0, _pendingId: "", _replyAfter: 0, _srvUsers: 0, _srvAssistants: 0,
1262
1436
  _abortId: "", _turnId: "", stopHidden: true, ok: false,
1263
- widget: NO_WIDGET,
1437
+ widget: NO_WIDGET, mention: mentionViewNow,
1264
1438
  attachments: [], attachmentsHidden: true,
1265
1439
  };
1266
1440
 
@@ -1422,8 +1596,11 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1422
1596
  * A latch rather than state, for the same reason `inFlightSend` is one: it is
1423
1597
  * a transient recovery hint about one failed attempt, not something that
1424
1598
  * should survive a reload or be persisted with the transcript. Keyed by
1425
- * conversation AND content so it can only ever re-trigger the exact turn it
1426
- * was recorded for.
1599
+ * conversation, content AND the references (mentions) the turn was stored
1600
+ * with, so it can only ever re-trigger the exact turn it was recorded for: the
1601
+ * same words retried with other picks are a new turn, and re-triggering the
1602
+ * stored one would answer about the old picks and then clear the new chips as
1603
+ * if they had been sent.
1427
1604
  *
1428
1605
  * It also has to EXPIRE, and cannot do so on its own clock. A trigger whose
1429
1606
  * RESPONSE was lost — a timeout, a reset — failed here while succeeding at the
@@ -1432,7 +1609,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1432
1609
  * owner's next identical question instead, which is quieter and worse. The
1433
1610
  * expiry is therefore a question asked of the SERVER at reuse time — see
1434
1611
  * `turnAnswered` — rather than anything inferred from local counts. */
1435
- let stalledTurn: { convId: string; content: string; userMessageId: string } | null = null;
1612
+ let stalledTurn: { convId: string; content: string; refsKey: string; userMessageId: string } | null = null;
1436
1613
 
1437
1614
  /** A monotonic turn epoch, bumped by every TURN BOUNDARY: a successful `stop`,
1438
1615
  * the start of a `send`, and a `newChat`.
@@ -1792,12 +1969,25 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1792
1969
  const withDraft = (next: AriaChatState, since: number): AriaChatState =>
1793
1970
  draftSeq === since || draft === next.input ? next : { ...next, input: draft };
1794
1971
 
1795
- async function send(s: AriaChatState, content: string, attached: readonly AriaAttachmentChip[] = []): Promise<AriaChatState> {
1972
+ async function send(
1973
+ s: AriaChatState, content: string, attached: readonly AriaAttachmentChip[] = [], withPicks = true,
1974
+ ): Promise<AriaChatState> {
1796
1975
  // A new turn is a turn boundary: any tick still parked on the PREVIOUS
1797
1976
  // turn's transcript read must stand down rather than write its snapshot over
1798
1977
  // this one. Bump before the first await so an in-flight tick is already stale
1799
1978
  // by the time it resolves.
1800
1979
  turnSeq += 1;
1980
+ // The picks above the composer ride WITH this turn, as references beside
1981
+ // the text — never as tokens inside it. Read once, here: a chip the owner
1982
+ // adds while this send is in the air belongs to the NEXT turn, and only the
1983
+ // ones actually sent are cleared below.
1984
+ mentionSite();
1985
+ const sentPicks = withPicks ? mentionModel.picks : [];
1986
+ const references = sentPicks.map((p) => mentionReference({ id: p.itemId, title: p.label }, p.category));
1987
+ // What this turn's references ARE, for `stalledTurn`'s identity. `data` names
1988
+ // the item and its category; the order is the owner's, and so is the turn's.
1989
+ const refsKey = references.map((r) => r.data ?? "").join("\u0000");
1990
+ const refs = refsLine(references);
1801
1991
  // Only chips whose bytes have LANDED travel; `submit` refuses to get here
1802
1992
  // with any other kind, and this filter is the second lock on that door.
1803
1993
  const riding = attached.filter((c) => c.state === "ready" && !!c.ref);
@@ -1824,13 +2014,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1824
2014
  localId = minted.id;
1825
2015
  }
1826
2016
  const optimistic: AriaBubble[] = [
1827
- ...s.messages.filter((b) => b.id !== localId), plainBubble(localId, "USER", you(), content, attachedNames),
2017
+ ...s.messages.filter((b) => b.id !== localId), plainBubble(localId, "USER", you(), content, attachedNames, refs),
1828
2018
  ];
1829
2019
  const base: AriaChatState = {
1830
2020
  // `copyNote: ""` — a Copy confirmation belongs to the moment, and a new
1831
2021
  // turn ends it. Left in place it would sit under the next reply saying
1832
2022
  // "Copied" about text the owner copied several turns ago.
1833
- ...s, prompts: [], input: "", error: "", copyNote: "",
2023
+ ...s, prompts: [], input: "", error: "", copyNote: "", _pendingRefs: refs,
1834
2024
  // The PREVIOUS turn's handles must not survive into this one: aborting by
1835
2025
  // a stale id would abandon a reply that has already been read, and waiting
1836
2026
  // on a stale turn id would look for an answer that already exists. Both
@@ -1891,7 +2081,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1891
2081
  // send is about (`supersedes`)? And if so, is that turn still worth
1892
2082
  // re-triggering (`stalled`)?
1893
2083
  latch = stalledTurn;
1894
- supersedes = !!latch && latch.convId === convId && latch.content === content;
2084
+ supersedes = !!latch && latch.convId === convId && latch.content === content && latch.refsKey === refsKey;
1895
2085
  // …and only while it is still UNANSWERED, which is ASKED rather than
1896
2086
  // inferred. A trigger whose response was lost failed here while succeeding
1897
2087
  // at the service, so the answer arrives anyway and the id now names a
@@ -1937,9 +2127,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1937
2127
  } else {
1938
2128
  answer = await sendMessage(convId, content, {
1939
2129
  origin,
1940
- // Omitted rather than `[]` when there are none, so a plain text turn
1941
- // is byte-for-byte what it was before attachments existed.
2130
+ // Each omitted rather than `[]` when there are none, so a plain text
2131
+ // turn is byte-for-byte what it was before attachments and mentions.
1942
2132
  ...(attachments.length ? { attachments } : {}),
2133
+ ...(references.length ? { messageReferences: references } : {}),
1943
2134
  });
1944
2135
  abortId = userMessageIdOf(answer);
1945
2136
  }
@@ -1988,6 +2179,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1988
2179
  // away a turn this send never touched.
1989
2180
  if (supersedes && stalledTurn === latch) stalledTurn = null;
1990
2181
  retriedId = "";
2182
+ // Delivered, so its chips are spent — the stored turn carries them now,
2183
+ // and on a re-trigger it already did. Only THESE chips: one added during
2184
+ // the send is waiting for the next turn. A FAILED send keeps them all,
2185
+ // so the retry sends what the owner picked.
2186
+ if (sentPicks.length) {
2187
+ setMention({ ...mentionModel, picks: mentionModel.picks.filter((p) => !sentPicks.includes(p)) });
2188
+ }
1991
2189
  // A conversation minted just now starts empty, so the marks taken above
1992
2190
  // from the PREVIOUS conversation's counts would be ahead of it and its
1993
2191
  // first reply would never read as new — the same stranded "typing…" this
@@ -2043,7 +2241,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2043
2241
  const storedId = (e as { storedUserMessageId?: unknown })?.storedUserMessageId;
2044
2242
  const keptId = typeof storedId === "string" && storedId ? storedId : retriedId;
2045
2243
  stalledTurn = convId && keptId
2046
- ? { convId, content, userMessageId: keptId }
2244
+ ? { convId, content, refsKey, userMessageId: keptId }
2047
2245
  // Nothing stored by this attempt. Drop the latch only if it is the one
2048
2246
  // this send was working on and is still the one we captured; otherwise it
2049
2247
  // belongs to another turn and must survive.
@@ -2089,7 +2287,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2089
2287
  * action rather than maintained inside each of them: `stopHidden` is a pure
2090
2288
  * function of two other fields, and eight reducers each remembering to set it
2091
2289
  * is eight chances for the control to be shown when there is nothing to stop
2092
- * — or, worse, hidden while a reply the owner wants to abandon runs on. */
2290
+ * — or, worse, hidden while a reply the owner wants to abandon runs on.
2291
+ *
2292
+ * It also paints the mention picker (see `mentionModel`), and for the same
2293
+ * reason: every action's result passes through here, so this is the one
2294
+ * place a stale snapshot cannot sneak an old picker back onto the screen.
2295
+ * Both edits are made only when something changed, so a reducer that
2296
+ * returns the state untouched still returns the SAME object. */
2093
2297
  let committed: AriaChatState | null = null;
2094
2298
  let commits = 0;
2095
2299
  /** A result as the runtime stores it: the one-shot directives it strips on
@@ -2102,6 +2306,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2102
2306
  return rest as AriaChatState;
2103
2307
  };
2104
2308
  const gated = (s: AriaChatState): AriaChatState => {
2309
+ mentionSite();
2105
2310
  const hidden = !(turnInFlight(s) && !!s._abortId);
2106
2311
  // The reply actions follow the same rule for the same reason: a derived
2107
2312
  // flag, recomputed on every action's way out, so no reducer can forget it.
@@ -2118,9 +2323,10 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2118
2323
  seedChips(actioned);
2119
2324
  for (const id of [...chipBytes.keys()]) if (!chips.some((c) => c.id === id)) chipBytes.delete(id);
2120
2325
  const attachmentsHidden = chips.length === 0;
2121
- const out = actioned.attachments === chips && actioned.attachmentsHidden === attachmentsHidden
2326
+ let out = actioned.attachments === chips && actioned.attachmentsHidden === attachmentsHidden
2122
2327
  ? actioned
2123
2328
  : { ...actioned, attachments: chips, attachmentsHidden };
2329
+ if (out.mention !== mentionViewNow) out = { ...out, mention: mentionViewNow };
2124
2330
  // Every state the runtime commits passes through here (an `undefined`
2125
2331
  // result commits nothing and skips it), so this is the newest committed
2126
2332
  // state — what `copyMessage` puts its note on when other actions landed
@@ -2387,6 +2593,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2387
2593
  // A fresh conversation is a turn boundary too: a tick parked on the OLD
2388
2594
  // conversation's transcript must not reconcile it over the new greeting.
2389
2595
  turnSeq += 1;
2596
+ // A fresh chat drops its drafts, the picks included — the same way it
2597
+ // empties the composer. The provider list is kept: it belongs to the
2598
+ // business, not to the conversation.
2599
+ mentionSeq += 1;
2600
+ setMention({ ...mentionModel, open: false, provider: null, query: "", items: [], itemsLoading: false,
2601
+ itemsFailed: "", picks: [] });
2390
2602
  // ➕ discards the draft (`input: ""` below), and the photos are part of the
2391
2603
  // draft. An upload still running settles into nothing: its chip is gone.
2392
2604
  chipsSeeded = true;
@@ -2459,7 +2671,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2459
2671
  // reply go unnoticed and pinned the ladder fast forever. Safe to return
2460
2672
  // `s` here: the guard above proved this tick is still current.
2461
2673
  if (!raw.length) return s;
2462
- const { messages, pending } = reconcile(server, s._pending, s._pendingAfter, you(), s._pendingId, s._pendingAttach ?? []);
2674
+ const { messages, pending } = reconcile(server, s._pending, s._pendingAfter, you(), s._pendingId, s._pendingAttach ?? [], s._pendingRefs);
2463
2675
  // …but still never PAINT nothing over something. `messages` is only
2464
2676
  // adopted when it has content: a transcript that renders empty leaves
2465
2677
  // the screen as it was (the greeting, or the last good turn) while the
@@ -2563,6 +2775,97 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2563
2775
  };
2564
2776
  },
2565
2777
 
2778
+ // --- Mentions -----------------------------------------------------------
2779
+ //
2780
+ // Every one of these edits the mention MODEL and returns a state for
2781
+ // `gated` to paint it onto — see `mentionModel` for why the model lives
2782
+ // outside the reducer state.
2783
+
2784
+ mentionOpen: async (s) => {
2785
+ mentionSite();
2786
+ const m = mentionModel;
2787
+ mentionSeq += 1;
2788
+ const atCategories = { ...m, open: true, provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "" };
2789
+ // Listed once per business. A failed list is asked again: the failure
2790
+ // may have been the network, and the next open is the retry.
2791
+ if (m.providers !== null) { setMention(atCategories); return s; }
2792
+ if (m.providersLoading) { setMention(atCategories); return s; }
2793
+ const site = m.site;
2794
+ setMention({ ...atCategories, providersLoading: true, providersFailed: "" });
2795
+ let providers: MentionProvider[] | null = null;
2796
+ let failed = "";
2797
+ try {
2798
+ providers = allowlisted(providersOf(await listInstalledProviders()));
2799
+ } catch (e) {
2800
+ failed = mentionFailure(e, mentionLabels().providersFailed);
2801
+ }
2802
+ // A switch during the request already reset the model for the new
2803
+ // business; this answer belongs to the old one.
2804
+ if (mentionModel.site !== site) return undefined;
2805
+ setMention({ ...mentionModel, providers, providersLoading: false, providersFailed: failed });
2806
+ return committed ?? s;
2807
+ },
2808
+
2809
+ mentionClose: (s) => {
2810
+ if (!mentionModel.open) return s;
2811
+ mentionSeq += 1;
2812
+ setMention({ ...mentionModel, open: false, provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "" });
2813
+ return s;
2814
+ },
2815
+
2816
+ mentionBack: (s) => {
2817
+ if (!mentionModel.provider) return s;
2818
+ mentionSeq += 1;
2819
+ setMention({ ...mentionModel, provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "" });
2820
+ return s;
2821
+ },
2822
+
2823
+ mentionProvider: async (s, arg) => {
2824
+ mentionSite();
2825
+ const key = mentionArg(arg);
2826
+ const provider = mentionModel.open ? (mentionModel.providers ?? []).find((p) => p.key === key) : undefined;
2827
+ if (!provider) return s;
2828
+ const seq = ++mentionSeq;
2829
+ setMention({ ...mentionModel, provider, query: "", items: [], itemsLoading: true, itemsFailed: "" });
2830
+ return fetchItems(s, provider, "", seq);
2831
+ },
2832
+
2833
+ mentionQuery: async (s, arg) => {
2834
+ const provider = mentionModel.provider;
2835
+ if (!mentionModel.open || !provider) return s;
2836
+ // Trimmed: a search, where surrounding space is noise. Unlike the
2837
+ // composer, nothing is lost — the field keeps what was typed.
2838
+ const q = typeof arg === "string" ? arg.trim() : firstString(arg?.value, arg?.text);
2839
+ // The same search again is only worth re-asking when it failed.
2840
+ if (q === mentionModel.query && !mentionModel.itemsFailed && !mentionModel.itemsLoading) return s;
2841
+ const seq = ++mentionSeq;
2842
+ setMention({ ...mentionModel, query: q, itemsLoading: true, itemsFailed: "" });
2843
+ return fetchItems(s, provider, q, seq);
2844
+ },
2845
+
2846
+ mentionPick: (s, arg) => {
2847
+ const provider = mentionModel.provider;
2848
+ const itemId = mentionArg(arg);
2849
+ const item = provider ? mentionModel.items.find((i) => i.itemId === itemId) : undefined;
2850
+ if (!provider || !item) return s;
2851
+ const already = mentionModel.picks.some((p) => p.providerKey === provider.key && p.itemId === item.itemId);
2852
+ const picks = already ? mentionModel.picks : [...mentionModel.picks,
2853
+ { id: `m${++pickSeq}`, providerKey: provider.key, itemId: item.itemId, label: item.title, category: provider.label }];
2854
+ // A pick closes the picker: the owner came to point at one thing, and
2855
+ // the chip above the composer is where they now look.
2856
+ mentionSeq += 1;
2857
+ setMention({ ...mentionModel, picks, open: false, provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "" });
2858
+ return s;
2859
+ },
2860
+
2861
+ mentionRemove: (s, arg) => {
2862
+ const id = mentionArg(arg);
2863
+ const picks = mentionModel.picks.filter((p) => p.id !== id);
2864
+ if (picks.length === mentionModel.picks.length) return s;
2865
+ setMention({ ...mentionModel, picks });
2866
+ return s;
2867
+ },
2868
+
2566
2869
  widgetToggle: (s, arg) => {
2567
2870
  const value = optionValue(arg);
2568
2871
  if (!value) return s;
@@ -2900,7 +3203,9 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2900
3203
  try {
2901
3204
  // `send` directly: it is the one send path, and `submit` with no
2902
3205
  // argument would read the composer instead of sending the retry.
2903
- return await send(s, REGENERATE_TEXT);
3206
+ // Without the picks: they are a draft for the owner's NEXT message,
3207
+ // not part of asking for this answer again.
3208
+ return await send(s, REGENERATE_TEXT, [], false);
2904
3209
  } finally {
2905
3210
  regenerating = false;
2906
3211
  }
@@ -2930,6 +3235,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2930
3235
  widgetRespond: (s, arg) => impl.widgetRespond(s, arg).then(gated),
2931
3236
  widgetPrimary: (s) => impl.widgetPrimary(s).then(gated),
2932
3237
  widgetSecondary: (s) => impl.widgetSecondary(s).then(gated),
3238
+ mentionOpen: (s) => impl.mentionOpen(s).then((r) => (r ? gated(r) : r)),
3239
+ mentionClose: (s) => gated(impl.mentionClose(s)),
3240
+ mentionBack: (s) => gated(impl.mentionBack(s)),
3241
+ mentionProvider: (s, arg) => impl.mentionProvider(s, arg).then((r) => (r ? gated(r) : r)),
3242
+ mentionQuery: (s, arg) => impl.mentionQuery(s, arg).then((r) => (r ? gated(r) : r)),
3243
+ mentionPick: (s, arg) => gated(impl.mentionPick(s, arg)),
3244
+ mentionRemove: (s, arg) => gated(impl.mentionRemove(s, arg)),
2933
3245
  attach: (s, arg) => impl.attach(s, arg).then(gated),
2934
3246
  attachmentSettled: (s, arg) => gated(impl.attachmentSettled(s, arg)),
2935
3247
  removeAttachment: (s, arg) => gated(impl.removeAttachment(s, arg)),