@agent-native/core 0.182.1-nightly-20260917183518 → 0.182.1

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 (56) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +6 -0
  3. package/corpus/templates/calendar/CHANGELOG.md +1 -0
  4. package/corpus/templates/clips/CHANGELOG.md +7 -0
  5. package/corpus/templates/clips/app/components/library/sort-menu.tsx +2 -2
  6. package/corpus/templates/clips/changelog/2026-09-17-clips-waits-for-authentication-before-showing-the-recording-.md +6 -0
  7. package/corpus/templates/clips/changelog/2026-09-17-the-clips-sort-control-keeps-its-keyboard-focus-ring-visible.md +6 -0
  8. package/corpus/templates/clips/chrome-extension/src/popup.html +1 -1
  9. package/corpus/templates/clips/chrome-extension/src/popup.ts +30 -21
  10. package/corpus/templates/content/CHANGELOG.md +7 -0
  11. package/corpus/templates/content/actions/resolve-preview-document-draft.ts +181 -16
  12. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +28 -15
  13. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +11 -0
  14. package/corpus/templates/content/app/components/editor/document-save-rebase.ts +13 -10
  15. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +80 -0
  16. package/corpus/templates/content/changelog/2026-09-16-recovery-choices-stay-attached-to-the-draft-you-reviewed.md +6 -0
  17. package/corpus/templates/design/CHANGELOG.md +29 -0
  18. package/corpus/templates/design/actions/import-design-source.ts +42 -2
  19. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +26 -15
  20. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +83 -1
  21. package/corpus/templates/design/app/i18n-data.ts +3 -1
  22. package/corpus/templates/design/app/lib/design-file-upload.ts +6 -4
  23. package/corpus/templates/design/app/lib/design-import.ts +2 -0
  24. package/corpus/templates/design/app/lib/fig-client-import.ts +207 -49
  25. package/corpus/templates/design/changelog/2026-09-17-large-fig-files-import-directly-in-the-browser-without-the-o.md +6 -0
  26. package/corpus/templates/design/server/lib/fig-file-decoder.ts +31 -8
  27. package/corpus/templates/design/server/lib/import-design-files.ts +188 -75
  28. package/corpus/templates/design/server/plugins/db.ts +16 -7
  29. package/corpus/templates/design/shared/fig-to-frames.ts +55 -25
  30. package/corpus/templates/factory/CHANGELOG.md +6 -0
  31. package/corpus/templates/mail/CHANGELOG.md +6 -0
  32. package/corpus/templates/slides/CHANGELOG.md +12 -0
  33. package/corpus/templates/slides/app/components/editor/AssetLibraryPanel.tsx +1 -1
  34. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +1 -2
  35. package/corpus/templates/slides/app/hooks/use-agent-generating.ts +82 -2
  36. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +9 -2
  37. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +1 -1
  38. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
  39. package/corpus/templates/slides/server/handlers/assets.ts +134 -8
  40. package/corpus/templates/slides/server/handlers/uploads.ts +23 -10
  41. package/corpus/templates/slides/shared/upload-types.ts +1 -0
  42. package/dist/application-state/store.d.ts +9 -0
  43. package/dist/application-state/store.js +18 -0
  44. package/dist/automation/index.d.ts +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/deploy/build.js +7 -0
  47. package/dist/file-upload/actions/upload-image.d.ts +41 -10
  48. package/dist/file-upload/actions/upload-image.js +399 -19
  49. package/dist/jobs/scheduler.js +11 -0
  50. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  51. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  52. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  53. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  54. package/dist/resources/handlers.d.ts +1 -1
  55. package/dist/secrets/routes.d.ts +3 -3
  56. package/package.json +4 -4
@@ -6,8 +6,10 @@
6
6
  * entirely: a Netlify function request is capped at ~6MB while real `.fig`
7
7
  * files run to tens of megabytes, which is why the server route has to chunk.
8
8
  * Nothing large crosses the network on this path — embedded images go up one
9
- * at a time through `upload-image`, and each frame's HTML is its own request,
10
- * so no single request approaches the cap however big the file was.
9
+ * at a time through `upload-image`, and each frame's HTML is its own request.
10
+ * The individual image budget below keeps base64 action requests under the
11
+ * serverless transport cap while the `.fig` container itself can exceed the
12
+ * old 50 MB server upload ceiling up to a finite browser safety limit.
11
13
  *
