@agent-native/core 0.118.1 → 0.119.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 (184) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/a2a-protocol.mdx +15 -0
  4. package/corpus/core/docs/content/template-chat.mdx +1 -1
  5. package/corpus/core/docs/content/template-plan.mdx +3 -3
  6. package/corpus/core/package.json +1 -1
  7. package/corpus/core/src/a2a/activity.ts +371 -0
  8. package/corpus/core/src/a2a/client.ts +3 -1
  9. package/corpus/core/src/a2a/index.ts +21 -0
  10. package/corpus/core/src/a2a/types.ts +8 -0
  11. package/corpus/core/src/agent/model-config.ts +19 -23
  12. package/corpus/core/src/agent/production-agent.ts +167 -100
  13. package/corpus/core/src/agent/types.ts +16 -1
  14. package/corpus/core/src/client/chat/tool-call-display.tsx +190 -35
  15. package/corpus/core/src/client/chat-model-groups.ts +67 -27
  16. package/corpus/core/src/client/sharing/ShareButton.tsx +1 -27
  17. package/corpus/core/src/client/sharing/ShareDialog.tsx +0 -16
  18. package/corpus/core/src/client/sharing/useShareButtonController.ts +0 -7
  19. package/corpus/core/src/client/sharing/useShareDialogController.ts +0 -2
  20. package/corpus/core/src/client/sse-event-processor.ts +85 -1
  21. package/corpus/core/src/ingestion/figma-node-to-html.ts +20 -99
  22. package/corpus/core/src/ingestion/figma-paint-math.ts +371 -0
  23. package/corpus/core/src/ingestion/index.ts +20 -0
  24. package/corpus/core/src/localization/default-messages.ts +6 -0
  25. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -18
  26. package/corpus/core/src/scripts/call-agent.ts +49 -6
  27. package/corpus/core/src/server/agent-chat-plugin.ts +50 -10
  28. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -0
  29. package/corpus/templates/assets/app/routes/library.tsx +17 -9
  30. package/corpus/templates/clips/changelog/2026-07-23-fixed-recordings-failing-to-save-with-a-cancelled-or-cleanup.md +6 -0
  31. package/corpus/templates/clips/server/plugins/db.ts +9 -1
  32. package/corpus/templates/design/AGENTS.md +18 -5
  33. package/corpus/templates/design/FIGMA_INTEROPERABILITY.md +25 -12
  34. package/corpus/templates/design/actions/hydrate-figma-paste-images.ts +115 -0
  35. package/corpus/templates/design/actions/import-figma-clipboard.ts +83 -18
  36. package/corpus/templates/design/actions/import-figma-frame.ts +34 -21
  37. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +28 -1
  38. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +106 -11
  39. package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +273 -0
  40. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -7
  41. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +57 -9
  42. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +10 -0
  43. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +8 -1
  44. package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
  45. package/corpus/templates/design/app/i18n-data.ts +150 -2
  46. package/corpus/templates/design/app/lib/design-file-upload.ts +85 -0
  47. package/corpus/templates/design/app/lib/design-import.ts +19 -1
  48. package/corpus/templates/design/app/lib/figma-clipboard.ts +110 -10
  49. package/corpus/templates/design/changelog/2026-07-20-paste-figma-frames-directly-onto-the-canvas-no-token-needed.md +6 -0
  50. package/corpus/templates/design/changelog/2026-07-21-fig-upload-frame-url-and-rate-limit-errors.md +5 -0
  51. package/corpus/templates/design/changelog/2026-07-22-connecting-a-figma-token-now-actually-fills-in-a-pasted-desi.md +6 -0
  52. package/corpus/templates/design/changelog/2026-07-22-figma-line-arrow-vectors-and-stroked-icons-now-render-instea.md +6 -0
  53. package/corpus/templates/design/changelog/2026-07-22-fill-a-no-token-figma-paste-s-images-by-dropping-the-origina.md +6 -0
  54. package/corpus/templates/design/changelog/2026-07-22-imported-and-pasted-figma-frames-now-render-faithfully-fixed.md +6 -0
  55. package/corpus/templates/design/server/handlers/import-design-file.ts +50 -0
  56. package/corpus/templates/design/server/lib/fig-file-decoder.ts +90 -49
  57. package/corpus/templates/design/server/lib/fig-file-import.ts +19 -5
  58. package/corpus/templates/design/server/lib/fig-file-to-html.ts +583 -78
  59. package/corpus/templates/design/server/lib/figma-clipboard-local-decode.ts +238 -0
  60. package/corpus/templates/design/server/lib/figma-image-hydration.ts +389 -0
  61. package/corpus/templates/design/server/lib/figma-node-import.ts +136 -36
  62. package/corpus/templates/design/server/register-secrets.ts +14 -2
  63. package/dist/a2a/activity.d.ts +50 -0
  64. package/dist/a2a/activity.d.ts.map +1 -0
  65. package/dist/a2a/activity.js +236 -0
  66. package/dist/a2a/activity.js.map +1 -0
  67. package/dist/a2a/client.d.ts +2 -0
  68. package/dist/a2a/client.d.ts.map +1 -1
  69. package/dist/a2a/client.js +1 -1
  70. package/dist/a2a/client.js.map +1 -1
  71. package/dist/a2a/index.d.ts +2 -1
  72. package/dist/a2a/index.d.ts.map +1 -1
  73. package/dist/a2a/index.js +1 -0
  74. package/dist/a2a/index.js.map +1 -1
  75. package/dist/a2a/types.d.ts +1 -0
  76. package/dist/a2a/types.d.ts.map +1 -1
  77. package/dist/a2a/types.js.map +1 -1
  78. package/dist/agent/engine/anthropic-engine.d.ts +1 -1
  79. package/dist/agent/engine/builder-engine.d.ts +2 -2
  80. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  81. package/dist/agent/model-config.d.ts +16 -16
  82. package/dist/agent/model-config.d.ts.map +1 -1
  83. package/dist/agent/model-config.js +19 -23
  84. package/dist/agent/model-config.js.map +1 -1
  85. package/dist/agent/production-agent.d.ts +26 -0
  86. package/dist/agent/production-agent.d.ts.map +1 -1
  87. package/dist/agent/production-agent.js +137 -80
  88. package/dist/agent/production-agent.js.map +1 -1
  89. package/dist/agent/types.d.ts +10 -0
  90. package/dist/agent/types.d.ts.map +1 -1
  91. package/dist/agent/types.js.map +1 -1
  92. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  93. package/dist/client/chat/tool-call-display.js +63 -14
  94. package/dist/client/chat/tool-call-display.js.map +1 -1
  95. package/dist/client/chat-model-groups.d.ts.map +1 -1
  96. package/dist/client/chat-model-groups.js +44 -21
  97. package/dist/client/chat-model-groups.js.map +1 -1
  98. package/dist/client/sharing/ShareButton.d.ts +1 -1
  99. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  100. package/dist/client/sharing/ShareButton.js +3 -4
  101. package/dist/client/sharing/ShareButton.js.map +1 -1
  102. package/dist/client/sharing/ShareDialog.d.ts.map +1 -1
  103. package/dist/client/sharing/ShareDialog.js +1 -2
  104. package/dist/client/sharing/ShareDialog.js.map +1 -1
  105. package/dist/client/sharing/useShareButtonController.d.ts +0 -1
  106. package/dist/client/sharing/useShareButtonController.d.ts.map +1 -1
  107. package/dist/client/sharing/useShareButtonController.js +0 -6
  108. package/dist/client/sharing/useShareButtonController.js.map +1 -1
  109. package/dist/client/sharing/useShareDialogController.d.ts +0 -1
  110. package/dist/client/sharing/useShareDialogController.d.ts.map +1 -1
  111. package/dist/client/sharing/useShareDialogController.js +0 -1
  112. package/dist/client/sharing/useShareDialogController.js.map +1 -1
  113. package/dist/client/sse-event-processor.d.ts +12 -0
  114. package/dist/client/sse-event-processor.d.ts.map +1 -1
  115. package/dist/client/sse-event-processor.js +63 -1
  116. package/dist/client/sse-event-processor.js.map +1 -1
  117. package/dist/collab/routes.d.ts +1 -1
  118. package/dist/ingestion/figma-node-to-html.d.ts.map +1 -1
  119. package/dist/ingestion/figma-node-to-html.js +14 -88
  120. package/dist/ingestion/figma-node-to-html.js.map +1 -1
  121. package/dist/ingestion/figma-paint-math.d.ts +157 -0
  122. package/dist/ingestion/figma-paint-math.d.ts.map +1 -0
  123. package/dist/ingestion/figma-paint-math.js +274 -0
  124. package/dist/ingestion/figma-paint-math.js.map +1 -0
  125. package/dist/ingestion/index.d.ts +1 -0
  126. package/dist/ingestion/index.d.ts.map +1 -1
  127. package/dist/ingestion/index.js +1 -0
  128. package/dist/ingestion/index.js.map +1 -1
  129. package/dist/localization/default-messages.d.ts +6 -0
  130. package/dist/localization/default-messages.d.ts.map +1 -1
  131. package/dist/localization/default-messages.js +6 -0
  132. package/dist/localization/default-messages.js.map +1 -1
  133. package/dist/notifications/routes.d.ts +2 -2
  134. package/dist/observability/routes.d.ts +5 -5
  135. package/dist/progress/routes.d.ts +1 -1
  136. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  137. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  138. package/dist/resources/handlers.d.ts +1 -1
  139. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  140. package/dist/scripts/agent-engines/list-agent-engines.js +1 -18
  141. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  142. package/dist/scripts/call-agent.d.ts.map +1 -1
  143. package/dist/scripts/call-agent.js +49 -6
  144. package/dist/scripts/call-agent.js.map +1 -1
  145. package/dist/secrets/routes.d.ts +3 -3
  146. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  147. package/dist/server/agent-chat-plugin.js +38 -10
  148. package/dist/server/agent-chat-plugin.js.map +1 -1
  149. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  150. package/dist/server/realtime-token.d.ts +1 -1
  151. package/dist/server/transcribe-voice.d.ts +1 -1
  152. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -0
  153. package/docs/content/a2a-protocol.mdx +15 -0
  154. package/docs/content/template-chat.mdx +1 -1
  155. package/docs/content/template-plan.mdx +3 -3
  156. package/package.json +1 -1
  157. package/src/a2a/activity.ts +371 -0
  158. package/src/a2a/client.ts +3 -1
  159. package/src/a2a/index.ts +21 -0
  160. package/src/a2a/types.ts +8 -0
  161. package/src/agent/model-config.ts +19 -23
  162. package/src/agent/production-agent.ts +167 -100
  163. package/src/agent/types.ts +16 -1
  164. package/src/client/chat/tool-call-display.tsx +190 -35
  165. package/src/client/chat-model-groups.ts +67 -27
  166. package/src/client/sharing/ShareButton.tsx +1 -27
  167. package/src/client/sharing/ShareDialog.tsx +0 -16
  168. package/src/client/sharing/useShareButtonController.ts +0 -7
  169. package/src/client/sharing/useShareDialogController.ts +0 -2
  170. package/src/client/sse-event-processor.ts +85 -1
  171. package/src/ingestion/figma-node-to-html.ts +20 -99
  172. package/src/ingestion/figma-paint-math.ts +371 -0
  173. package/src/ingestion/index.ts +20 -0
  174. package/src/localization/default-messages.ts +6 -0
  175. package/src/scripts/agent-engines/list-agent-engines.ts +1 -18
  176. package/src/scripts/call-agent.ts +49 -6
  177. package/src/server/agent-chat-plugin.ts +50 -10
  178. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -0
  179. package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
  180. package/dist/observability/hosted-model-experiment.d.ts +0 -39
  181. package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
  182. package/dist/observability/hosted-model-experiment.js +0 -90
  183. package/dist/observability/hosted-model-experiment.js.map +0 -1
  184. package/src/observability/hosted-model-experiment.ts +0 -118
