@wix/himalaya-cli 0.1317.0 → 0.1319.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 (80) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +2 -1
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/theme/src/index.d.ts +21 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview-page/src/capabilities.generated.d.ts +2 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/brand-color-math.d.ts +98 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +1 -1
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +9 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -0
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/brand-color-math.mjs +183 -0
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -0
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -0
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +1 -0
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -0
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -0
  17. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/theme.mjs +189 -0
  18. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +8 -0
  19. package/dist/cli.mjs +475 -372
  20. package/dist/preview-assets.generated.bin +0 -0
  21. package/guides/app-config.md +1 -1
  22. package/guides/importable-sdks.md +1 -0
  23. package/guides/sdk-reference.json +39 -2
  24. package/guides/web-page-sdk-reference.md +1 -0
  25. package/package.json +2 -2
  26. package/templates/beauty-salon-owner/config.ts +1 -1
  27. package/templates/beauty-salon-owner/dev/index.ts +125 -24
  28. package/templates/beauty-salon-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  29. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +59 -31
  30. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  31. package/templates/community-club-owner/config.ts +1 -1
  32. package/templates/community-club-owner/dev/index.ts +125 -24
  33. package/templates/community-club-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  34. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +59 -31
  35. package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  36. package/templates/events-meetups-owner/config.ts +1 -1
  37. package/templates/events-meetups-owner/dev/index.ts +125 -24
  38. package/templates/events-meetups-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  39. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +59 -31
  40. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  41. package/templates/gym-fitness-owner/config.ts +1 -1
  42. package/templates/gym-fitness-owner/dev/index.ts +125 -24
  43. package/templates/gym-fitness-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  44. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +59 -31
  45. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  46. package/templates/nonprofit-cause-owner/config.ts +1 -1
  47. package/templates/nonprofit-cause-owner/dev/index.ts +125 -24
  48. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  49. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +59 -31
  50. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  51. package/templates/owner-lite/config.ts +1 -1
  52. package/templates/owner-lite/dev/index.ts +125 -24
  53. package/templates/owner-lite/tier5-src/_shared/aria-attachments.ts +41 -2
  54. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +59 -31
  55. package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  56. package/templates/podcast-creator-owner/config.ts +1 -1
  57. package/templates/podcast-creator-owner/dev/index.ts +125 -24
  58. package/templates/podcast-creator-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  59. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +59 -31
  60. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  61. package/templates/portfolio-creative-owner/config.ts +1 -1
  62. package/templates/portfolio-creative-owner/dev/index.ts +125 -24
  63. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  64. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +59 -31
  65. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  66. package/templates/restaurant-owner/config.ts +1 -1
  67. package/templates/restaurant-owner/dev/index.ts +125 -24
  68. package/templates/restaurant-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  69. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +59 -31
  70. package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  71. package/templates/retail-store-owner/config.ts +1 -1
  72. package/templates/retail-store-owner/dev/index.ts +125 -24
  73. package/templates/retail-store-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  74. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +59 -31
  75. package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  76. package/templates/yoga-studio-owner/config.ts +1 -1
  77. package/templates/yoga-studio-owner/dev/index.ts +125 -24
  78. package/templates/yoga-studio-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  79. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +59 -31
  80. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