12
14
  * The server route stays for callers that are not a browser (the agent, A2A,
13
15
  * the fidelity harness) and as the fallback when decoding here fails.
@@ -19,13 +21,20 @@ import { decodeFig } from "../../server/lib/fig-file-decoder.js";
19
21
  import type { DecodedFig } from "../../server/lib/fig-file-decoder.js";
20
22
  import { bytesToBase64 } from "../../shared/fig-bytes.js";
21
23
  import {
24
+ assertEmbeddedImageBudget,
22
25
  convertDecodedFigToEditableHtml,
26
+ MAX_FIG_FRAME_HTML_BYTES,
23
27
  inspectDecodedFig,
24
28
  shouldWarnForFigImport,
25
29
  type FigImportSummary,
26
30
  } from "../../shared/fig-to-frames.js";
27
31
  import type { ImportResult } from "./design-import";
28
32
 
33
+ /** Base64 plus action JSON must stay below the serverless request ceiling. */
34
+ export const MAX_CLIENT_IMAGE_BYTES = 4 * 1024 * 1024;
35
+ /** Finite browser allocation ceiling; this is far above the old 50 MB upload cap. */
36
+ export const MAX_CLIENT_FIG_BYTES = 512 * 1024 * 1024;
37
+
29
38
  export interface FigClientImportProgress {
30
39
  phase: "decoding" | "images" | "saving";
31
40
  /** 0-1 within the current phase, when it is countable. */
@@ -40,6 +49,16 @@ export interface FigClientImportOptions {
40
49
  onProgress?: (progress: FigClientImportProgress) => void;
41
50
  }
42
51
 
52
+ export class FigClientImportError extends Error {
53
+ constructor(
54
+ message: string,
55
+ readonly remoteMutationStarted: boolean,
56
+ ) {
57
+ super(message);
58
+ this.name = "FigClientImportError";
59
+ }
60
+ }
61
+
43
62
  export interface PreparedFigImport {
44
63
  file: File;
45
64
  decoded: DecodedFig;
@@ -56,6 +75,28 @@ function mimeForExt(ext: string): string {
56
75
  return "application/octet-stream";
57
76
  }
58
77
 
78
+ function browserImportId(): string {
79
+ return (
80
+ globalThis.crypto?.randomUUID?.() ??
81
+ `fig-${Date.now()}-${Math.random().toString(36).slice(2)}`
82
+ );
83
+ }
84
+
85
+ async function callWithOneRetry<T>(
86
+ action: string,
87
+ input: Record<string, unknown>,
88
+ ): Promise<T> {
89
+ try {
90
+ return (await callAction(action, input)) as T;
91
+ } catch (firstError) {
92
+ try {
93
+ return (await callAction(action, input)) as T;
94
+ } catch {
95
+ throw firstError;
96
+ }
97
+ }
98
+ }
99
+
59
100
  /**
60
101
  * Decode, upload the embedded images one request each, then save each frame in
61
102
  * its own request. Returns the same shape the server route returns so the
@@ -64,59 +105,168 @@ function mimeForExt(ext: string): string {
64
105
  export async function importFigInBrowser(
65
106
  options: FigClientImportOptions,
66
107
  ): Promise<ImportResult> {
67
- const { designId, file, onProgress } = options;
108
+ const { designId, file, onProgress, selection } = options;
109
+ if (file.size > MAX_CLIENT_FIG_BYTES) {
110
+ throw new Error(
111
+ `.fig file is too large for browser import (max ${MAX_CLIENT_FIG_BYTES / 1024 / 1024} MB).`,
112
+ );
113
+ }
68
114
  let decoded = options.decoded;
69
115
  if (!decoded) {
70
116
  onProgress?.({ phase: "decoding" });
71
- decoded = decodeFig(new Uint8Array(await file.arrayBuffer()));
117
+ const bytes = new Uint8Array(await file.arrayBuffer());
118
+ // The file never crosses the network on this path. Keep the decoder's
119
+ // decompression, node, image, and generated-HTML budgets, but remove the
120
+ // server-only raw upload ceiling.
121
+ decoded = decodeFig(bytes, { maxFileBytes: MAX_CLIENT_FIG_BYTES });
72
122
  }
73
-
123
+ assertEmbeddedImageBudget(decoded.images);
124
+ const oversizedImages = decoded.images.filter(
125
+ (image) => image.bytes.byteLength > MAX_CLIENT_IMAGE_BYTES,
126
+ );
127
+ const decodedForBrowserImport =
128
+ oversizedImages.length > 0
129
+ ? {
130
+ ...decoded,
131
+ images: decoded.images.filter(
132
+ (image) => image.bytes.byteLength <= MAX_CLIENT_IMAGE_BYTES,
133
+ ),
134
+ }
135
+ : decoded;
136
+ let remoteMutationStarted = false;
74
137
  let uploaded = 0;
75
- const total = decoded.images.length;
76
- const converted = await convertDecodedFigToEditableHtml(decoded, {
77
- originalName: file.name,
78
- // The upload action resolves the owner from the session; this value is only
79
- // read by the server-side uploader this path replaces.
80
- ownerEmail: "",
81
- // The action wraps the document; nothing to do here.
82
- normalizeHtml: (content: string) => content,
83
- selection: options.selection,
84
- uploader: async ({ data, filename, mimeType }) => {
85
- const url = (await callAction("upload-image", {
86
- data: `data:${mimeType ?? mimeForExt("")};base64,${bytesToBase64(
87
- data instanceof Uint8Array
88
- ? data
89
- : new Uint8Array(data as ArrayBuffer),
90
- )}`,
91
- filename,
92
- })) as { url?: string };
93
- uploaded += 1;
94
- onProgress?.({ phase: "images", ratio: total ? uploaded / total : 1 });
95
- // A null result means storage is unavailable; the converter rejects the
96
- // import rather than persisting frames with missing images.
97
- return url?.url ? { url: url.url } : null;
98
- },
99
- });
138
+ const total = decodedForBrowserImport.images.length;
139
+ const importId = browserImportId();
140
+ let converted;
141
+ try {
142
+ converted = await convertDecodedFigToEditableHtml(decodedForBrowserImport, {
143
+ originalName: file.name,
144
+ // The upload action resolves the owner from the session; this value is only
145
+ // read by the server-side uploader this path replaces.
146
+ ownerEmail: "",
147
+ // The action wraps the document; nothing to do here.
148
+ normalizeHtml: (content: string) => content,
149
+ maxFrameHtmlBytes: MAX_FIG_FRAME_HTML_BYTES,
150
+ selection,
151
+ uploader: async ({ data, filename, mimeType }) => {
152
+ remoteMutationStarted = true;
153
+ const idempotencyKey = `${importId}:${filename}`;
154
+ const uploadInput = {
155
+ data: `data:${mimeType ?? mimeForExt("")};base64,${bytesToBase64(
156
+ data instanceof Uint8Array
157
+ ? data
158
+ : new Uint8Array(data as ArrayBuffer),
159
+ )}`,
160
+ filename,
161
+ idempotencyKey,
162
+ };
163
+ const url = await callWithOneRetry<{ url?: string }>(
164
+ "upload-image",
165
+ uploadInput,
166
+ );
167
+ uploaded += 1;
168
+ onProgress?.({
169
+ phase: "images",
170
+ ratio: total ? uploaded / total : 1,
171
+ });
172
+ // A null result means storage is unavailable; the converter rejects the
173
+ // import rather than persisting frames with missing images.
174
+ if (!url?.url) return null;
175
+ return {
176
+ url: url.url,
177
+ cleanup: async () => {
178
+ const result = await callWithOneRetry<{
179
+ alreadyMissing?: boolean;
180
+ committed?: boolean;
181
+ deleted?: boolean;
182
+ }>("upload-image", { idempotencyKey, cleanup: "delete" });
183
+ return (
184
+ result.deleted === true ||
185
+ result.alreadyMissing === true ||
186
+ result.committed === true
187
+ );
188
+ },
189
+ finalize: async () => {
190
+ const result = await callWithOneRetry<{
191
+ alreadyMissing?: boolean;
192
+ released?: boolean;
193
+ }>("upload-image", { idempotencyKey, cleanup: "release" });
194
+ return result.released === true || result.alreadyMissing === true;
195
+ },
196
+ };
197
+ },
198
+ });
199
+ } catch (error) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ throw new FigClientImportError(message, remoteMutationStarted);
202
+ }
100
203
 
101
- const saved: ImportResult = { files: [], warnings: [...converted.warnings] };
204
+ const saved: ImportResult = {
205
+ files: [],
206
+ warnings: converted.warnings,
207
+ skippedEmbeddedImageCount: oversizedImages.length || undefined,
208
+ };
102
209
  let index = 0;
103
- for (const frame of converted.files) {
104
- onProgress?.({
105
- phase: "saving",
106
- ratio: converted.files.length ? index / converted.files.length : 1,
107
- });
108
- const result = (await callAction("import-design-source", {
109
- designId,
110
- sourceType: "fig-frame",
111
- content: frame.content,
112
- originalName: frame.filename,
113
- frameTitle: frame.preferredFrame?.title,
114
- frameWidth: frame.preferredFrame?.width,
115
- frameHeight: frame.preferredFrame?.height,
116
- })) as ImportResult;
117
- saved.designId = result.designId ?? saved.designId;
118
- saved.files = [...(saved.files ?? []), ...(result.files ?? [])];
119
- index += 1;
210
+ const savedFileIds: string[] = [];
211
+ try {
212
+ for (const [frameIndex, frame] of converted.files.entries()) {
213
+ onProgress?.({
214
+ phase: "saving",
215
+ ratio: converted.files.length ? index / converted.files.length : 1,
216
+ });
217
+ // Do not fall back after this point: the action may have committed even
218
+ // if the browser lost its response.
219
+ remoteMutationStarted = true;
220
+ const result = await callWithOneRetry<ImportResult>(
221
+ "import-design-source",
222
+ {
223
+ designId,
224
+ sourceType: "fig-frame",
225
+ content: frame.content,
226
+ originalName: frame.filename,
227
+ frameTitle: frame.preferredFrame?.title,
228
+ frameWidth: frame.preferredFrame?.width,
229
+ frameHeight: frame.preferredFrame?.height,
230
+ clientImportId: `${importId}:frame:${frameIndex}`,
231
+ clientImportBatchId: importId,
232
+ clientImportFinalFrame: frameIndex === converted.files.length - 1,
233
+ },
234
+ );
235
+ if (result.error) throw new Error(result.error);
236
+ saved.designId = result.designId ?? saved.designId;
237
+ saved.files = [...(saved.files ?? []), ...(result.files ?? [])];
238
+ savedFileIds.push(...(result.files ?? []).map((file) => file.id));
239
+ index += 1;
240
+ }
241
+ // Receipt release is best-effort after all frames are saved. A partial
242
+ // release must not roll back valid frames; the server-side receipt sweep
243
+ // expires abandoned staged receipts.
244
+ await converted.finalize?.();
245
+ } catch (error) {
246
+ const cleanup = await Promise.allSettled(
247
+ savedFileIds.map((id) =>
248
+ callAction("delete-file", { id, allowLockedLayers: true }),
249
+ ),
250
+ );
251
+ const cleanupFailures = cleanup.filter(
252
+ (result) => result.status === "rejected",
253
+ ).length;
254
+ const imageCleanupFailures = (await converted.cleanup?.()) ?? 0;
255
+ const message = error instanceof Error ? error.message : String(error);
256
+ const cleanupMessage = [
257
+ cleanupFailures > 0
258
+ ? `Cleanup failed for ${cleanupFailures} partially imported screen${cleanupFailures === 1 ? "" : "s"}.`
259
+ : "",
260
+ imageCleanupFailures > 0
261
+ ? `Storage cleanup failed for ${imageCleanupFailures} uploaded image${imageCleanupFailures === 1 ? "" : "s"}.`
262
+ : "",
263
+ ]
264
+ .filter(Boolean)
265
+ .join(" ");
266
+ throw new FigClientImportError(
267
+ cleanupMessage ? `${message} ${cleanupMessage}` : message,
268
+ remoteMutationStarted,
269
+ );
120
270
  }
121
271
  return {
122
272
  ...saved,
@@ -128,7 +278,15 @@ export async function prepareFigImport(
128
278
  file: File,
129
279
  onProgress?: (progress: FigClientImportProgress) => void,
130
280
  ): Promise<PreparedFigImport> {
281
+ if (file.size > MAX_CLIENT_FIG_BYTES) {
282
+ throw new Error(
283
+ `.fig file is too large for browser import (max ${MAX_CLIENT_FIG_BYTES / 1024 / 1024} MB).`,
284
+ );
285
+ }
131
286
  onProgress?.({ phase: "decoding" });
132
- const decoded = decodeFig(new Uint8Array(await file.arrayBuffer()));
287
+ const decoded = decodeFig(new Uint8Array(await file.arrayBuffer()), {
288
+ maxFileBytes: MAX_CLIENT_FIG_BYTES,
289
+ });
290
+ assertEmbeddedImageBudget(decoded.images);
133
291
  return { file, decoded, summary: inspectDecodedFig(decoded) };
134
292
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-17
4
+ ---
5
+
6
+ Large .fig files import directly in the browser without the old upload ceiling
@@ -75,6 +75,17 @@ export interface DecodedFig {
75
75
  thumbnail: Uint8Array | null;
76
76
  }
77
77
 
78
+ export interface DecodeFigOptions {
79
+ /** `null` removes only the raw-file cap for browser-local decoding. */
80
+ maxFileBytes?: number | null;
81
+ }
82
+
83
+ function maxFileBytes(options?: DecodeFigOptions): number | null {
84
+ return options?.maxFileBytes === undefined
85
+ ? MAX_FIG_FILE_BYTES
86
+ : options.maxFileBytes;
87
+ }
88
+
78
89
  function sha1(buf: Uint8Array): string {
79
90
  return sha1Hex(buf);
80
91
  }
@@ -205,9 +216,15 @@ function decompressChunk(buf: Uint8Array): Uint8Array {
205
216
  return checkDecompressedSize(buf.slice());
206
217
  }
207
218
 
208
- export function decodeKiwiContainer(file: Uint8Array): DecodedFigKiwi {
209
- if (file.length > MAX_FIG_FILE_BYTES) {
210
- throw new Error(".fig file is too large (max 50 MB).");
219
+ export function decodeKiwiContainer(
220
+ file: Uint8Array,
221
+ options?: DecodeFigOptions,
222
+ ): DecodedFigKiwi {
223
+ const fileLimit = maxFileBytes(options);
224
+ if (fileLimit !== null && file.length > fileLimit) {
225
+ throw new Error(
226
+ `.fig file is too large (max ${Math.round(fileLimit / 1024 / 1024)} MB).`,
227
+ );
211
228
  }
212
229
  if (
213
230
  !bytesEqual(file.subarray(0, 8), FIG_KIWI_MAGIC) &&
@@ -824,9 +841,15 @@ function assertSafeBinarySchemaShape(schemaBuf: Uint8Array): void {
824
841
 
825
842
  // Handles both modern fig-kiwi files and legacy zip-format archives.
826
843
  // `document` is null if kiwi decoding failed.
827
- export function decodeFig(file: Uint8Array): DecodedFig {
828
- if (file.length > MAX_FIG_FILE_BYTES) {
829
- throw new Error(".fig file is too large (max 50 MB).");
844
+ export function decodeFig(
845
+ file: Uint8Array,
846
+ options?: DecodeFigOptions,
847
+ ): DecodedFig {
848
+ const fileLimit = maxFileBytes(options);
849
+ if (fileLimit !== null && file.length > fileLimit) {
850
+ throw new Error(
851
+ `.fig file is too large (max ${Math.round(fileLimit / 1024 / 1024)} MB).`,
852
+ );
830
853
  }
831
854
  if (isZip(file)) {
832
855
  const entries = readZip(file);
@@ -837,7 +860,7 @@ export function decodeFig(file: Uint8Array): DecodedFig {
837
860
  let version: number | undefined;
838
861
  let extraBlobs: Uint8Array[] = [];
839
862
  if (!canvasEntry) throw new Error(".fig zip is missing canvas.fig.");
840
- const inner = decodeKiwiContainer(canvasEntry.data);
863
+ const inner = decodeKiwiContainer(canvasEntry.data, options);
841
864
  version = inner.version;
842
865
  extraBlobs = inner.blobs;
843
866
  const kiwiResult = decodeKiwiDocument(inner.schema, inner.document);
@@ -872,7 +895,7 @@ export function decodeFig(file: Uint8Array): DecodedFig {
872
895
  };
873
896
  }
874
897
 
875
- const decoded = decodeKiwiContainer(file);
898
+ const decoded = decodeKiwiContainer(file, options);
876
899
  const kiwiResult = decodeKiwiDocument(decoded.schema, decoded.document);
877
900
  const images = collectImagesFromBlobs(decoded.blobs);
878
901
  const thumbnail = findThumbnail(decoded.document, decoded.blobs);