@@ -20,6 +20,10 @@ const MAX_HTML_BYTES = 2 * 1024 * 1024;
20
20
  const MULTIPART_OVERHEAD_BYTES = 1024 * 1024;
21
21
  const TOTAL_BODY_LIMIT = MAX_FIG_FILE_BYTES + MULTIPART_OVERHEAD_BYTES;
22
22
 
23
+ // Matches the local-kiwi clipboard frame cap so a token-free .fig hydration of
24
+ // a multi-frame paste can fill every imported screen in one upload.
25
+ const MAX_HYDRATE_FILES = 50;
26
+
23
27
  function fieldText(
24
28
  parts: Awaited<ReturnType<typeof readMultipartFormData>>,
25
29
  name: string,
@@ -129,6 +133,51 @@ export const importDesignFile = defineEventHandler(async (event) => {
129
133
  if (data.length > MAX_FIG_FILE_BYTES) {
130
134
  throw new Error(".fig file is too large (max 50 MB).");
131
135
  }
136
+
137
+ // Token-free hydration: fill the image placeholders left by a no-token
138
+ // clipboard paste using the SAME .fig's embedded image bytes. No Figma
139
+ // token, no REST call — the .fig `images/` entries are keyed by the
140
+ // same SHA-1 hash the paste stamped into data-figma-image-ref.
141
+ const hydrateFileIdsRaw = fieldText(parts, "hydrateFileIds");
142
+ if (hydrateFileIdsRaw) {
143
+ const fileIds = hydrateFileIdsRaw
144
+ .split(",")
145
+ .map((id) => id.trim())
146
+ .filter(Boolean);
147
+ if (fileIds.length === 0) {
148
+ throw new Error("No screen ids provided to hydrate.");
149
+ }
150
+ if (fileIds.length > MAX_HYDRATE_FILES) {
151
+ throw new Error(
152
+ `Too many screens to hydrate at once (max ${MAX_HYDRATE_FILES}).`,
153
+ );
154
+ }
155
+ const { hydrateFileImagesFromFig, indexFigImages } =
156
+ await import("../lib/figma-image-hydration.js");
157
+ // Decode + index the uploaded .fig once, then reuse across screens.
158
+ const figImages = indexFigImages(data);
159
+ const results = [];
160
+ let totalResolved = 0;
161
+ let totalMissing = 0;
162
+ for (const fileId of fileIds) {
163
+ const result = await hydrateFileImagesFromFig({
164
+ fileId,
165
+ figImages,
166
+ ownerEmail: session.email!,
167
+ });
168
+ results.push(result);
169
+ totalResolved += result.resolved;
170
+ totalMissing += result.missing;
171
+ }
172
+ return {
173
+ importKind: "fig-hydrate" as const,
174
+ designId,
175
+ results,
176
+ totalResolved,
177
+ totalMissing,
178
+ };
179
+ }
180
+
132
181
  // Keep Kiwi/Zstd and the sizeable editable renderer off the normal HTML
133
182
  // upload path. They are loaded only for an actual `.fig` request.
134
183
  const { importFigFileToEditableHtml } =
@@ -148,6 +197,7 @@ export const importDesignFile = defineEventHandler(async (event) => {
148
197
  importKind: "fig",
149
198
  ...saved,
150
199
  stats: converted.stats,
200
+ unresolvedImageRefCount: converted.stats.unresolvedImageRefCount,
151
201
  };
152
202
  }
153
203
 
@@ -21,16 +21,20 @@ const MAX_KIWI_CHUNKS = 4_096;
21
21
  const MAX_ZIP_ENTRIES = 2_048;
22
22
  const MAX_ZIP_NAME_BYTES = 512;
23
23
  const MAX_COMPRESSION_RATIO = 1_000;
24
- const MAX_DECODED_OBJECTS = 250_000;
25
24
  const MAX_DECODE_DEPTH = 256;
26
- const MAX_COLLECTION_LENGTH = 250_000;
27
- const MAX_COLLECTION_ITEMS = 1_000_000;
25
+ // Sized with ~15x headroom over a real 11 MB corpus .fig (~530k objects,
26
+ // ~1.5M items, longest single collection ~7.6k) so genuine files decode while a
27
+ // crafted document still hits a finite total-work ceiling.
28
+ const MAX_DECODED_OBJECTS = 8_000_000;
29
+ const MAX_COLLECTION_LENGTH = 2_000_000;
30
+ const MAX_COLLECTION_ITEMS = 24_000_000;
28
31
  const MAX_DECODE_READS = 64 * 1024 * 1024;
29
32
  const MAX_SANITIZED_BINARY_BYTES = 32 * 1024 * 1024;
30
33
  const MAX_DECODED_STRING_BYTES = 4 * 1024 * 1024;
31
34
  const MAX_TOTAL_STRING_BYTES = 32 * 1024 * 1024;
32
35
  const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
33
36
  const FIG_KIWI_MAGIC = Buffer.from("fig-kiwi", "utf8");
37
+ const FIGJAM_KIWI_MAGIC = Buffer.from("fig-jam.", "utf8");
34
38
  const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
35
39
  const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
36
40
  const JPEG_MAGIC = Buffer.from([0xff, 0xd8, 0xff]);
@@ -53,6 +57,7 @@ export interface DecodedFig {
53
57
  format: "kiwi" | "zip";
54
58
  version?: number;
55
59
  document: unknown;
60
+ decodeError?: string;
56
61
  images: DecodedFigImage[];
57
62
  thumbnail: Buffer | null;
58
63
  }
@@ -193,7 +198,10 @@ export function decodeKiwiContainer(file: Buffer): DecodedFigKiwi {
193
198
  if (file.length > MAX_FIG_FILE_BYTES) {
194
199
  throw new Error(".fig file is too large (max 50 MB).");
195
200
  }
196
- if (!file.subarray(0, 8).equals(FIG_KIWI_MAGIC)) {
201
+ if (
202
+ !file.subarray(0, 8).equals(FIG_KIWI_MAGIC) &&
203
+ !file.subarray(0, 8).equals(FIGJAM_KIWI_MAGIC)
204
+ ) {
197
205
  throw new Error("Not a fig-kiwi file (missing magic header)");
198
206
  }
199
207
  if (file.length < 12) {
@@ -476,7 +484,7 @@ function sanitizeForJson(
476
484
  const entries = Object.entries(value);
477
485
  budget.items += entries.length;
478
486
  if (budget.items > MAX_COLLECTION_ITEMS) {
479
- throw new Error("Decoded .fig document has too many object fields.");
487
+ throw new Error("Decoded .fig document has too many fields.");
480
488
  }
481
489
  try {
482
490
  for (const [k, v] of entries) {
@@ -625,23 +633,26 @@ function compileBudgetedSchema(schema: Schema): CompiledDecoder {
625
633
  }
626
634
 
627
635
  // Returns null on any decode failure so callers can still surface the raw
628
- // document buffer.
636
+ // document buffer. Also returns an optional decodeError string so callers can
637
+ // surface the reason rather than falling back to a generic message.
629
638
  function decodeKiwiDocument(
630
639
  schemaBuf: Buffer,
631
640
  documentBuf: Buffer,
632
- ): unknown | null {
641
+ ): { document: unknown | null; decodeError?: string } {
633
642
  let schema: Schema;
634
643
  try {
635
644
  assertSafeBinarySchemaShape(schemaBuf);
636
645
  schema = decodeBinarySchema(schemaBuf);
637
- } catch {
638
- return null;
646
+ } catch (e) {
647
+ return {
648
+ document: null,
649
+ decodeError: `Schema parsing failed: ${e instanceof Error ? e.message : String(e)}`,
650
+ };
639
651
  }
640
652
  // kiwi-schema compiles the schema with `new Function`, so never pass names
641
653
  // from an untrusted binary schema to it without strict identifier and size
642
654
  // validation. Real Figma schemas use ordinary identifiers and a `Message`
643
655
  // root; anything else is an unsupported/probably hostile variant.
644
- if (schema.definitions.length > 1_024) return null;
645
656
  const definitionNames = new Set(schema.definitions.map((d) => d.name));
646
657
  const safeIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
647
658
  const primitiveTypes = new Set([
@@ -654,40 +665,71 @@ function decodeKiwiDocument(
654
665
  "int64",
655
666
  "uint64",
656
667
  ]);
657
- let fieldCount = 0;
658
668
  for (const definition of schema.definitions) {
659
- if (!safeIdentifier.test(definition.name)) return null;
660
- if (!Array.isArray(definition.fields) || definition.fields.length > 1_024) {
661
- return null;
669
+ if (!safeIdentifier.test(definition.name)) {
670
+ return {
671
+ document: null,
672
+ decodeError: `Unsafe schema identifier: "${definition.name}"`,
673
+ };
674
+ }
675
+ if (!Array.isArray(definition.fields)) {
676
+ return {
677
+ document: null,
678
+ decodeError: `Definition "${definition.name}" has no fields array`,
679
+ };
662
680
  }
663
- fieldCount += definition.fields.length;
664
- if (fieldCount > 20_000) return null;
665
681
  for (const field of definition.fields) {
666
- if (!safeIdentifier.test(field.name)) return null;
682
+ if (!safeIdentifier.test(field.name)) {
683
+ return {
684
+ document: null,
685
+ decodeError: `Unsafe field identifier: "${definition.name}.${field.name}"`,
686
+ };
687
+ }
667
688
  if (
668
689
  field.type !== null &&
669
690
  !primitiveTypes.has(field.type) &&
670
691
  !definitionNames.has(field.type)
671
692
  ) {
672
- return null;
693
+ return {
694
+ document: null,
695
+ decodeError: `Unknown field type "${field.type}" in "${definition.name}.${field.name}"`,
696
+ };
673
697
  }
674
698
  }
675
699
  }
676
- const rootMessage = schema.definitions.find(
677
- (definition) =>
678
- definition.name === "Message" && definition.kind === "MESSAGE",
679
- );
680
- if (!rootMessage) return null;
700
+ // Current Figma .fig files use "Message" as root; fall back to the first
701
+ // MESSAGE definition when the name differs (schema evolution resilience).
702
+ const rootMessage =
703
+ schema.definitions.find(
704
+ (d) => d.name === "Message" && d.kind === "MESSAGE",
705
+ ) ?? schema.definitions.find((d) => d.kind === "MESSAGE");
706
+ if (!rootMessage) {
707
+ return {
708
+ document: null,
709
+ decodeError: `No MESSAGE definition found in schema (${schema.definitions.length} definitions: ${schema.definitions
710
+ .map((d) => d.name)
711
+ .slice(0, 10)
712
+ .join(", ")})`,
713
+ };
714
+ }
681
715
 
682
716
  let compiled: CompiledDecoder;
683
717
  try {
684
718
  compiled = compileBudgetedSchema(schema);
685
- } catch {
686
- return null;
719
+ } catch (e) {
720
+ return {
721
+ document: null,
722
+ decodeError: `Schema compilation failed: ${e instanceof Error ? e.message : String(e)}`,
723
+ };
687
724
  }
688
725
  const decodeKey = `decode${rootMessage.name}`;
689
726
  const decoder = compiled[decodeKey];
690
- if (typeof decoder !== "function") return null;
727
+ if (typeof decoder !== "function") {
728
+ return {
729
+ document: null,
730
+ decodeError: `Compiled decoder missing function "${decodeKey}"`,
731
+ };
732
+ }
691
733
 
692
734
  try {
693
735
  const view = new Uint8Array(
@@ -697,15 +739,20 @@ function decodeKiwiDocument(
697
739
  );
698
740
  const bb = new BudgetByteBuffer(view);
699
741
  const document = decoder.call(compiled, bb);
700
- return sanitizeForJson(document, {
701
- objects: 0,
702
- items: 0,
703
- binaryBytes: 0,
704
- stringBytes: 0,
705
- active: new WeakSet(),
706
- });
707
- } catch {
708
- return null;
742
+ return {
743
+ document: sanitizeForJson(document, {
744
+ objects: 0,
745
+ items: 0,
746
+ binaryBytes: 0,
747
+ stringBytes: 0,
748
+ active: new WeakSet(),
749
+ }),
750
+ };
751
+ } catch (e) {
752
+ return {
753
+ document: null,
754
+ decodeError: `Document decoding failed: ${e instanceof Error ? e.message : String(e)}`,
755
+ };
709
756
  }
710
757
  }
711
758
 
@@ -746,23 +793,12 @@ function assertSafeBinarySchemaShape(schemaBuf: Buffer): void {
746
793
  ),
747
794
  );
748
795
  const definitionCount = bb.readVarUint();
749
- if (definitionCount > 1_024) {
750
- throw new Error(".fig schema has too many definitions.");
751
- }
752
- let totalFields = 0;
753
796
  for (let index = 0; index < definitionCount; index += 1) {
754
797
  bb.readString();
755
798
  const kind = bb.readByte();
756
799
  if (kind > 2)
757
800
  throw new Error(".fig schema has an invalid definition kind.");
758
801
  const fields = bb.readVarUint();
759
- if (fields > 1_024) {
760
- throw new Error(".fig schema definition has too many fields.");
761
- }
762
- totalFields += fields;
763
- if (totalFields > 20_000) {
764
- throw new Error(".fig schema has too many fields.");
765
- }
766
802
  for (let fieldIndex = 0; fieldIndex < fields; fieldIndex += 1) {
767
803
  bb.readString();
768
804
  bb.readVarInt();
@@ -790,7 +826,8 @@ export function decodeFig(file: Buffer): DecodedFig {
790
826
  const inner = decodeKiwiContainer(canvasEntry.data);
791
827
  version = inner.version;
792
828
  extraBlobs = inner.blobs;
793
- document = decodeKiwiDocument(inner.schema, inner.document);
829
+ const kiwiResult = decodeKiwiDocument(inner.schema, inner.document);
830
+ document = kiwiResult.document;
794
831
 
795
832
  const images: DecodedFigImage[] = [];
796
833
  const seen = new Set<string>();
@@ -813,19 +850,23 @@ export function decodeFig(file: Buffer): DecodedFig {
813
850
  format: "zip",
814
851
  version,
815
852
  document,
853
+ ...(kiwiResult.decodeError
854
+ ? { decodeError: kiwiResult.decodeError }
855
+ : {}),
816
856
  images,
817
857
  thumbnail: thumbnailEntry?.data ?? null,
818
858
  };
819
859
  }
820
860
 
821
861
  const decoded = decodeKiwiContainer(file);
822
- const document = decodeKiwiDocument(decoded.schema, decoded.document);
862
+ const kiwiResult = decodeKiwiDocument(decoded.schema, decoded.document);
823
863
  const images = collectImagesFromBlobs(decoded.blobs);
824
864
  const thumbnail = findThumbnail(decoded.document, decoded.blobs);
825
865
  return {
826
866
  format: "kiwi",
827
867
  version: decoded.version,
828
- document,
868
+ document: kiwiResult.document,
869
+ ...(kiwiResult.decodeError ? { decodeError: kiwiResult.decodeError } : {}),
829
870
  images,
830
871
  thumbnail,
831
872
  };
@@ -32,6 +32,8 @@ export interface FigFileImportResult {
32
32
  imageCount: number;
33
33
  uploadedImageCount: number;
34
34
  omittedImageCount: number;
35
+ approximatedNodeCount: number;
36
+ unresolvedImageRefCount: number;
35
37
  };
36
38
  }
37
39
 
@@ -45,16 +47,20 @@ function mimeTypeForImage(image: DecodedFigImage): string {
45
47
  return "application/octet-stream";
46
48
  }
47
49
 
48
- function nodeChangesFromDocument(document: unknown): unknown[] {
50
+ function nodeChangesFromDocument(
51
+ document: unknown,
52
+ decodeError?: string,
53
+ ): unknown[] {
49
54
  if (!document || typeof document !== "object") {
55
+ const detail = decodeError ? ` Decode detail: ${decodeError}.` : "";
50
56
  throw new Error(
51
- "This .fig variant could not be decoded. Try a Figma link/API import or export the frame as HTML/SVG instead.",
57
+ `This .fig file could not be decoded.${detail} The schema format may have changed since this file was saved, or the file may be a newer Figma version. Try: (1) copy the frame in Figma and paste directly onto the Design canvas — no API quota needed, or (2) use a Figma frame link to import via the API.`,
52
58
  );
53
59
  }
54
60
  const nodeChanges = (document as { nodeChanges?: unknown }).nodeChanges;
55
61
  if (!Array.isArray(nodeChanges)) {
56
62
  throw new Error(
57
- "This .fig variant does not expose editable node data. Try a Figma link/API import instead.",
63
+ "This .fig file decoded but does not contain editable node data. Copy the frame in Figma and paste onto the canvas, or use a Figma frame link to import via the API.",
58
64
  );
59
65
  }
60
66
  if (nodeChanges.length > MAX_FIG_NODES) {
@@ -156,8 +162,12 @@ export async function convertDecodedFigToEditableHtml(
156
162
  },
157
163
  ): Promise<FigFileImportResult> {
158
164
  assertSafeDecodedFigDocument(decoded.document);
159
- const nodeChanges = nodeChangesFromDocument(decoded.document);
165
+ const nodeChanges = nodeChangesFromDocument(
166
+ decoded.document,
167
+ decoded.decodeError,
168
+ );
160
169
  assertEmbeddedImageBudget(decoded.images);
170
+
161
171
  // Render and validate before uploading any extracted images so an invalid or
162
172
  // excessively complex document cannot leave orphaned storage objects behind.
163
173
  // The upload primitive has no cross-provider delete contract, so validate
@@ -171,6 +181,7 @@ export async function convertDecodedFigToEditableHtml(
171
181
  const preliminary = renderHtmlTemplates(decoded.document, {
172
182
  imageMap: worstCaseImageMap,
173
183
  missingImageUrl: "about:blank",
184
+ trackUnresolvedImageRefs: true,
174
185
  });
175
186
  validateRenderedFrames(preliminary);
176
187
  const images = await uploadEmbeddedImages(
@@ -186,6 +197,7 @@ export async function convertDecodedFigToEditableHtml(
186
197
  // Never persist a data URL or a broken relative link when an image
187
198
  // blob could not be uploaded. The warning makes the omission clear.
188
199
  missingImageUrl: "about:blank",
200
+ trackUnresolvedImageRefs: true,
189
201
  });
190
202
  validateRenderedFrames(rendered);
191
203
 
@@ -244,6 +256,8 @@ export async function convertDecodedFigToEditableHtml(
244
256
  imageCount: decoded.images.length,
245
257
  uploadedImageCount: images.uploaded,
246
258
  omittedImageCount: images.omitted,
259
+ approximatedNodeCount: rendered.approximatedNodes.length,
260
+ unresolvedImageRefCount: rendered.unresolvedImageRefs?.size ?? 0,
247
261
  },
248
262
  };
249
263
  }
@@ -253,7 +267,7 @@ function validateRenderedFrames(
253
267
  ): void {
254
268
  if (rendered.frames.length === 0) {
255
269
  throw new Error(
256
- "No editable top-level frames were found in this .fig file. Try importing a Figma frame link instead.",
270
+ "No editable top-level frames were found in this .fig file.",
257
271
  );
258
272
  }
259
273
  if (rendered.frames.length > MAX_FIG_FRAMES) {