@wix/himalaya-cli 0.1382.0 → 0.1383.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 (92) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/camera/src/index.d.ts +18 -1
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/file-picker/src/index.d.ts +14 -4
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/files/src/index.d.ts +1 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/photos/src/index.d.ts +9 -4
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/presigned-upload/src/index.d.ts +1 -1
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +1 -1
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/index.d.ts +12 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -1
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -1
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -1
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/camera.mjs +17 -1
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -1
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/file-picker.mjs +17 -5
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/files.mjs +9 -5
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -1
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -1
  17. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +17 -5
  18. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/presigned-upload.mjs +2 -1
  19. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -1
  20. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/share.mjs +3 -0
  21. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -1
  22. package/dist/cli.mjs +25 -24
  23. package/dist/preview-assets.generated.bin +0 -0
  24. package/guides/sdk-reference.json +58 -10
  25. package/guides/web-page-sdk-reference.md +3 -3
  26. package/package.json +2 -2
  27. package/templates/beauty-salon-owner/config.ts +1 -1
  28. package/templates/beauty-salon-owner/dev/index.ts +3 -0
  29. package/templates/beauty-salon-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  30. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +114 -44
  31. package/templates/beauty-salon-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  32. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  33. package/templates/community-club-owner/config.ts +1 -1
  34. package/templates/community-club-owner/dev/index.ts +3 -0
  35. package/templates/community-club-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  36. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +114 -44
  37. package/templates/community-club-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  38. package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  39. package/templates/events-meetups-owner/config.ts +1 -1
  40. package/templates/events-meetups-owner/dev/index.ts +3 -0
  41. package/templates/events-meetups-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  42. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +114 -44
  43. package/templates/events-meetups-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  44. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  45. package/templates/gym-fitness-owner/config.ts +1 -1
  46. package/templates/gym-fitness-owner/dev/index.ts +3 -0
  47. package/templates/gym-fitness-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  48. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +114 -44
  49. package/templates/gym-fitness-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  50. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  51. package/templates/nonprofit-cause-owner/config.ts +1 -1
  52. package/templates/nonprofit-cause-owner/dev/index.ts +3 -0
  53. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  54. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +114 -44
  55. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  56. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  57. package/templates/owner-lite/config.ts +1 -1
  58. package/templates/owner-lite/dev/index.ts +3 -0
  59. package/templates/owner-lite/tier5-src/_shared/aria-attachments.ts +35 -18
  60. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +114 -44
  61. package/templates/owner-lite/tier5-src/_shared/aria-upload-queue.ts +47 -0
  62. package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  63. package/templates/podcast-creator-owner/config.ts +1 -1
  64. package/templates/podcast-creator-owner/dev/index.ts +3 -0
  65. package/templates/podcast-creator-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  66. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +114 -44
  67. package/templates/podcast-creator-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  68. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  69. package/templates/portfolio-creative-owner/config.ts +1 -1
  70. package/templates/portfolio-creative-owner/dev/index.ts +3 -0
  71. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  72. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +114 -44
  73. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  74. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  75. package/templates/restaurant-owner/config.ts +1 -1
  76. package/templates/restaurant-owner/dev/index.ts +3 -0
  77. package/templates/restaurant-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  78. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +114 -44
  79. package/templates/restaurant-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  80. package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  81. package/templates/retail-store-owner/config.ts +1 -1
  82. package/templates/retail-store-owner/dev/index.ts +3 -0
  83. package/templates/retail-store-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  84. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +114 -44
  85. package/templates/retail-store-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  86. package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
  87. package/templates/yoga-studio-owner/config.ts +1 -1
  88. package/templates/yoga-studio-owner/dev/index.ts +3 -0
  89. package/templates/yoga-studio-owner/tier5-src/_shared/aria-attachments.ts +35 -18
  90. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +114 -44
  91. package/templates/yoga-studio-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
  92. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
@@ -18,6 +18,8 @@
18
18
  * never silently dropped — the native Aria app drops unsupported files without a
19
19
  * word, and an owner who picked something and sees nothing happen assumes the
20
20
  * button is broken. */
