@wix/himalaya-cli 0.1318.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 +473 -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
@@ -62054,6 +62054,65 @@ const SCREENS = runtimeSetting({
62054
62054
  },
62055
62055
  {
62056
62056
  "id": "",
62057
+ "body": {
62058
+ "package": "himalaya",
62059
+ "type": "VStack",
62060
+ "props": {
62061
+ "spacing": 4,
62062
+ "children": [
62063
+ {
62064
+ "id": "",
62065
+ "body": {
62066
+ "package": "himalaya",
62067
+ "type": "Icon",
62068
+ "props": {
62069
+ "name": "doc",
62070
+ "size": 24,
62071
+ "tint": "textSecondary"
62072
+ }
62073
+ },
62074
+ "modifiers": {
62075
+ "accessibilityRole": "decorative"
62076
+ }
62077
+ },
62078
+ {
62079
+ "id": "",
62080
+ "body": {
62081
+ "package": "himalaya",
62082
+ "type": "Text",
62083
+ "props": {
62084
+ "text": "$item.name",
62085
+ "style": "caption",
62086
+ "color": "textPrimary",
62087
+ "maxLines": 2
62088
+ }
62089
+ },
62090
+ "modifiers": {
62091
+ "height": 44
62092
+ }
62093
+ },
62094
+ {
62095
+ "id": "",
62096
+ "body": {
62097
+ "package": "himalaya",
62098
+ "type": "Text",
62099
+ "props": {
62100
+ "text": "$item.typeLabel",
62101
+ "style": "caption",
62102
+ "color": "textSecondary",
62103
+ "maxLines": 1
62104
+ }
62105
+ }
62106
+ }
62107
+ ]
62108
+ }
62109
+ },
62110
+ "modifiers": {
62111
+ "hiddenPath": "$item.documentHidden"
62112
+ }
62113
+ },
62114
+ {
62115
+ "id": "himi_owner_lite_aria.attachment_status_$item.id",
62057
62116
  "body": {
62058
62117
  "package": "himalaya",
62059
62118
  "type": "Text",
@@ -62065,6 +62124,7 @@ const SCREENS = runtimeSetting({
62065
62124
  }
62066
62125
  },
62067
62126
  "modifiers": {
62127
+ "height": 44,
62068
62128
  "hiddenPath": "$item.statusHidden"
62069
62129
  }
62070
62130
  },
@@ -62080,21 +62140,22 @@ const SCREENS = runtimeSetting({
62080
62140
  "id": "himi_owner_lite_aria.attachment_retry_$item.id",
62081
62141
  "body": {
62082
62142
  "package": "himalaya",
62083
- "type": "Text",
62143
+ "type": "Button",
62084
62144
  "props": {
62085
- "text": "Retry",
62086
- "style": "caption",
62087
- "color": "accentPrimary",
62088
- "maxLines": 1
62145
+ "title": "Retry",
62146
+ "action": {
62147
+ "actionId": "tier5.himiOwnerLiteAria.retryAttachment",
62148
+ "params": {
62149
+ "id": "$item.id"
62150
+ }
62151
+ },
62152
+ "style": "tertiary",
62153
+ "size": "sm"
62089
62154
  }
62090
62155
  },
62091
62156
  "modifiers": {
62092
- "onTap": {
62093
- "actionId": "tier5.himiOwnerLiteAria.retryAttachment",
62094
- "params": {
62095
- "id": "$item.id"
62096
- }
62097
- },
62157
+ "width": 92,
62158
+ "height": 48,
62098
62159
  "hiddenPath": "$item.retryHidden"
62099
62160
  }
62100
62161
  },
@@ -62102,32 +62163,51 @@ const SCREENS = runtimeSetting({
62102
62163
  "id": "himi_owner_lite_aria.attachment_remove_$item.id",
62103
62164
  "body": {
62104
62165
  "package": "himalaya",
62105
- "type": "Icon",
62166
+ "type": "Button",
62106
62167
  "props": {
62107
- "name": "close",
62108
- "size": 14,
62109
- "tint": "textSecondary"
62168
+ "title": "Remove attachment",
62169
+ "action": {
62170
+ "actionId": "tier5.himiOwnerLiteAria.removeAttachment",
62171
+ "params": {
62172
+ "id": "$item.id"
62173
+ }
62174
+ },
62175
+ "style": "tertiary",
62176
+ "iconOnly": true,
62177
+ "leadingIcon": "close",
62178
+ "cornerRadius": 24,
62179
+ "minHeight": 48
62110
62180
  }
62111
62181
  },
62112
62182
  "modifiers": {
62113
- "onTap": {
62114
- "actionId": "tier5.himiOwnerLiteAria.removeAttachment",
62115
- "params": {
62116
- "id": "$item.id"
62117
- }
62118
- },
62119
- "accessibilityLabel": "Remove photo"
62183
+ "width": 48,
62184
+ "height": 48,
62185
+ "accessibilityLabel": "Remove attachment",
62186
+ "accessibilityRole": "button"
62120
62187
  }
62121
62188
  }
62122
62189
  ]
62123
62190
  }
62191
+ },
62192
+ "modifiers": {
62193
+ "height": 48
62124
62194
  }
62125
62195
  }