@@ -61,10 +61,11 @@ import {
61
61
  import {
62
62
  ARIA_MAX_ATTACHMENTS,
63
63
  ARIA_MAX_IMAGE_BYTES,
64
+ ARIA_MAX_DOCUMENT_BYTES,
64
65
  attachmentLine,
65
66
  chipIn,
66
67
  imageFileName,
67
- normalizeMime,
68
+ attachmentMime,
68
69
  parseDataUri,
69
70
  refusalFor,
70
71
  sendBlocker,
@@ -480,6 +481,9 @@ export interface AriaChatLabels {
480
481
  /** Send refused while a photo is still uploading, or failed to. */
481
482
  attachWaitUploading?: Resolvable<string>;
482
483
  attachWaitFailed?: Resolvable<string>;
484
+ /** Send refused by a failed file that cannot be retried (a cold-start chip
485
+ * whose source did not survive): Remove is the only control it offers. */
486
+ attachWaitRemove?: Resolvable<string>;
483
487
  /** Send refused with photos and no words. The service requires message text
484
488
  * on every turn, so a photo alone is not a sendable message. */
485
489
  attachNeedsText?: Resolvable<string>;
@@ -535,7 +539,7 @@ export interface AriaDictationOptions {
535
539
  export interface AriaAttachmentPorts {
536
540
  /** Let the owner pick (or take) one photo. Resolves the image as a `data:`
537
541
  * URI, or null when they cancelled. */
538
- pick(source: "library" | "camera"): Promise<{ dataUri: string; name?: string; mimeType?: string } | null>;
542
+ pick(source: "library" | "camera" | "document"): Promise<{ dataUri: string; name?: string; mimeType?: string } | null>;
539
543
  /** PUT `base64` to a pre-signed url with `contentType`, carrying NO user
540
544
  * credentials — the url authorizes itself. */
541
545
  put(url: string, base64: string, contentType: string): Promise<{ ok: boolean; status: number }>;
@@ -1652,23 +1656,28 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1652
1656
  const attachUploadingText = () => res(o.labels?.attachUploading ?? "Uploading…");
1653
1657
  const attachFailedText = () => res(o.labels?.attachFailed ?? "Couldn't upload");
1654
1658
  const chipLabels = () => ({ uploading: attachUploadingText(), failed: attachFailedText() });
1655
- const refusalText = (why: "type" | "size" | "count"): string =>
1659
+ const refusalText = (why: "type" | "size" | "count", maxBytes = ARIA_MAX_IMAGE_BYTES): string =>
1656
1660
  why === "count"
1657
- ? o.labels?.attachTooMany?.(ARIA_MAX_ATTACHMENTS) ?? `You can attach up to ${ARIA_MAX_ATTACHMENTS} photos.`
1661
+ ? o.labels?.attachTooMany?.(ARIA_MAX_ATTACHMENTS) ?? `You can attach up to ${ARIA_MAX_ATTACHMENTS} files.`
1658
1662
  : why === "size"
1659
- ? o.labels?.attachTooLarge?.(ARIA_MAX_IMAGE_BYTES / (1024 * 1024))
1660
- ?? `That photo is too large to attach (up to ${ARIA_MAX_IMAGE_BYTES / (1024 * 1024)} MB).`
1663
+ ? o.labels?.attachTooLarge?.(maxBytes / (1024 * 1024))
1664
+ ?? `That file is too large to attach (up to ${maxBytes / (1024 * 1024)} MB).`
1661
1665
  : res(o.labels?.attachUnsupported ?? "That file type can't be attached.");
1662
- const waitUploadingText = () => res(o.labels?.attachWaitUploading ?? "Wait for your photo to finish uploading.");
1663
- const waitFailedText = () => res(o.labels?.attachWaitFailed ?? "Retry or remove the photo that didn't upload.");
1664
- const needsTextText = () => res(o.labels?.attachNeedsText ?? "Add a message to send with your photo.");
1666
+ const waitUploadingText = () => res(o.labels?.attachWaitUploading ?? "Wait for your file to finish uploading.");
1667
+ const waitFailedText = () => res(o.labels?.attachWaitFailed ?? "Retry or remove the file that didn't upload.");
1668
+ const waitRemoveText = () => res(o.labels?.attachWaitRemove ?? "Remove the file that didn't upload, then add it again.");
1669
+ /** The failed-file notice must name only a control the chips offer: "Retry"
1670
+ * while any failed chip can still retry, otherwise "Remove". */
1671
+ const failedNotice = (): string =>
1672
+ chips.some((c) => c.state === "failed" && !c.retryHidden) ? waitFailedText() : waitRemoveText();
1673
+ const needsTextText = () => res(o.labels?.attachNeedsText ?? "Add a message to send with your file.");
1665
1674
  /** Clear the status line if what it says is an attachment notice, and only
1666
1675
  * then. Every notice is advice about the composer's photos, so once the owner
1667
1676
  * acts on them it is stale — but the same line also carries "Aria is
1668
1677
  * typing…", which a photo edit must never erase. */
1669
1678
  const withoutNotice = <T extends { statusText: string }>(s: T): T => {
1670
- const notices = [waitUploadingText(), waitFailedText(), needsTextText(),
1671
- refusalText("type"), refusalText("size"), refusalText("count")];
1679
+ const notices = [waitUploadingText(), waitFailedText(), waitRemoveText(), needsTextText(),
1680
+ refusalText("type"), refusalText("size"), refusalText("size", ARIA_MAX_DOCUMENT_BYTES), refusalText("count")];
1672
1681
  return s.statusText && notices.includes(s.statusText) ? { ...s, statusText: "" } : s;
1673
1682
  };
1674
1683
  const toolFailedText = () => res(o.labels?.toolFailed ?? `${assistant()} couldn't finish that. Try again.`);
@@ -2023,16 +2032,20 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2023
2032
  chipsSeeded = true;
2024
2033
  const persisted = Array.isArray(s.attachments) ? s.attachments : [];
2025
2034
  // An upload does not survive the process that ran it, so a chip persisted
2026
- // mid-upload would say "Uploading…" forever. It still holds its bytes, so it
2027
- // comes back as FAILED — with the retry control — rather than being dropped.
2035
+ // mid-upload would say "Uploading…" forever. Restore it as failed; only a
2036
+ // chip with retained inline bytes can offer Retry until the file-backed follow-up.
2028
2037
  chips = persisted
2029
2038
  .filter((c) => c && typeof c.id === "string")
2030
- .map((c) => (c.state === "uploading" ? chipIn(c, "failed", chipLabels()) : c));
2039
+ .map((c) => {
2040
+ const chip = chipIn(c, c.state === "uploading" ? "failed" : c.state, chipLabels(), c.ref);
2041
+ // Inline bytes were worker-owned. A cold-start document or large image
2042
+ // has no source left: keep Remove available, never offer a dead Retry.
2043
+ return chip.state === "failed" && !chip.thumbUrl ? { ...chip, retryHidden: true } : chip;
2044
+ });
2031
2045
  for (const c of chips) {
2032
2046
  const n = Number(/^att-(\d+)$/.exec(c.id)?.[1] ?? 0);
2033
2047
  if (n > chipSeq) chipSeq = n;
2034
- // A rehydrated chip's bytes are whatever it kept inline. A large photo kept
2035
- // none, so its retry fails honestly rather than sending nothing.
2048
+ // A rehydrated chip can retry only the source it kept inline.
2036
2049
  if (c.thumbUrl && !chipBytes.has(c.id)) chipBytes.set(c.id, c.thumbUrl);
2037
2050
  }
2038
2051
  };
@@ -2044,21 +2057,21 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2044
2057
  if (!chips.length) return null;
2045
2058
  const blocker = sendBlocker(chips);
2046
2059
  if (blocker === "uploading") return { ...s, statusText: waitUploadingText(), error: "" };
2047
- if (blocker === "failed") return { ...s, statusText: waitFailedText(), error: "" };
2060
+ if (blocker === "failed") return { ...s, statusText: failedNotice(), error: "" };
2048
2061
  if (!content) return { ...s, statusText: needsTextText(), error: "" };
2049
2062
  return null;
2050
2063
  };
2051
2064
 
2052
2065
  /** Keep a "wait for your photo" notice true to the chips as they are NOW. An
2053
- * upload that FAILED while Send was held on "Wait for your photo to finish
2066
+ * upload that FAILED while Send was held on "Wait for your file to finish
2054
2067
  * uploading" must say to retry or remove it instead, and one that landed,
2055
2068
  * with nothing else pending, clears it. Any other status line (the typing
2056
2069
  * line above all) is left alone. */
2057
2070
  const refreshWaitNotice = (s: AriaChatState): AriaChatState => {
2058
- const waiting = [waitUploadingText(), waitFailedText()];
2071
+ const waiting = [waitUploadingText(), waitFailedText(), waitRemoveText()];
2059
2072
  if (!s.statusText || !waiting.includes(s.statusText)) return s;
2060
2073
  const blocker = sendBlocker(chips);
2061
- const next = blocker === "uploading" ? waitUploadingText() : blocker === "failed" ? waitFailedText() : "";
2074
+ const next = blocker === "uploading" ? waitUploadingText() : blocker === "failed" ? failedNotice() : "";
2062
2075
  return next === s.statusText ? s : { ...s, statusText: next };
2063
2076
  };
2064
2077
 
@@ -3794,8 +3807,8 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3794
3807
  const atCategories = { ...m, open: true, provider: null, query: "", items: [], itemsLoading: false, itemsFailed: "" };
3795
3808
  // Listed once per business. A failed list is asked again: the failure
3796
3809
  // may have been the network, and the next open is the retry.
3797
- if (m.providers !== null) { setMention(atCategories); return s; }
3798
- if (m.providersLoading) { setMention(atCategories); return s; }
3810
+ if (m.providers !== null) { setMention(atCategories); return committed ?? s; }
3811
+ if (m.providersLoading) { setMention(atCategories); return committed ?? s; }
3799
3812
  const site = m.site;
3800
3813
  setMention({ ...atCategories, providersLoading: true, providersFailed: "" });
3801
3814
  let providers: MentionProvider[] | null = null;
@@ -4112,7 +4125,8 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
4112
4125
  // picker whose result will be thrown away wastes the owner's time.
4113
4126
  if (chips.length >= ARIA_MAX_ATTACHMENTS) return { ...s, statusText: refusalText("count"), error: "" };
4114
4127
  const raw = typeof arg === "string" ? arg : firstString(arg?.source, arg?.value);
4115
- const source = raw.trim().toLowerCase() === "camera" ? "camera" : "library";
4128
+ const source = raw.trim().toLowerCase() === "document" ? "document"
4129
+ : raw.trim().toLowerCase() === "camera" ? "camera" : "library";
4116
4130
  // The picker is open for as long as the owner takes, and a poll, a reply or
4117
4131
  // another action can commit a NEWER state meanwhile. Returning `{ ...s }`
4118
4132
  // afterwards would put this pre-picker snapshot back over it: an older
@@ -4124,17 +4138,18 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
4124
4138
  let picked: Awaited<ReturnType<AriaAttachmentPorts["pick"]>>;
4125
4139
  try {
4126
4140
  picked = await ports.pick(source);
4127
- } catch {
4141
+ } catch (error) {
4142
+ if (/too large|size limit/i.test(String(error))) return { ...latest(), statusText: refusalText("size", source === "document" ? ARIA_MAX_DOCUMENT_BYTES : ARIA_MAX_IMAGE_BYTES), error: "" };
4128
4143
  // A picker that could not open is not the owner's fault and there is no
4129
4144
  // file to talk about; say nothing rather than something misleading.
4130
- return latest();
4145
+ return withoutNotice(latest());
4131
4146
  }
4132
4147
  if (!picked || !picked.dataUri) return latest(); // cancelled
4133
4148
  const parsed = parseDataUri(picked.dataUri);
4134
- const mimeType = normalizeMime(picked.mimeType || parsed?.mimeType || "");
4149
+ const mimeType = attachmentMime(picked.mimeType || parsed?.mimeType || "", picked.name);
4135
4150
  // Re-checked after the await: other picks may have landed meanwhile.
4136
4151
  const refused = refusalFor(parsed, mimeType, chips.length);
4137
- if (refused) return { ...latest(), statusText: refusalText(refused), error: "" };
4152
+ if (refused) return { ...latest(), statusText: refusalText(refused, mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES), error: "" };
4138
4153
  chipSeq += 1;
4139
4154
  const id = `att-${chipSeq}`;
4140
4155
  chipBytes.set(id, picked.dataUri);
@@ -4143,7 +4158,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
4143
4158
  name: imageFileName(mimeType, chipSeq, picked.name),
4144
4159
  mimeType,
4145
4160
  // Only a small image rides the state as its own thumbnail; see `chipBytes`.
4146
- thumbUrl: picked.dataUri.length <= THUMB_INLINE_MAX ? picked.dataUri : "",
4161
+ thumbUrl: mimeType.startsWith("image/") && picked.dataUri.length <= THUMB_INLINE_MAX ? picked.dataUri : "",
4147
4162
  }, "uploading", chipLabels());
4148
4163
  chips = [...chips, chip];
4149
4164
  startUpload(chip);
@@ -4202,7 +4217,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
4202
4217
  retryAttachment: (s, arg) => {
4203
4218
  const id = chipIdOf(arg);
4204
4219
  const current = chips.find((c) => c.id === id);
4205
- if (!current || current.state !== "failed") return s;
4220
+ if (!current || current.state !== "failed" || (!chipBytes.get(id) && !current.thumbUrl)) return s;
4206
4221
  // A NEW id for the new attempt, in the same place in the strip. Keeping the
4207
4222
  // old one would let the first attempt's outcome, if it is still in the air,
4208
4223
  // settle this one.
@@ -4421,8 +4436,21 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
4421
4436
  await probeDictation();
4422
4437
  return gated(r.listening && !dictationLive ? { ...r, listening: false } : r);
4423
4438
  },
4424
- submit: (s, arg) => impl.submit(s, arg).then(gated),
4425
- submitPrompt: (s, arg) => impl.submitPrompt(s, arg).then(gated),
4439
+ submit: async (s, arg) => {
4440
+ const revision = draftSeq;
4441
+ const next = await impl.submit(s, arg);
4442
+ // Refusals preserve a draft; an edit while the async reducer resolved
4443
+ // belongs to the owner, including consuming a selected mention trigger.
4444
+ // Preserve the send result and its pending turn/poll; only rebase the draft.
4445
+ return gated(revision !== draftSeq && next.input === s.input && committed
4446
+ ? { ...next, input: committed.input } : next);
4447
+ },
4448
+ submitPrompt: async (s, arg) => {
4449
+ const revision = draftSeq;
4450
+ const next = await impl.submitPrompt(s, arg);
4451
+ return gated(revision !== draftSeq && next.input === s.input && committed
4452
+ ? { ...next, input: committed.input } : next);
4453
+ },
4426
4454
  newChat: (s) => impl.newChat(s).then(gated),
4427
4455
  typing: (s, arg) => gated(impl.typing(s, arg)),
4428
4456
  tick: (s) => impl.tick(s).then((r) => (r ? gated(r) : r)),
@@ -28,7 +28,7 @@ import { defineService } from "@himalaya/state";
28
28
  import { open } from "@himalaya/open-url";
29
29
  import { writeText } from "@himalaya/clipboard";
30
30
  import { availability, recognizeOnce, stop as stopSpeech } from "@himalaya/speech";
31
- import { capturePhoto } from "@himalaya/camera";
31
+ import { capturePhoto, pickDocument } from "@himalaya/camera";
32
32
  import { put } from "@himalaya/presigned-upload";
33
33
  import { alert } from "@himalaya/ui";
34
34
  import { auth } from "@himalaya/wix-platform";
@@ -250,12 +250,13 @@ const core = createAriaChat({
250
250
  widgetItemsUnavailable: () => copyParam("ownerAria.widget.itemsUnavailable", "Items aren't available here."),
251
251
  attachUploading: () => copyParam("ownerAria.attach.uploading", "Uploading…"),
252
252
  attachFailed: () => copyParam("ownerAria.attach.failed", "Couldn't upload"),
253
- attachTooMany: (max) => copyTemplate("ownerAria.attach.tooMany", "You can attach up to {max} photos.", { max }),
253
+ attachTooMany: (max) => copyTemplate("ownerAria.attach.tooMany", "You can attach up to {max} files.", { max }),
254
254
  attachUnsupported: () => copyParam("ownerAria.attach.unsupported", "That file type can't be attached."),
255
- attachTooLarge: (mb) => copyTemplate("ownerAria.attach.tooLarge", "That photo is too large to attach (up to {mb} MB).", { mb }),
256
- attachWaitUploading: () => copyParam("ownerAria.attach.waitUploading", "Wait for your photo to finish uploading."),
257
- attachWaitFailed: () => copyParam("ownerAria.attach.waitFailed", "Retry or remove the photo that didn't upload."),
258
- attachNeedsText: () => copyParam("ownerAria.attach.needsText", "Add a message to send with your photo."),
255
+ attachTooLarge: (mb) => copyTemplate("ownerAria.attach.tooLarge", "That file is too large to attach (up to {mb} MB).", { mb }),
256
+ attachWaitUploading: () => copyParam("ownerAria.attach.waitUploading", "Wait for your file to finish uploading."),
257
+ attachWaitFailed: () => copyParam("ownerAria.attach.waitFailed", "Retry or remove the file that didn't upload."),
258
+ attachWaitRemove: () => copyParam("ownerAria.attach.waitRemove", "Remove the file that didn't upload, then add it again."),
259
+ attachNeedsText: () => copyParam("ownerAria.attach.needsText", "Add a message to send with your file."),
259
260
  toolFailed: () => copyParam("ownerAria.toolFailed", "Aria couldn't finish that. Try again."),
260
261
  },
261
262
  clientTools: { confirm: confirmWithOwner, userAgent },
@@ -305,6 +306,10 @@ const core = createAriaChat({
305
306
  // which go to a self-authorizing url with no Wix credential attached.
306
307
  attachments: {
307
308
  pick: async (source) => {
309
+ if (source === "document") {
310
+ const file = await pickDocument();
311
+ return file ? { dataUri: file.uri, name: file.name, mimeType: file.mime } : null;
312
+ }
308
313
  const asset = await capturePhoto({ source, maxEdgePixels: 2048 });
309
314
  return asset ? { dataUri: asset.uri, mimeType: asset.mime } : null;
310
315
  },
@@ -318,6 +323,92 @@ const core = createAriaChat({
318
323
  },
319
324
  });
320
325
 
326
+ // Focused native TextFields keep their local non-empty edit rather than adopting
327
+ // worker normalization (the shared shouldAdoptWorkerValue rule). Remember only
328
+ // prefixes where THIS draft actually selected a reference, so later editor echoes and
329
+ // the Form.message submission cannot restore those triggers into the wire text.
330
+ // Email addresses and newly authored @ characters are never blanket-stripped.
331
+ let mentionTriggerPrefixes: string[] = [];
332
+ let pendingMentionTrigger: { rawPrefix: string; prefix: string } | null = null;
333
+ let mentionDraftRevision = 0;
334
+ let previousMentionRaw = "";
335
+ // Set by a selection that consumed a typed trigger: the normalized draft the
336
+ // worker now holds. A field that blurs adopts that value WITHOUT dispatching
337
+ // typing, so the next keystroke is an edit of it, not of previousMentionRaw.
338
+ // The first typing after a selection whose raw value is byte-identical to the
339
+ // pre-selection one cannot be an edit of the focused raw text (nothing changed),
340
+ // so it is read as an edit of the adopted draft: that field holds no consumed
341
+ // marker any more, and a standalone @ there is a new trigger.
342
+ let adoptedMentionDraft: string | null = null;
343
+ function reconcileMentionDraft(raw: string): string {
344
+ // Move consumed trigger positions with an edit anywhere in the focused field.
345
+ // A replaced/deleted trigger loses its marker, so newly authored @ text stays literal.
346
+ let start = 0;
347
+ while (start < previousMentionRaw.length && start < raw.length && previousMentionRaw[start] === raw[start]) start += 1;
348
+ let suffix = 0;
349
+ while (suffix < previousMentionRaw.length - start && suffix < raw.length - start
350
+ && previousMentionRaw[previousMentionRaw.length - suffix - 1] === raw[raw.length - suffix - 1]) suffix += 1;
351
+ const end = previousMentionRaw.length - suffix;
352
+ const delta = raw.length - previousMentionRaw.length;
353
+ const move = (position: number): number | null => position < start ? position : position >= end ? position + delta : null;
354
+ mentionTriggerPrefixes = mentionTriggerPrefixes.flatMap(prefix => {
355
+ let position = move(prefix.length);
356
+ // A whole-draft replacement ("Ask @" → "New @") shares only whitespace and the @
357
+ // with the old text, so the common suffix lines the NEW @ up with the old marker.
358
+ // When every word before the marker was replaced, its context is gone: the @ is
359
+ // newly authored and must stay (and may open the picker again).
360
+ if (position !== null && start === 0 && prefix.length >= end && prefix.trim()
361
+ && !previousMentionRaw.slice(end, prefix.length).trim()) return [];
362
+ // Coalesced edits may add text on both sides of the marker. Keep it when
363
+ // its complete previous context still occurs exactly once in the new value.
364
+ if (position === null && prefix) {
365
+ const anchor = `${prefix}@`; const found = raw.indexOf(anchor);
366
+ if (found >= 0 && raw.indexOf(anchor, found + 1) < 0) position = found + prefix.length;
367
+ }
368
+ // A coalesced focused-field edit can replace context on both sides of a
369
+ // selected marker. If this is the sole consumed marker and the new draft
370
+ // has one standalone @, its position is unambiguous even without the old
371
+ // prefix. Email addresses and authored @suffixes are never candidates.
372
+ if (position === null && mentionTriggerPrefixes.length === 1) {
373
+ const candidates = [...raw.matchAll(/(^|\s)@(?=\s|$)/g)].map(match => match.index! + match[1].length);
374
+ if (candidates.length === 1) position = candidates[0];
375
+ }
376
+ // Only the standalone trigger that opened the picker is consumed. Adding
377
+ // a literal suffix (or joining it to the preceding word) authors ordinary
378
+ // @ text even when the focused editor retains the selected trigger.
379
+ const standalone = position !== null && raw[position] === "@"
380
+ && (position === 0 || /\s/.test(raw[position - 1]))
381
+ && (position + 1 === raw.length || /\s/.test(raw[position + 1]));
382
+ return standalone ? [raw.slice(0, position!)] : [];
383
+ });
384
+ if (pendingMentionTrigger) {
385
+ const position = move(pendingMentionTrigger.rawPrefix.length);
386
+ const standalone = position !== null && raw[position] === "@"
387
+ && (position === 0 || /\s/.test(raw[position - 1]))
388
+ && (position + 1 === raw.length || /\s/.test(raw[position + 1]));
389
+ pendingMentionTrigger = standalone
390
+ ? { rawPrefix: raw.slice(0, position!), prefix: raw.slice(0, position!) } : null;
391
+ if (pendingMentionTrigger) {
392
+ for (const prefix of [...mentionTriggerPrefixes].sort((a, b) => b.length - a.length)) {
393
+ if (prefix.length < pendingMentionTrigger.rawPrefix.length) pendingMentionTrigger.prefix = pendingMentionTrigger.prefix.slice(0, prefix.length) + pendingMentionTrigger.prefix.slice(prefix.length + 1);
394
+ }
395
+ }
396
+ }
397
+ previousMentionRaw = raw;
398
+ let text = raw;
399
+ for (const prefix of [...mentionTriggerPrefixes].sort((a, b) => b.length - a.length)) {
400
+ text = text.slice(0, prefix.length) + text.slice(prefix.length + 1);
401
+ }
402
+ return text;
403
+ }
404
+ function reconciledMentionArg(arg?: AriaActionArg): AriaActionArg | undefined {
405
+ if (typeof arg === "string") return reconcileMentionDraft(arg);
406
+ if (!arg) return arg;
407
+ const keys: Array<"message" | "text" | "value" | "intent" | "id"> = ["message", "text", "value", "intent", "id"];
408
+ const key = keys.find(k => typeof arg[k] === "string" && arg[k]);
409
+ return key ? { ...arg, [key]: reconcileMentionDraft(String(arg[key])) } : arg;
410
+ }
411
+
321
412
  const svc = defineService<AriaChatState, {
322
413
  load: (s: AriaChatState, arg?: AriaActionArg) => Promise<AriaChatState>;
323
414
  submit: (s: AriaChatState, arg?: AriaActionArg) => Promise<AriaChatState>;
@@ -374,13 +465,51 @@ const svc = defineService<AriaChatState, {
374
465
  state: core.initialState,
375
466
  actions: {
376
467
  // `load` is the onAppear dispatch, so it is where the copy pack arrives.
377
- load: (s, arg) => { stashCopy(arg); stashTargets(arg); stashRealtimeUrl(arg); return core.load(s); },
378
- submit: core.submit,
379
- submitPrompt: core.submitPrompt,
380
- newChat: core.newChat,
468
+ load: (s, arg) => { mentionDraftRevision += 1; mentionTriggerPrefixes = []; pendingMentionTrigger = null; previousMentionRaw = ""; adoptedMentionDraft = null; stashCopy(arg); stashTargets(arg); stashRealtimeUrl(arg); return core.load(s); },
469
+ submit: async (s, arg) => {
470
+ const normalized = reconciledMentionArg(arg);
471
+ const previousPrefixes = mentionTriggerPrefixes; const previousTrigger = pendingMentionTrigger;
472
+ const revision = mentionDraftRevision;
473
+ const next = await core.submit(s, normalized);
474
+ // A refusal keeps this draft; a later keystroke/new-chat belongs to its own.
475
+ if (revision === mentionDraftRevision) {
476
+ if (next.input && next.input === s.input) { mentionTriggerPrefixes = previousPrefixes; pendingMentionTrigger = previousTrigger; }
477
+ else { mentionTriggerPrefixes = []; pendingMentionTrigger = null; previousMentionRaw = ""; adoptedMentionDraft = null; }
478
+ }
479
+ return next;
480
+ },
481
+ submitPrompt: async (s, arg) => {
482
+ const previousPrefixes = mentionTriggerPrefixes; const previousTrigger = pendingMentionTrigger;
483
+ const revision = mentionDraftRevision;
484
+ const next = await core.submitPrompt(s, arg);
485
+ if (revision === mentionDraftRevision) {
486
+ if (next.input && next.input === s.input) { mentionTriggerPrefixes = previousPrefixes; pendingMentionTrigger = previousTrigger; }
487
+ else { mentionTriggerPrefixes = []; pendingMentionTrigger = null; previousMentionRaw = ""; adoptedMentionDraft = null; }
488
+ }
489
+ return next;
490
+ },
491
+ newChat: (s) => { mentionDraftRevision += 1; mentionTriggerPrefixes = []; pendingMentionTrigger = null; previousMentionRaw = ""; adoptedMentionDraft = null; return core.newChat(s); },
381
492
  // The composer's onChange. Mirrors the draft into state so `submit` can
382
493
  // empty it and the field actually clears.
383
- typing: core.typing,
494
+ typing: (s, arg) => {
495
+ mentionDraftRevision += 1;
496
+ const raw = typeof arg === "string" ? arg : ([arg?.value, arg?.text, arg?.message].find((v): v is string => typeof v === "string") ?? "");
497
+ if (adoptedMentionDraft !== null) {
498
+ if (raw === previousMentionRaw) { mentionTriggerPrefixes = []; previousMentionRaw = adoptedMentionDraft; }
499
+ adoptedMentionDraft = null;
500
+ }
501
+ const next = core.typing(s, { value: reconcileMentionDraft(raw) });
502
+ // A standalone @ asks for a reference; emails and embedded @ characters
503
+ // remain ordinary text. A successful selection consumes its trigger.
504
+ if (next.input !== s.input && /(?:^|\s)@$/.test(next.input) && !next.mention.openHidden) {
505
+ // Keep the fragment editable until a reference is selected. Cancelling
506
+ // the picker must not turn a literal @example.com into example.com.
507
+ pendingMentionTrigger = { rawPrefix: raw.slice(0, -1), prefix: next.input.slice(0, -1) };
508
+ void svc.mentionOpen();
509
+ return next;
510
+ }
511
+ return next;
512
+ },
384
513
  // Named by the `_poll` directive above. A name that does not resolve here is
385
514
  // a `poll-action-missing` error in `himi test --content`.
386
515
  tick: core.tick,
@@ -419,11 +548,23 @@ const svc = defineService<AriaChatState, {
419
548
  // chips above the composer. Every one is named by the descriptor, so every
420
549
  // one has to be exported here — see the note on the picker family above.
421
550
  mentionOpen: core.mentionOpen,
422
- mentionClose: core.mentionClose,
551
+ mentionClose: (s) => { pendingMentionTrigger = null; return core.mentionClose(s); },
423
552
  mentionBack: core.mentionBack,
424
553
  mentionProvider: core.mentionProvider,
425
554
  mentionQuery: core.mentionQuery,
426
- mentionPick: core.mentionPick,
555
+ mentionPick: (s, arg) => {
556
+ const next = core.mentionPick(s, arg);
557
+ const trigger = pendingMentionTrigger;
558
+ if (trigger && !s.mention.hidden && next.mention.hidden && next.input.startsWith(`${trigger.prefix}@`)) {
559
+ mentionDraftRevision += 1;
560
+ pendingMentionTrigger = null;
561
+ if (!mentionTriggerPrefixes.includes(trigger.rawPrefix)) mentionTriggerPrefixes.push(trigger.rawPrefix);
562
+ const normalized = next.input.slice(0, trigger.prefix.length) + next.input.slice(trigger.prefix.length + 1);
563
+ adoptedMentionDraft = normalized;
564
+ return core.typing(next, { value: normalized });
565
+ }
566
+ return next;
567
+ },
427
568
  mentionRemove: core.mentionRemove,
428
569
  // The composer's photos. `attachmentSettled` is dispatched by the upload
429
570
  // itself (see `attachments.settled` above), never by the descriptor.