21
+ import type { FileHandle } from "@himalaya/sdk-types";
22
+
21
23
  export const ARIA_IMAGE_TYPES: readonly string[] = [
22
24
  "image/png",
23
25
  "image/jpeg",
@@ -25,6 +27,7 @@ export const ARIA_IMAGE_TYPES: readonly string[] = [
25
27
  "image/webp",
26
28
  "image/gif",
27
29
  "image/heic",
30
+ "image/heif",
28
31
  ];
29
32
 
30
33
  /** Document MIME types supported by the native Aria reference. */
@@ -33,24 +36,22 @@ export const ARIA_DOCUMENT_TYPES: readonly string[] = [
33
36
  "application/vnd.ms-excel", "application/vnd.ms-powerpoint",
34
37
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
35
38
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
39
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
36
40
  ];
37
41
 
42
+ /** Android's actual DocumentsProvider reports CSV using this equivalent MIME.
43
+ * Include it in picker filters, then normalize it back to the wire's text/csv. */
44
+ export const ARIA_DOCUMENT_PICKER_TYPES: readonly string[] = [...ARIA_DOCUMENT_TYPES, "text/comma-separated-values"];
45
+
38
46
  /** At most this many attachments ride one message. The native Aria app caps at
39
47
  * 10; nothing on the wire says otherwise. */
40
48
  export const ARIA_MAX_ATTACHMENTS = 10;
41
49
 
42
- /** The largest image the composer will try to send, in bytes.
43
- *
44
- * Not a product choice: it is the ceiling of the transfer this slice uses. A
45
- * picked photo crosses the worker bridge as a base64 string and is sent inline,
46
- * and the inline upload refuses anything over 8 MiB (the `MAX_INLINE_BASE64_BYTES`
47
- * bound of the pre-signed upload capability, which it enforces on every arm).
48
- * Checking it HERE is what turns that refusal into a sentence the owner can act
49
- * on, instead of a chip that fails for no stated reason. */
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;
50
+ /** Prepared/staged limits from first-party Composer policy b2205e5f. Full
51
+ * upload bytes stay in FileHandles; the inline bridge bound is separate. */
52
+ export const ARIA_MAX_IMAGE_BYTES = 4 * 1024 * 1024;
53
+ export const ARIA_MAX_DOCUMENT_BYTES = 25 * 1024 * 1024;
54
+ export const ARIA_MAX_STAGED_BYTES = 100 * 1024 * 1024;
54
55
 
55
56
  /** A chip in the composer. Every field is always present — a `$item.` path that
56
57
  * is missing on some rows does not substitute — and each `…Hidden` flag
@@ -64,7 +65,7 @@ export interface AriaAttachmentChip {
64
65
  state: "uploading" | "ready" | "failed";
65
66
  /** The chip's thumbnail as a `data:` URI, set only when the picked image is
66
67
  * small; "" otherwise, which collapses the preview. The full bytes an upload
67
- * and a retry send are held by the worker, never in the chip, because the chip
68
+ * and a retry send stay in the host file substrate; only its handle rides the chip, which
68
69
  * rides the service state and a large photo there would exceed the web
69
70
  * runtime's frame limit. */
70
71
  thumbUrl: string;
@@ -80,6 +81,9 @@ export interface AriaAttachmentChip {
80
81
  /** `genie-upload://<id>` once the bytes have landed; "" until then. The send
81
82
  * carries a chip only when this is set. */
82
83
  ref: string;
84
+ /** Persistent substrate handle: retry after worker restart needs no inline bytes. */
85
+ file: FileHandle | null;
86
+ sizeBytes: number;
83
87
  }
84
88
 
85
89
  /** What an upload reports back, delivered as a fresh action so it lands on the
@@ -120,6 +124,7 @@ export function normalizeMime(mime: string): string {
120
124
  case "public.jpeg": return "image/jpeg";
121
125
  case "public.png": return "image/png";
122
126
  case "public.heic": return "image/heic";
127
+ case "public.heif": return "image/heif";
123
128
  case "com.compuserve.gif": return "image/gif";
124
129
  case "org.webmproject.webp": return "image/webp";
125
130
  case "image/jpg": return "image/jpeg";
@@ -156,20 +161,29 @@ export function imageFileName(mimeType: string, n: number, given?: string): stri
156
161
  }
157
162
 
158
163
  /** Why a picked file cannot be attached, or "" when it can. */
159
- export type AttachRefusal = "" | "type" | "size" | "count";
164
+ export type AttachRefusal = "" | "type" | "size" | "count" | "total";
160
165
 
161
- export function refusalFor(parsed: ParsedDataUri | null, mimeType: string, attached: number): AttachRefusal {
166
+ /** Pinned first-party Composer policy, revision b2205e5f. The file here is
167
+ * already normalized: raster preparation happens in the host before staging. */
168
+ export function refusalForFile(bytes: number, mimeType: string, attached: number, stagedBytes = 0): AttachRefusal {
162
169
  if (attached >= ARIA_MAX_ATTACHMENTS) return "count";
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";
170
+ if (![...ARIA_IMAGE_TYPES, ...ARIA_DOCUMENT_TYPES].includes(mimeType) || !Number.isFinite(bytes) || bytes < 0) return "type";
171
+ const limit = mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES;
172
+ if (bytes > limit) return "size";
173
+ if (bytes + stagedBytes > ARIA_MAX_STAGED_BYTES) return "total";
165
174
  return "";
166
175
  }
167
176
 
177
+ export function refusalFor(parsed: ParsedDataUri | null, mimeType: string, attached: number): AttachRefusal {
178
+ if (!parsed) return attached >= ARIA_MAX_ATTACHMENTS ? "count" : "type";
179
+ return refusalForFile(parsed.bytes, mimeType, attached);
180
+ }
181
+
168
182
  /** A chip in a given state. One constructor so the flags can never disagree
169
183
  * with the state they describe — a failed chip without its retry control is
170
184
  * the shape of bug this rules out. */
171
185
  export function chipIn(
172
- base: Pick<AriaAttachmentChip, "id" | "name" | "mimeType" | "thumbUrl">,
186
+ base: Pick<AriaAttachmentChip, "id" | "name" | "mimeType" | "thumbUrl"> & Partial<Pick<AriaAttachmentChip, "file" | "sizeBytes">>,
173
187
  state: AriaAttachmentChip["state"],
174
188
  labels: { uploading: string; failed: string },
175
189
  ref = "",
@@ -182,9 +196,12 @@ export function chipIn(
182
196
  "application/vnd.ms-excel": "XLS", "application/vnd.ms-powerpoint": "PPT",
183
197
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
184
198
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
199
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX",
185
200
  };
186
201
  return {
187
202
  ...base,
203
+ file: base.file ?? null,
204
+ sizeBytes: base.sizeBytes ?? 0,
188
205
  state,
189
206
  thumbHidden: !base.thumbUrl,
190
207
  documentHidden: base.mimeType.startsWith("image/"),
@@ -62,16 +62,19 @@ import {
62
62
  ARIA_MAX_ATTACHMENTS,
63
63
  ARIA_MAX_IMAGE_BYTES,
64
64
  ARIA_MAX_DOCUMENT_BYTES,
65
+ ARIA_MAX_STAGED_BYTES,
65
66
  attachmentLine,
66
67
  chipIn,
67
68
  imageFileName,
68
69
  attachmentMime,
69
70
  parseDataUri,
70
- refusalFor,
71
+ refusalForFile,
71
72
  sendBlocker,
72
73
  type AriaAttachmentChip,
73
74
  type AriaAttachmentOutcome,
74
75
  } from "./aria-attachments.js";
76
+ import type { FileHandle } from "@himalaya/sdk-types";
77
+ import { createAriaUploadQueue } from "./aria-upload-queue.js";
75
78
  import { runClientToolExchange, type AriaClientTools, type ConsentMemory } from "./aria-client-tools.js";
76
79
  import { proseOfParts, scanEnvelopes, stripHidden } from "./aria-widgets.js";
77
80
  import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
@@ -480,6 +483,7 @@ export interface AriaChatLabels {
480
483
  attachTooMany?: (max: number) => string;
481
484
  attachUnsupported?: Resolvable<string>;
482
485
  attachTooLarge?: (maxMb: number) => string;
486
+ attachTotalTooLarge?: (maxMb: number) => string;
483
487
  /** Send refused while a photo is still uploading, or failed to. */
484
488
  attachWaitUploading?: Resolvable<string>;
485
489
  attachWaitFailed?: Resolvable<string>;
@@ -538,18 +542,26 @@ export interface AriaDictationOptions {
538
542
  * Both ports must REJECT (or resolve not-ok) on failure. A port that swallows
539
543
  * an error and resolves as if it had worked would put a photo on the message
540
544
  * that the service never received. */
545
+ export interface AriaPickedAttachment { dataUri?: string; file?: FileHandle; name?: string; mimeType?: string; previewUri?: string; refusal?: "type" | "size" | "count" | "total" }
541
546
  export interface AriaAttachmentPorts {
542
547
  /** Let the owner pick (or take) one photo. Resolves the image as a `data:`
543
548
  * URI, or null when they cancelled. */
544
- pick(source: "library" | "camera" | "document"): Promise<{ dataUri: string; name?: string; mimeType?: string } | null>;
549
+ pick(source: "library" | "camera" | "document", budget?: { maxItems: number; maxTotalBytes: number }): Promise<AriaPickedAttachment | AriaPickedAttachment[] | null>;
545
550
  /** PUT `base64` to a pre-signed url with `contentType`, carrying NO user
546
551
  * credentials — the url authorizes itself. */
547
552
  put(url: string, base64: string, contentType: string): Promise<{ ok: boolean; status: number }>;
553
+ /** File-backed transfer never copies a document into a worker base64 string. */
554
+ putFile?(url: string, file: FileHandle, contentType: string): Promise<{ ok: boolean; status: number }>;
555
+ /** Release only imported files refused by draft policy, never live/retry handles. */
556
+ discard?(file: FileHandle): Promise<void>;
548
557
  /** Hand an upload's outcome back to the service as a NEW action
549
558
  * (`attachmentSettled`). A fresh dispatch rather than a return value, because
550
559
  * the upload outlives the action that started it — which must return at once
551
560
  * so the chip paints while the bytes are still moving. */
552
561
  settled(outcome: AriaAttachmentOutcome): void;
562
+ /** Flush this draft at attachment boundaries, before background/pop persistence.
563
+ * The supplied state excludes transient runtime directives. */
564
+ persistDraft?(state: AriaChatState): void;
553
565
  }
554
566
 
555
567
  /** Server-provided prompt cards — the chat-service's sibling knowledge service
@@ -1678,12 +1690,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1678
1690
  const attachUploadingText = () => res(o.labels?.attachUploading ?? "Uploading…");
1679
1691
  const attachFailedText = () => res(o.labels?.attachFailed ?? "Couldn't upload");
1680
1692
  const chipLabels = () => ({ uploading: attachUploadingText(), failed: attachFailedText() });
1681
- const refusalText = (why: "type" | "size" | "count", maxBytes = ARIA_MAX_IMAGE_BYTES): string =>
1693
+ const refusalText = (why: "type" | "size" | "count" | "total", mimeType = "image/jpeg"): string =>
1682
1694
  why === "count"
1683
1695
  ? o.labels?.attachTooMany?.(ARIA_MAX_ATTACHMENTS) ?? `You can attach up to ${ARIA_MAX_ATTACHMENTS} files.`
1696
+ : why === "total" ? o.labels?.attachTotalTooLarge?.(ARIA_MAX_STAGED_BYTES / (1024 * 1024)) ?? `Your attached files exceed ${ARIA_MAX_STAGED_BYTES / (1024 * 1024)} MB in total. Remove a file.`
1684
1697
  : why === "size"
1685
- ? o.labels?.attachTooLarge?.(maxBytes / (1024 * 1024))
1686
- ?? `That file is too large to attach (up to ${maxBytes / (1024 * 1024)} MB).`
1698
+ ? o.labels?.attachTooLarge?.((mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES) / (1024 * 1024))
1699
+ ?? `That file is too large to attach (up to ${(mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES) / (1024 * 1024)} MB).`
1687
1700
  : res(o.labels?.attachUnsupported ?? "That file type can't be attached.");
1688
1701
  const waitUploadingText = () => res(o.labels?.attachWaitUploading ?? "Wait for your file to finish uploading.");
1689
1702
  const waitFailedText = () => res(o.labels?.attachWaitFailed ?? "Retry or remove the file that didn't upload.");
@@ -1699,7 +1712,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
1699
1712
  * typing…", which a photo edit must never erase. */
1700
1713
  const withoutNotice = <T extends { statusText: string }>(s: T): T => {
1701
1714
  const notices = [waitUploadingText(), waitFailedText(), waitRemoveText(), needsTextText(),
1702
- refusalText("type"), refusalText("size"), refusalText("size", ARIA_MAX_DOCUMENT_BYTES), refusalText("count")];
1715
+ refusalText("type"), refusalText("size"), refusalText("size", "application/pdf"), refusalText("count"), refusalText("total")];
1703
1716
  return s.statusText && notices.includes(s.statusText) ? { ...s, statusText: "" } : s;
1704
1717
  };
1705
1718
  const toolFailedText = () => res(o.labels?.toolFailed ?? `${assistant()} couldn't finish that. Try again.`);
@@ -2035,6 +2048,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2035
2048
  * hands the first action a PERSISTED state whose chips this worker has never
2036
2049
  * seen; the latch starts empty and would otherwise wipe them. */
2037
2050
  let chipsSeeded = false;
2051
+ let lastPersistedChips: AriaAttachmentChip[] | undefined;
2038
2052
  /** Mints chip ids. Never derived from the file: two photos named
2039
2053
  * `IMG_0001.jpg` are two chips. */
2040
2054
  let chipSeq = 0;
@@ -2045,6 +2059,24 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2045
2059
  * to 8 MB each, the chips alone would exceed that, and the action would fail
2046
2060
  * before anything rendered. Pruned to the live chips in `gated`. */
2047
2061
  const chipBytes = new Map<string, string>();
2062
+ // Account files belong to this draft until removed/sent. Each queued/active
2063
+ // transport leases its handle until it really exits, including cancelled work.
2064
+ const ownedFiles = new Map<string, FileHandle>();
2065
+ const fileUses = new Map<string, number>();
2066
+ const releasingFiles = new Set<string>();
2067
+ const pruneFiles = (): void => {
2068
+ const discard = o.attachments?.discard;
2069
+ if (!discard) return;
2070
+ const live = new Set(chips.flatMap(chip => chip.file ? [chip.file.uri] : []));
2071
+ for (const [uri, file] of ownedFiles) {
2072
+ if (live.has(uri) || (fileUses.get(uri) ?? 0) > 0 || releasingFiles.has(uri)) continue;
2073
+ releasingFiles.add(uri);
2074
+ void discard(file).then(() => ownedFiles.delete(uri)).catch(() => {
2075
+ // Keep ownership so a later gate can retry a failed cleanup.
2076
+ }).finally(() => releasingFiles.delete(uri));
2077
+ }
2078
+ };
2079
+ const uploadQueue = createAriaUploadQueue<AriaAttachmentOutcome>();
2048
2080
  /** A picked image this small doubles as its own chip thumbnail; a larger one
2049
2081
  * shows the chip without a preview. Ten of these stay well under a frame. */
2050
2082
  const THUMB_INLINE_MAX = 200_000;
@@ -2054,22 +2086,24 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2054
2086
  chipsSeeded = true;
2055
2087
  const persisted = Array.isArray(s.attachments) ? s.attachments : [];
2056
2088
  // An upload does not survive the process that ran it, so a chip persisted
2057
- // mid-upload would say "Uploading…" forever. Restore it as failed; only a
2058
- // chip with retained inline bytes can offer Retry until the file-backed follow-up.
2089
+ // mid-upload would say "Uploading…" forever. Its persisted file handle can
2090
+ // be retried in this owner scope. Legacy chips without a retained source
2091
+ // still offer Remove, but cannot offer a working retry.
2059
2092
  chips = persisted
2060
2093
  .filter((c) => c && typeof c.id === "string")
2061
2094
  .map((c) => {
2062
2095
  const chip = chipIn(c, c.state === "uploading" ? "failed" : c.state, chipLabels(), c.ref);
2063
- // Inline bytes were worker-owned. A cold-start document or large image
2064
- // has no source left: keep Remove available, never offer a dead Retry.
2065
- return chip.state === "failed" && !chip.thumbUrl ? { ...chip, retryHidden: true } : chip;
2096
+ return chip.state === "failed" && !chip.file && !parseDataUri(chip.thumbUrl)
2097
+ ? { ...chip, retryHidden: true } : chip;
2066
2098
  });
2067
2099
  for (const c of chips) {
2068
2100
  const n = Number(/^att-(\d+)$/.exec(c.id)?.[1] ?? 0);
2069
2101
  if (n > chipSeq) chipSeq = n;
2070
- // A rehydrated chip can retry only the source it kept inline.
2102
+ // Legacy inline consumers can retry a stored thumbnail. File-backed
2103
+ // consumers use c.file, including documents above the inline bridge limit.
2071
2104
  if (c.thumbUrl && !chipBytes.has(c.id)) chipBytes.set(c.id, c.thumbUrl);
2072
2105
  }
2106
+ for (const chip of chips) if (chip.file) ownedFiles.set(chip.file.uri, chip.file);
2073
2107
  };
2074
2108
 
2075
2109
  /** Is Send refused while photos are pending, and what does the owner see?
@@ -2103,20 +2137,32 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
2103
2137
  const startUpload = (chip: AriaAttachmentChip): void => {
2104
2138
  const ports = o.attachments;
2105
2139
  if (!ports) return;
2140
+ const site = siteOf();
2141
+ const current = () => siteOf() === site && chips.some(c => c.id === chip.id && c.state === "uploading");
2106
2142
  const run = async (): Promise<AriaAttachmentOutcome> => {
2143
+ if (!current()) return { id: chip.id, ok: false };
2107
2144
  const parsed = parseDataUri(chipBytes.get(chip.id) ?? chip.thumbUrl);
2108
- if (!parsed) return { id: chip.id, ok: false };
2145
+ if (!chip.file && !parsed) return { id: chip.id, ok: false };
2109
2146
  const minted = await createFileUpload(chip.name, chip.mimeType);
2147
+ if (!current()) return { id: chip.id, ok: false };
2110
2148
  const id = (minted?.upload?.id ?? "").trim();
2111
2149
  const target = uploadTargetUrl(minted?.upload?.uploadUrl ?? "", chip.name);
2112
2150
  if (!id || !target) return { id: chip.id, ok: false };
2113
- const sent = await ports.put(target, parsed.base64, chip.mimeType);
2151
+ const sent = chip.file && ports.putFile ? await ports.putFile(target, chip.file, chip.mimeType)
2152
+ : parsed ? await ports.put(target, parsed.base64, chip.mimeType) : null;
2114
2153
  return sent?.ok ? { id: chip.id, ok: true, ref: genieUploadRef(id) } : { id: chip.id, ok: false };
2115
2154
  };
2116
- run().then(
2117
- (outcome) => ports.settled(outcome),
2118
- () => ports.settled({ id: chip.id, ok: false }),
2119
- );
2155
+ if (chip.file) {
2156
+ ownedFiles.set(chip.file.uri, chip.file);
2157
+ fileUses.set(chip.file.uri, (fileUses.get(chip.file.uri) ?? 0) + 1);
2158
+ }
2159
+ uploadQueue.enqueue(chip.id, run, outcome => ports.settled(outcome ?? { id: chip.id, ok: false }), () => {
2160
+ if (chip.file) {
2161
+ const uses = (fileUses.get(chip.file.uri) ?? 1) - 1;
2162
+ if (uses > 0) fileUses.set(chip.file.uri, uses); else fileUses.delete(chip.file.uri);
2163
+ }
2164
+ pruneFiles();
2165
+ });
2120
2166
  };
2121
2167
 
2122
2168
  /** One create at a time, per site.
@@ -3295,6 +3341,8 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3295
3341
  // a cold start stamps the PERSISTED chips rather than an empty latch.
3296
3342
  seedChips(actioned);
3297
3343
  for (const id of [...chipBytes.keys()]) if (!chips.some((c) => c.id === id)) chipBytes.delete(id);
3344
+ uploadQueue.retain(new Set(chips.filter(c => c.state === "uploading").map(c => c.id)));
3345
+ pruneFiles();
3298
3346
  const attachmentsHidden = chips.length === 0;
3299
3347
  let out = actioned.attachments === chips && actioned.attachmentsHidden === attachmentsHidden
3300
3348
  ? actioned
@@ -3313,6 +3361,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
3313
3361
  // `_poll` from here would navigate, or re-arm the timer, a second time.
3314
3362
  committed = settled(out);
3315
3363
  commits += 1;
3364
+ if (lastPersistedChips !== chips && o.attachments?.persistDraft) {
3365
+ try {
3366
+ o.attachments.persistDraft(committed);
3367
+ lastPersistedChips = chips;
3368
+ } catch { /* native snapshot writes are best-effort; retry at the next gate */ }
3369
+ }
3316
3370
  return out;
3317
3371
  };
3318
3372
 
@@ -4194,6 +4248,8 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
4194
4248
  // Refused BEFORE the picker opens when there is no room: presenting a
4195
4249
  // picker whose result will be thrown away wastes the owner's time.
4196
4250
  if (chips.length >= ARIA_MAX_ATTACHMENTS) return { ...s, statusText: refusalText("count"), error: "" };
4251
+ const remainingBytes = ARIA_MAX_STAGED_BYTES - chips.reduce((sum, chip) => sum + chip.sizeBytes, 0);
4252
+ if (remainingBytes <= 0) return { ...s, statusText: refusalText("total"), error: "" };
4197
4253
  const raw = typeof arg === "string" ? arg : firstString(arg?.source, arg?.value);
4198
4254
  const source = raw.trim().toLowerCase() === "document" ? "document"
4199
4255
  : raw.trim().toLowerCase() === "camera" ? "camera" : "library";
@@ -4207,34 +4263,48 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
4207
4263
  const latest = (): AriaChatState => (commits !== at && committed ? committed : s);
4208
4264
  let picked: Awaited<ReturnType<AriaAttachmentPorts["pick"]>>;
4209
4265
  try {
4210
- picked = await ports.pick(source);
4211
- } catch (error) {
4212
- 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: "" };
4266
+ picked = await ports.pick(source, { maxItems: ARIA_MAX_ATTACHMENTS - chips.length, maxTotalBytes: remainingBytes });
4267
+ } catch {
4213
4268
  // A picker that could not open is not the owner's fault and there is no
4214
- // file to talk about; say nothing rather than something misleading.
4269
+ // file to talk about; clear stale attachment advice without erasing
4270
+ // a newer live status, draft or transcript.
4215
4271
  return withoutNotice(latest());
4216
4272
  }