62126
62196
  ]
62127
62197
  }
62128
62198
  },
62129
62199
  "modifiers": {
62130
- "width": 72
62200
+ "width": 160,
62201
+ "padding": {
62202
+ "top": 8,
62203
+ "bottom": 8,
62204
+ "leading": 8,
62205
+ "trailing": 8
62206
+ },
62207
+ "background": {
62208
+ "token": "surfaceSecondary"
62209
+ },
62210
+ "cornerRadius": 12
62131
62211
  }
62132
62212
  }
62133
62213
  ]
@@ -62139,7 +62219,7 @@ const SCREENS = runtimeSetting({
62139
62219
  "leading": 0,
62140
62220
  "trailing": 0,
62141
62221
  "top": 8,
62142
- "bottom": 0
62222
+ "bottom": 8
62143
62223
  }
62144
62224
  }
62145
62225
  },
@@ -62192,6 +62272,27 @@ const SCREENS = runtimeSetting({
62192
62272
  },
62193
62273
  "accessibilityLabel": "Take a photo"
62194
62274
  }
62275
+ },
62276
+ {
62277
+ "id": "himi_owner_lite_aria.attach.document",
62278
+ "body": {
62279
+ "package": "himalaya",
62280
+ "type": "Icon",
62281
+ "props": {
62282
+ "name": "document",
62283
+ "size": 22,
62284
+ "tint": "accentPrimary"
62285
+ }
62286
+ },
62287
+ "modifiers": {
62288
+ "onTap": {
62289
+ "actionId": "tier5.himiOwnerLiteAria.attach",
62290
+ "params": {
62291
+ "source": "document"
62292
+ }
62293
+ },
62294
+ "accessibilityLabel": "Add files"
62295
+ }
62195
62296
  }
62196
62297
  ]
62197
62298
  }
@@ -27,6 +27,14 @@ export const ARIA_IMAGE_TYPES: readonly string[] = [
27
27
  "image/heic",
28
28
  ];
29
29
 
30
+ /** Document MIME types supported by the native Aria reference. */
31
+ export const ARIA_DOCUMENT_TYPES: readonly string[] = [
32
+ "text/csv", "text/plain", "application/pdf", "application/msword",
33
+ "application/vnd.ms-excel", "application/vnd.ms-powerpoint",
34
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
35
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
36
+ ];
37
+
30
38
  /** At most this many attachments ride one message. The native Aria app caps at
31
39
  * 10; nothing on the wire says otherwise. */
32
40
  export const ARIA_MAX_ATTACHMENTS = 10;
@@ -40,6 +48,9 @@ export const ARIA_MAX_ATTACHMENTS = 10;
40
48
  * Checking it HERE is what turns that refusal into a sentence the owner can act
41
49
  * on, instead of a chip that fails for no stated reason. */
42
50
  export const ARIA_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
51
+ // Inline documents include base64 and JSON overhead under the 4 MiB frame bridge.
52
+ // Larger documents use the file-backed attachment follow-up.
53
+ export const ARIA_MAX_DOCUMENT_BYTES = 2 * 1024 * 1024;
43
54
 
44
55
  /** A chip in the composer. Every field is always present — a `$item.` path that
45
56
  * is missing on some rows does not substitute — and each `…Hidden` flag
@@ -58,6 +69,9 @@ export interface AriaAttachmentChip {
58
69
  * runtime's frame limit. */
59
70
  thumbUrl: string;
60
71
  thumbHidden: boolean;
72
+ /** Filename/type card for documents (images keep the 72pt preview). */
73
+ documentHidden: boolean;
74
+ typeLabel: string;
61
75
  /** "Uploading…" / "Couldn't upload" / "" — collapses when empty. */
62
76
  statusText: string;
63
77
  statusHidden: boolean;
@@ -113,6 +127,21 @@ export function normalizeMime(mime: string): string {
113
127
  }
114
128
  }
115
129
 
130
+ /** Native providers sometimes report a generic MIME; match the reference's
131
+ * extension fallback only for those values, not a conflicting explicit type. */
132
+ export function attachmentMime(mime: string, name = ""): string {
133
+ const normalized = normalizeMime(mime);
134
+ if (normalized === "text/comma-separated-values") return "text/csv";
135
+ if (normalized && normalized !== "application/octet-stream") return normalized;
136
+ const extension = name.split(".").pop()?.toLowerCase() ?? "";
137
+ const types: Record<string, string> = {
138
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif", heic: "image/heic", heif: "image/heif",
139
+ csv: "text/csv", txt: "text/plain", pdf: "application/pdf", doc: "application/msword", xls: "application/vnd.ms-excel", ppt: "application/vnd.ms-powerpoint",
140
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
141
+ };
142
+ return types[extension] ?? normalized;
143
+ }
144
+
116
145
  /** A file name for an image the picker did not name, by type. The service
117
146
  * requires one (1–255 characters), and it is what the transcript shows. */
118
147
  export function imageFileName(mimeType: string, n: number, given?: string): string {
@@ -131,8 +160,8 @@ export type AttachRefusal = "" | "type" | "size" | "count";
131
160
 
132
161
  export function refusalFor(parsed: ParsedDataUri | null, mimeType: string, attached: number): AttachRefusal {
133
162
  if (attached >= ARIA_MAX_ATTACHMENTS) return "count";
134
- if (!parsed || !ARIA_IMAGE_TYPES.includes(mimeType)) return "type";
135
- if (parsed.bytes > ARIA_MAX_IMAGE_BYTES) return "size";
163
+ if (!parsed || ![...ARIA_IMAGE_TYPES, ...ARIA_DOCUMENT_TYPES].includes(mimeType)) return "type";
164
+ if (parsed.bytes > (mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES)) return "size";
136
165
  return "";
137
166
  }
138
167
 
@@ -146,10 +175,20 @@ export function chipIn(
146
175
  ref = "",
147
176
  ): AriaAttachmentChip {
148
177
  const statusText = state === "uploading" ? labels.uploading : state === "failed" ? labels.failed : "";
178
+ const dot = base.name.lastIndexOf(".");
179
+ const extension = dot > 0 && dot < base.name.length - 1 ? base.name.slice(dot + 1).toUpperCase() : "";
180
+ const mimeLabels: Record<string, string> = {
181
+ "application/pdf": "PDF", "text/csv": "CSV", "text/plain": "TXT", "application/msword": "DOC",
182
+ "application/vnd.ms-excel": "XLS", "application/vnd.ms-powerpoint": "PPT",
183
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
184
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
185
+ };
149
186
  return {
150
187
  ...base,
151
188
  state,
152
189
  thumbHidden: !base.thumbUrl,
190
+ documentHidden: base.mimeType.startsWith("image/"),
191
+ typeLabel: extension || mimeLabels[base.mimeType] || base.mimeType,
153
192
  statusText,
154
193
  statusHidden: !statusText,
155
194
  retryHidden: state !== "failed",
@@ -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)),