4217
- if (!picked || !picked.dataUri) return latest(); // cancelled
4218
- const parsed = parseDataUri(picked.dataUri);
4219
- const mimeType = attachmentMime(picked.mimeType || parsed?.mimeType || "", picked.name);
4220
- // Re-checked after the await: other picks may have landed meanwhile.
4221
- const refused = refusalFor(parsed, mimeType, chips.length);
4222
- if (refused) return { ...latest(), statusText: refusalText(refused, mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES), error: "" };
4223
- chipSeq += 1;
4224
- const id = `att-${chipSeq}`;
4225
- chipBytes.set(id, picked.dataUri);
4226
- const chip = chipIn({
4227
- id,
4228
- name: imageFileName(mimeType, chipSeq, picked.name),
4229
- mimeType,
4230
- // Only a small image rides the state as its own thumbnail; see `chipBytes`.
4231
- thumbUrl: mimeType.startsWith("image/") && picked.dataUri.length <= THUMB_INLINE_MAX ? picked.dataUri : "",
4232
- }, "uploading", chipLabels());
4233
- chips = [...chips, chip];
4234
- startUpload(chip);
4235
- // Clear a refusal left over from an earlier pick: this one worked. The
4236
- // typing line is not touched, so picking mid-turn does not hide it.
4237
- return { ...withoutNotice(latest()), error: "" };
4273
+ if (!picked) return latest();
4274
+ const batch = Array.isArray(picked) ? picked : [picked];
4275
+ // A cancelled picker returns an empty batch: nothing was attached or refused, so the
4276
+ // advice already on screen (a size refusal, a failed upload) stays where it is.
4277
+ if (!batch.length) return latest();
4278
+ let notice = "";
4279
+ for (const file of batch) {
4280
+ const parsed = file.dataUri ? parseDataUri(file.dataUri) : null;
4281
+ const name = file.name ?? file.file?.name;
4282
+ const mimeType = attachmentMime(file.mimeType || file.file?.mimeType || parsed?.mimeType || "", name);
4283
+ const sizeBytes = file.file?.sizeBytes ?? parsed?.bytes;
4284
+ const refused = file.refusal || (sizeBytes === undefined ? "type"
4285
+ : refusalForFile(sizeBytes, mimeType, chips.length, chips.reduce((sum, c) => sum + c.sizeBytes, 0)));
4286
+ if (refused) {
4287
+ notice = refusalText(refused, mimeType);
4288
+ if (file.file && ports.discard) {
4289
+ try { await ports.discard(file.file); } catch { /* preserve the refusal if cleanup is unavailable */ }
4290
+ }
4291
+ continue;
4292
+ }
4293
+ chipSeq += 1;
4294
+ const id = `att-${chipSeq}`;
4295
+ if (file.dataUri) chipBytes.set(id, file.dataUri);
4296
+ const preview = file.previewUri || (mimeType.startsWith("image/") && file.dataUri && file.dataUri.length <= THUMB_INLINE_MAX ? file.dataUri : "");
4297
+ const chip = chipIn({
4298
+ id, name: imageFileName(mimeType, chipSeq, name), mimeType,
4299
+ thumbUrl: mimeType.startsWith("image/") && preview.length <= THUMB_INLINE_MAX ? preview : "",
4300
+ file: file.file ?? null, sizeBytes,
4301
+ }, "uploading", chipLabels());
4302
+ chips = [...chips, chip];
4303
+ startUpload(chip);
4304
+ }
4305
+ // Stage valid files in a partially accepted batch and explain every
4306
+ // refusal; never silently discard an over-limit selection.
4307
+ return { ...withoutNotice(latest()), statusText: notice || withoutNotice(latest()).statusText, error: "" };
4238
4308
  },
4239
4309
 
4240
4310
  suggestionsSettled: (s, arg) => {
@@ -4287,7 +4357,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
4287
4357
  retryAttachment: (s, arg) => {
4288
4358
  const id = chipIdOf(arg);
4289
4359
  const current = chips.find((c) => c.id === id);
4290
- if (!current || current.state !== "failed" || (!chipBytes.get(id) && !current.thumbUrl)) return s;
4360
+ if (!current || current.state !== "failed" || (!current.file && !parseDataUri(chipBytes.get(id) ?? current.thumbUrl))) return s;
4291
4361
  // A NEW id for the new attempt, in the same place in the strip. Keeping the
4292
4362
  // old one would let the first attempt's outcome, if it is still in the air,
4293
4363
  // settle this one.
@@ -0,0 +1,47 @@
1
+ /** Three upload workers, matching first-party Aria b2205e5f. A removed
2
+ * in-flight task retains its slot until its transport actually exits: ignoring
3
+ * an outcome is not cancellation of native buffers/network activity. */
4
+ export function createAriaUploadQueue<T>(concurrency = 3) {
5
+ if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error("invalid upload concurrency");
6
+ interface Task { id: string; cancelled: boolean; started: boolean; released: boolean; release?: () => void; run: () => Promise<T>; settle: (result: T | undefined, error?: unknown) => void }
7
+ const pending: Task[] = [];
8
+ const tasks = new Map<string, Task>();
9
+ let active = 0;
10
+ const release = (task: Task): void => {
11
+ if (task.released) return;
12
+ task.released = true;
13
+ try { task.release?.(); } catch { /* cleanup must not strand the queue */ }
14
+ };
15
+ const pump = (): void => {
16
+ while (active < concurrency && pending.length) {
17
+ const task = pending.shift()!;
18
+ if (task.cancelled) { release(task); continue; }
19
+ task.started = true;
20
+ active += 1;
21
+ void Promise.resolve().then(task.run).then(
22
+ result => { if (!task.cancelled) task.settle(result); },
23
+ error => { if (!task.cancelled) task.settle(undefined, error); },
24
+ ).catch(() => { /* delivery must not strand a transport slot */ }).finally(() => {
25
+ active -= 1;
26
+ release(task);
27
+ if (tasks.get(task.id) === task) tasks.delete(task.id);
28
+ pump();
29
+ });
30
+ }
31
+ };
32
+ return {
33
+ enqueue(id: string, run: () => Promise<T>, settle: Task["settle"], release?: () => void): void {
34
+ if (tasks.has(id)) return;
35
+ const task = { id, cancelled: false, started: false, released: false, release, run, settle };
36
+ tasks.set(id, task); pending.push(task); pump();
37
+ },
38
+ retain(ids: ReadonlySet<string>): void {
39
+ for (const [id, task] of tasks) {
40
+ if (ids.has(id)) continue;
41
+ task.cancelled = true;
42
+ if (!task.started) release(task);
43
+ tasks.delete(id);
44
+ }
45
+ },
46
+ };
47
+ }
@@ -26,11 +26,17 @@
26
26
 
27
27
  import { readAsyncJobState } from "../_shared/aria-async-jobs.js";
28
28
  import { defineService } from "@himalaya/state";
29
+ import { serializeForAgent } from "@himalaya/agent";
29
30
  import { open } from "@himalaya/open-url";
30
31
  import { writeText } from "@himalaya/clipboard";
31
32
  import { availability, recognizeOnce, stop as stopSpeech } from "@himalaya/speech";
32
- import { capturePhoto, pickDocument } from "@himalaya/camera";
33
- import { put } from "@himalaya/presigned-upload";
33
+ import { capturePhoto, prepareImage } from "@himalaya/camera";
34
+ import { pickDetailed as pickPhotos } from "@himalaya/photos";
35
+ import { pickDetailed as pickFiles } from "@himalaya/file-picker";
36
+ import { write as writeFile, remove as removeFile } from "@himalaya/files";
37
+ import { put, putFile } from "@himalaya/presigned-upload";
38
+ import { ARIA_MAX_ATTACHMENTS, ARIA_MAX_DOCUMENT_BYTES, ARIA_MAX_STAGED_BYTES, ARIA_IMAGE_TYPES, ARIA_DOCUMENT_PICKER_TYPES, attachmentMime, parseDataUri } from "../_shared/aria-attachments.js";
39
+ import type { FileHandle } from "@himalaya/sdk-types";
34
40
  import { alert } from "@himalaya/ui";
35
41
  import { auth } from "@himalaya/wix-platform";
36
42
  import {
@@ -42,6 +48,7 @@ import {
42
48
  type AriaChatState,
43
49
  type AriaDictationResult,
44
50
  type AriaPromptChip,
51
+ type AriaPickedAttachment,
45
52
  } from "../_shared/aria-chat.js";
46
53
  import { WIDGET_SUBTREE_SLOT } from "../_shared/aria-generic-widget.js";
47
54
  import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
@@ -265,6 +272,7 @@ const core = createAriaChat({
265
272
  attachFailed: () => copyParam("ownerAria.attach.failed", "Couldn't upload"),
266
273
  attachTooMany: (max) => copyTemplate("ownerAria.attach.tooMany", "You can attach up to {max} files.", { max }),
267
274
  attachUnsupported: () => copyParam("ownerAria.attach.unsupported", "That file type can't be attached."),
275
+ attachTotalTooLarge: (mb) => copyTemplate("ownerAria.attach.totalTooLarge", "Your attached files exceed {mb} MB in total. Remove a file.", { mb }),
268
276
  attachTooLarge: (mb) => copyTemplate("ownerAria.attach.tooLarge", "That file is too large to attach (up to {mb} MB).", { mb }),
269
277
  attachWaitUploading: () => copyParam("ownerAria.attach.waitUploading", "Wait for your file to finish uploading."),
270
278
  attachWaitFailed: () => copyParam("ownerAria.attach.waitFailed", "Retry or remove the file that didn't upload."),
@@ -311,25 +319,68 @@ const core = createAriaChat({
311
319
  deliver: (r) => { void svc.dictated(r); },
312
320
  available: async () => (await availability()).recognition !== "unavailable",
313
321
  },
314
- // Photo attachments. The two device capabilities live HERE rather than
315
- // in the shared core so that only a screen which declares them imports them:
316
- // `camera` for the picker (it returns the image already re-encoded as JPEG and
317
- // scaled to 2048 px on its longest side, which is also what keeps a phone
318
- // photo inside the inline upload bound) and `presigned_upload` for the bytes,
319
- // which go to a self-authorizing url with no Wix credential attached.
322
+ // Pick into the account file substrate, normalize raster images on the host,
323
+ // and upload handles without copying full file bytes into worker state.
320
324
  attachments: {
321
- pick: async (source) => {
325
+ persistDraft: state => serializeForAgent({ [SERVICE]: state }),
326
+ discard: file => removeFile(file),
327
+ pick: async (source, budget) => {
328
+ let files: FileHandle[];
329
+ const picked: AriaPickedAttachment[] = [];
322
330
  if (source === "document") {
323
- const file = await pickDocument();
324
- return file ? { dataUri: file.uri, name: file.name, mimeType: file.mime } : null;
331
+ const result = await pickFiles({ types: [...ARIA_IMAGE_TYPES, ...ARIA_DOCUMENT_PICKER_TYPES], multiple: true,
332
+ maxBytes: ARIA_MAX_DOCUMENT_BYTES, maxImageBytes: 32 * 1024 * 1024,
333
+ maxItems: budget?.maxItems ?? ARIA_MAX_ATTACHMENTS, maxTotalBytes: budget?.maxTotalBytes ?? ARIA_MAX_STAGED_BYTES });
334
+ files = result.items;
335
+ picked.push(...result.rejected.map(file => ({ name: file.name, mimeType: file.mimeType, refusal: file.reason })));
325
336
  }
326
- const asset = await capturePhoto({ source, maxEdgePixels: 2048 });
327
- return asset ? { dataUri: asset.uri, mimeType: asset.mime } : null;
337
+ else if (source === "library") {
338
+ const result = await pickPhotos({ limit: budget?.maxItems ?? ARIA_MAX_ATTACHMENTS, type: "images",
339
+ maxBytes: 32 * 1024 * 1024, maxTotalBytes: budget?.maxTotalBytes ?? ARIA_MAX_STAGED_BYTES });
340
+ files = result.items;
341
+ picked.push(...result.rejected.map(file => ({ name: file.name, mimeType: file.mimeType, refusal: file.reason })));
342
+ }
343
+ else {
344
+ const asset = await capturePhoto({ source: "camera", maxEdgePixels: 2048 });
345
+ if (!asset) return [];
346
+ const parsed = parseDataUri(asset.uri);
347
+ if (!parsed) throw new Error("Camera returned an unsupported image");
348
+ const path = `captures/${Date.now()}-${Math.random().toString(36).slice(2)}-photo.jpg`;
349
+ files = [await writeFile(path, { base64: parsed.base64 }, { mimeType: asset.mime })];
350
+ }
351
+ // A camera capture is stored under a unique internal path; it has no name of its own to
352
+ // show or send, so `name: ""` asks the chat core for its `photo-N.jpg` fallback instead.
353
+ const shownName = source === "camera" ? { name: "" } : {};
354
+ for (const original of files) {
355
+ const mimeType = attachmentMime(original.mimeType, original.name);
356
+ const file = { ...original, mimeType };
357
+ if (ARIA_IMAGE_TYPES.includes(mimeType)) {
358
+ try {
359
+ const prepared = await prepareImage(file);
360
+ picked.push({ file: prepared.file, previewUri: prepared.previewUri, ...shownName });
361
+ // The codec may write a replacement. Keep the prepared upload/retry
362
+ // handle, and release only the now-unused original import.
363
+ if (prepared.file.uri !== file.uri) {
364
+ try { await removeFile(file); } catch { /* best-effort import cleanup */ }
365
+ }
366
+ } catch (error) {
367
+ // A rejected codec/oversize image still reaches policy as an
368
+ // unsupported entry, so the owner gets an actionable refusal.
369
+ try { await removeFile(file); } catch { /* keep the policy refusal on cleanup failure */ }
370
+ picked.push({ name: file.name, mimeType, refusal: /size limit|oversized/i.test(String(error)) ? "size" : "type" });
371
+ }
372
+ } else picked.push({ file, ...shownName });
373
+ }
374
+ return picked;
328
375
  },
329
376
  put: async (url, base64, contentType) => {
330
377
  const r = await put(url, { base64 }, { contentType, method: "PUT" });
331
378
  return { ok: r.ok, status: r.status };
332
379
  },
380
+ putFile: async (url, file, contentType) => {
381
+ const r = await putFile(url, file, { contentType, method: "PUT" });
382
+ return { ok: r.ok, status: r.status };
383
+ },
333
384
  // A NEW action on the service, not a return value: the upload outlives the
334
385
  // `attach` that started it. `svc` is read at call time, long after it exists.
335
386
  settled: (outcome) => { void svc.attachmentSettled(outcome); },
@@ -37,7 +37,7 @@ export default defineApp({
37
37
  actions: "http://localhost:8190/v1/actions",
38
38
  sync: "http://localhost:8190/v1/sync",
39
39
  },
40
- capabilities: ["live_activity","push","share","secure_storage","open_url","local_database","clipboard","ui","speech","camera","presigned_upload","app_info","biometrics","wix_platform"],
40
+ capabilities: ["live_activity","push","share","secure_storage","open_url","local_database","clipboard","ui","speech","camera","photos","file_picker","files","presigned_upload","app_info","biometrics","wix_platform"],
41
41
  designTokensPath: "./tokens.json",
42
42
  navigation: {
43
43
  "navKind": "responsive",
@@ -61284,6 +61284,9 @@ const SCREENS = runtimeSetting({
61284
61284
  "ui",
61285
61285
  "speech",
61286
61286
  "camera",
61287
+ "photos",
61288
+ "file_picker",
61289
+ "files",
61287
61290
  "presigned_upload",
61288
61291
  "stream",
61289
61292
  "wix_platform"