@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
@@ -1,6 +1,7 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { z } from "zod";
3
3
 
4
+ import { importFigmaClipboardFromBuffer } from "../server/lib/figma-clipboard-local-decode.js";
4
5
  import {
5
6
  buildFigmaNodeCandidates,
6
7
  extractVisibleTexts,
@@ -19,7 +20,13 @@ import { parseFigmaFileKey } from "../shared/figma-url.js";
19
20
 
20
21
  const NODE_STRUCTURE_DEPTH = 3;
21
22
 
22
- const CREDENTIAL_MISSING_RE = /credential not configured/i;
23
+ // Also matches a Figma 403 that occurs when the token is saved but lacks
24
+ // file_content:read scope — the validator only checks current_user:read.
25
+ const CREDENTIAL_MISSING_RE =
26
+ /credential not configured|figma.*request failed:.*403|figma.*request failed:.*forbidden/i;
27
+ // Transient errors should not block local-kiwi fallback when the buffer is present.
28
+ const TRANSIENT_ERROR_RE =
29
+ /quota cooldown|provider.*quota|rate.?limit|fetch failed|network.*error|timeout|ECONNRESET|ENOTFOUND|ERR_NETWORK/i;
23
30
  const DURABLE_STORAGE_REQUIRED_RE =
24
31
  /authenticated user so assets can be stored durably|could not store a Figma image durably|needs durable file storage/i;
25
32
 
@@ -69,6 +76,13 @@ export default defineAction({
69
76
  .describe(
70
77
  "Figma clipboard HTML used for fallback matching. When exact node ids are present, the client removes the large private data-buffer while retaining figmeta and visible HTML.",
71
78
  ),
79
+ clipboardBuffer: z
80
+ .string()
81
+ .max(15_000_000)
82
+ .optional()
83
+ .describe(
84
+ "Base64-encoded fig-kiwi binary from the clipboard's data-buffer. Present when the client used the local-kiwi strategy (no Figma access token). The server decodes this to build editable HTML from geometry, text, and fills without a REST call.",
85
+ ),
72
86
  originalName: z.string().optional(),
73
87
  }),
74
88
  run: async ({
@@ -77,6 +91,7 @@ export default defineAction({
77
91
  selectedNodeIds,
78
92
  selectedNodeIdsTruncated,
79
93
  clipboardHtml,
94
+ clipboardBuffer,
80
95
  originalName,
81
96
  }) => {
82
97
  const fileKey = parseFigmaFileKey(figmetaFileKey);
@@ -98,10 +113,8 @@ export default defineAction({
98
113
  try {
99
114
  if (selectedNodeIds?.length) {
100
115
  const nodesById = await fetchFigmaNodes(fileKey, selectedNodeIds);
101
- const { files, fidelityEntries } = await buildScreenFilesFromFigmaNodes(
102
- fileKey,
103
- nodesById,
104
- );
116
+ const { files, fidelityEntries, missingImageFillCount } =
117
+ await buildScreenFilesFromFigmaNodes(fileKey, nodesById);
105
118
  const saved = await saveImportedDesignFiles({
106
119
  designId,
107
120
  sourceType: "figma-clipboard-rest",
@@ -110,9 +123,15 @@ export default defineAction({
110
123
  const selectionWarnings = selectedNodeIdsTruncated
111
124
  ? [SELECTION_TRUNCATED_GUIDANCE]
112
125
  : [];
126
+ const fillWarnings =
127
+ missingImageFillCount > 0
128
+ ? [
129
+ `${missingImageFillCount} image fill${missingImageFillCount === 1 ? "" : "s"} could not be fetched from Figma and were omitted. This can happen for deleted images or very large assets.`,
130
+ ]
131
+ : [];
113
132
  return {
114
133
  ...saved,
115
- warnings: [...saved.warnings, ...selectionWarnings],
134
+ warnings: [...saved.warnings, ...selectionWarnings, ...fillWarnings],
116
135
  strategy: "restNodes" as const,
117
136
  figma: {
118
137
  fileKey,
@@ -142,17 +161,22 @@ export default defineAction({
142
161
  if (matchResult.status === "matched") {
143
162
  const nodeIds = matchResult.matches.map((match) => match.id);
144
163
  const nodesById = await fetchFigmaNodes(fileKey, nodeIds);
145
- const { files, fidelityEntries } = await buildScreenFilesFromFigmaNodes(
146
- fileKey,
147
- nodesById,
148
- );
164
+ const { files, fidelityEntries, missingImageFillCount } =
165
+ await buildScreenFilesFromFigmaNodes(fileKey, nodesById);
149
166
  const saved = await saveImportedDesignFiles({
150
167
  designId,
151
168
  sourceType: "figma-clipboard-rest",
152
169
  files,
153
170
  });
171
+ const fillWarnings =
172
+ missingImageFillCount > 0
173
+ ? [
174
+ `${missingImageFillCount} image fill${missingImageFillCount === 1 ? "" : "s"} could not be fetched from Figma and were omitted.`,
175
+ ]
176
+ : [];
154
177
  return {
155
178
  ...saved,
179
+ warnings: [...(saved.warnings ?? []), ...fillWarnings],
156
180
  strategy: "restNodes" as const,
157
181
  figma: {
158
182
  fileKey,
@@ -174,26 +198,67 @@ export default defineAction({
174
198
  throw error;
175
199
  }
176
200
  figmaApiKeyMissing = CREDENTIAL_MISSING_RE.test(errorMessage);
201
+ const isTransient = TRANSIENT_ERROR_RE.test(errorMessage);
177
202
  if (
178
203
  selectedNodeIds?.length &&
179
204
  !parsedClipboard.fallbackHtml &&
180
- !figmaApiKeyMissing
205
+ !figmaApiKeyMissing &&
206
+ (!isTransient || !clipboardBuffer)
181
207
  ) {
182
208
  // Exact ids prove this was a current Figma clipboard. With no visible
183
- // fallback, a real REST/import failure must remain truthful and
184
- // actionable rather than being mislabeled as a clipboard format that
185
- // omitted ids.
209
+ // fallback, a permanent REST failure must surface as a real error rather
210
+ // than silently degrading. Transient errors fall through to local-kiwi
211
+ // only when a buffer is present to decode; without a buffer there is
212
+ // nothing to fall back to, so even transient errors must propagate.
186
213
  throw error;
187
214
  }
188
215
  if (!figmaApiKeyMissing) {
189
- // A real (non-credential) REST failure — network error, revoked
190
- // token, file access issue, etc. Still fall back to the honest
191
- // clipboard preview rather than losing the paste entirely, but this
192
- // isn't a "no confident match" case, so don't claim ambiguity.
193
216
  matchStatus = "error";
194
217
  }
195
218
  }
196
219
 
220
+ // Local-kiwi fallback: decode the binary buffer when REST failed for any
221
+ // reason (missing token, 403, quota cooldown, network error) and the buffer
222
+ // is present. Always produces editable geometry, text, and auto-layout.
223
+ // IMAGE fills land as about:blank placeholders that hydrate-figma-paste-images
224
+ // resolves retroactively once the quota clears or the token is configured.
225
+ if ((figmaApiKeyMissing || matchStatus === "error") && clipboardBuffer) {
226
+ try {
227
+ const localResult = await importFigmaClipboardFromBuffer({
228
+ bufferBase64: clipboardBuffer,
229
+ fileKey,
230
+ originalName,
231
+ });
232
+ if (localResult.files.length > 0) {
233
+ const saved = await saveImportedDesignFiles({
234
+ designId,
235
+ sourceType: "figma-clipboard-local-kiwi",
236
+ files: localResult.files,
237
+ });
238
+ return {
239
+ ...saved,
240
+ warnings: [...saved.warnings, ...localResult.warnings],
241
+ strategy: "localKiwi" as const,
242
+ figmaApiKeyMissing,
243
+ figma: { fileKey, selectedNodeIds },
244
+ unresolvedImages: localResult.unresolvedImageRefs.length,
245
+ fidelityReport: {
246
+ exactCount: 0,
247
+ approximated: [],
248
+ imageFallbacks: [],
249
+ unresolvedImages: localResult.unresolvedImageRefs.length,
250
+ },
251
+ guidance:
252
+ localResult.unresolvedImageRefs.length > 0
253
+ ? `Imported from Figma using local decode — geometry, text, and styles are editable. ${localResult.unresolvedImageRefs.length} image${localResult.unresolvedImageRefs.length === 1 ? "" : "s"} need a Figma access token to load. Connect Figma in Settings to fill them in, or use "Copy as PNG" for individual images.`
254
+ : "Imported from Figma using local decode — geometry, text, and styles are fully editable. Connect Figma in Settings for highest-fidelity REST imports.",
255
+ };
256
+ }
257
+ } catch {
258
+ // Local decode failed — fall through to html-fallback below.
259
+ }
260
+ }
261
+
197
262
  if (!parsedClipboard.fallbackHtml) {
198
263
  return {
199
264
  designId,
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  import {
6
6
  buildScreenFilesFromFigmaNodes,
7
7
  fetchFigmaNode,
8
+ isFigmaRateLimitError,
8
9
  resolveTargetNodeId,
9
10
  summarizeFidelity,
10
11
  } from "../server/lib/figma-node-import.js";
@@ -78,29 +79,41 @@ export default defineAction({
78
79
  const designId = await resolveImportDesignId(args.designId);
79
80
  await assertAccess("design", designId, "editor");
80
81
 
81
- const nodeId = await resolveTargetNodeId(fileKey, requestedNodeId);
82
- const rootNode = await fetchFigmaNode(fileKey, nodeId);
82
+ try {
83
+ const nodeId = await resolveTargetNodeId(fileKey, requestedNodeId);
84
+ const rootNode = await fetchFigmaNode(fileKey, nodeId);
83
85
 
84
- const { files, fidelityEntries } = await buildScreenFilesFromFigmaNodes(
85
- fileKey,
86
- { [nodeId]: rootNode },
87
- {
88
- source: () => ({ figmaUrl: args.figmaUrl ?? null }),
89
- },
90
- );
86
+ const { files, fidelityEntries } = await buildScreenFilesFromFigmaNodes(
87
+ fileKey,
88
+ { [nodeId]: rootNode },
89
+ {
90
+ source: () => ({ figmaUrl: args.figmaUrl ?? null }),
91
+ },
92
+ );
91
93
 
92
- const saved = await saveImportedDesignFiles({
93
- designId,
94
- sourceType: "figma-import",
95
- files,
96
- });
94
+ const saved = await saveImportedDesignFiles({
95
+ designId,
96
+ sourceType: "figma-import",
97
+ files,
98
+ });
97
99
 
98
- return {
99
- ...saved,
100
- figma: { fileKey, nodeId, nodeName: rootNode.name ?? null },
101
- fidelityReport: summarizeFidelity(fidelityEntries),
102
- guidance:
103
- "Review fidelityReport.imageFallbacks for subtrees rendered as PNG (masks, vector/boolean geometry, lines/arcs, advanced strokes/text, transformed image crops, and unsupported node types) and fidelityReport.approximated for properties CSS cannot express exactly (rotation, per-side stroke alignment, radial/angular/diamond gradients, blur radius scale, and live component/variable/prototype semantics).",
104
- };
100
+ return {
101
+ ...saved,
102
+ figma: { fileKey, nodeId, nodeName: rootNode.name ?? null },
103
+ fidelityReport: summarizeFidelity(fidelityEntries),
104
+ guidance:
105
+ "Review fidelityReport.imageFallbacks for subtrees rendered as PNG (masks, vector/boolean geometry, lines/arcs, advanced strokes/text, transformed image crops, and unsupported node types) and fidelityReport.approximated for properties CSS cannot express exactly (rotation, per-side stroke alignment, radial/angular/diamond gradients, blur radius scale, and live component/variable/prototype semantics).",
106
+ };
107
+ } catch (err) {
108
+ if (isFigmaRateLimitError(err)) {
109
+ throw Object.assign(err, {
110
+ rateLimitRetryAfter: err.retryAfterSeconds,
111
+ rateLimitPlanTier: err.figmaPlanTier,
112
+ rateLimitType: err.figmaRateLimitType,
113
+ rateLimitUpgradeUrl: err.figmaUpgradeUrl,
114
+ });
115
+ }
116
+ throw err;
117
+ }
105
118
  },
106
119
  });
@@ -97,6 +97,7 @@ import type {
97
97
  IframeContextMenuPayload,
98
98
  IframeFigmaClipboardPastePayload,
99
99
  IframeHotkeyPayload,
100
+ IframeImagePastePayload,
100
101
  } from "./design-canvas/iframe-events";
101
102
  import {
102
103
  forwardEmbeddedCanvasPanMessage,
@@ -473,6 +474,7 @@ interface DesignCanvasProps {
473
474
  onElementDblClickText?: (info: ElementInfo) => void;
474
475
  onIframeHotkey?: (event: IframeHotkeyPayload) => void;
475
476
  onFigmaClipboardPaste?: (event: IframeFigmaClipboardPastePayload) => void;
477
+ onImagePaste?: (event: IframeImagePastePayload) => void;
476
478
  onIframeContextMenu?: (event: IframeContextMenuPayload) => void;
477
479
  onEditorDragStateChange?: (active: boolean) => void;
478
480
  onVisualStructureChange?: (
@@ -1045,6 +1047,7 @@ export function DesignCanvas({
1045
1047
  onElementDblClickText,
1046
1048
  onIframeHotkey,
1047
1049
  onFigmaClipboardPaste,
1050
+ onImagePaste,
1048
1051
  onIframeContextMenu,
1049
1052
  onEditorDragStateChange,
1050
1053
  onVisualStructureChange,
@@ -2135,6 +2138,7 @@ export function DesignCanvas({
2135
2138
  // static-fallback board thumbnails that call appendHitTestResponder.
2136
2139
  // Without this, a drop onto a live/active screen has no responder and
2137
2140
  // always falls through to the 50ms request timeout.
2141
+ const imageDiagBridge = "";
2138
2142
  const bridgeToInject =
2139
2143
  MOTION_PREVIEW_BRIDGE_SCRIPT +
2140
2144
  SHADER_FILL_PREVIEW_BRIDGE_SCRIPT +
@@ -2143,7 +2147,8 @@ export function DesignCanvas({
2143
2147
  NAV_BRIDGE_SCRIPT +
2144
2148
  LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT +
2145
2149
  embeddedGestureBridgeForCurrentState +
2146
- editorChromeBridge;
2150
+ editorChromeBridge +
2151
+ imageDiagBridge;
2147
2152
  const frameContent = getEmbeddedFrameDocumentContent({
2148
2153
  content: iframeRenderContent,
2149
2154
  embeddedFrameBackground,
@@ -2687,6 +2692,27 @@ export function DesignCanvas({
2687
2692
  if (content) onFigmaClipboardPaste?.({ content });
2688
2693
  return;
2689
2694
  }
2695
+ if (e.data.type === "canvas-image-paste") {
2696
+ const raw = Array.isArray(e.data.files) ? e.data.files : [];
2697
+ const MAX_IMAGE_PASTE_FILES = 20;
2698
+ const MAX_DATA_URL_BYTES = 20 * 1024 * 1024; // 20 MB per file
2699
+ const files = raw
2700
+ .slice(0, MAX_IMAGE_PASTE_FILES)
2701
+ .filter(
2702
+ (
2703
+ f: unknown,
2704
+ ): f is { dataUrl: string; type: string; name: string } => {
2705
+ if (!f || typeof f !== "object") return false;
2706
+ const dataUrl = (f as { dataUrl?: unknown }).dataUrl;
2707
+ if (typeof dataUrl !== "string") return false;
2708
+ if (!dataUrl.startsWith("data:image/")) return false;
2709
+ if (dataUrl.length > MAX_DATA_URL_BYTES) return false;
2710
+ return true;
2711
+ },
2712
+ );
2713
+ if (files.length > 0) onImagePaste?.({ files });
2714
+ return;
2715
+ }
2690
2716
  if (e.data.type === "element-contextmenu") {
2691
2717
  const clientX = Number(e.data.clientX);
2692
2718
  const clientY = Number(e.data.clientY);
@@ -2879,6 +2905,7 @@ export function DesignCanvas({
2879
2905
  onElementDblClickText,
2880
2906
  onIframeHotkey,
2881
2907
  onFigmaClipboardPaste,
2908
+ onImagePaste,
2882
2909
  onIframeContextMenu,
2883
2910
  onEditorDragStateChange,
2884
2911
  onVisualStructureChange,
@@ -27,6 +27,7 @@ import {
27
27
  } from "@/lib/design-file-upload";
28
28
  import {
29
29
  importResultNotification,
30
+ isFigmaRateLimitImportError,
30
31
  looksLikeStandaloneHtml,
31
32
  VISUAL_EDIT_CONNECT_COMMAND,
32
33
  VISUAL_EDIT_INSTALL_COMMAND,
@@ -42,6 +43,7 @@ import type { DesignExtensionSlotContext } from "./DesignExtensionsPanel";
42
43
 
43
44
  interface DesignImportPanelProps {
44
45
  context: Pick<DesignExtensionSlotContext, "designId" | "viewMode">;
46
+ onImport?: (result: ImportResult) => void;
45
47
  }
46
48
 
47
49
  type ImportMode =
@@ -51,7 +53,11 @@ type ImportMode =
51
53
  | "html"
52
54
  | "local-app";
53
55
 
54
- export function DesignImportPanel({ context }: DesignImportPanelProps) {
56
+ export function DesignImportPanel(p: DesignImportPanelProps) {
57
+ const context = p.context;
58
+ const onImport = p.onImport;
59
+ const onImportRef = useRef(onImport);
60
+ onImportRef.current = onImport;
55
61
  const t = useT();
56
62
  const { formatNumber } = useFormatters();
57
63
  const navigate = useNavigate();
@@ -77,6 +83,8 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
77
83
  const [htmlText, setHtmlText] = useState("");
78
84
  const [activeMode, setActiveMode] = useState<ImportMode | null>(null);
79
85
  const [lastResult, setLastResult] = useState<ImportResult | null>(null);
86
+ const [figmaRateLimitError, setFigmaRateLimitError] =
87
+ useState<ImportResult | null>(null);
80
88
  const [figUploadName, setFigUploadName] = useState<string | null>(null);
81
89
  const [figUploadProgress, setFigUploadProgress] = useState<number | null>(
82
90
  null,
@@ -91,6 +99,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
91
99
  queryClient.invalidateQueries({ queryKey: ["action", "get-design"] }),
92
100
  queryClient.invalidateQueries({ queryKey: ["action"] }),
93
101
  ]);
102
+ if (result) onImportRef.current?.(result);
94
103
  const fidelityWarnings: string[] = [];
95
104
  const imageFallbackCount = result?.fidelityReport?.imageFallbacks.length;
96
105
  if (imageFallbackCount) {
@@ -190,6 +199,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
190
199
  figmaConnectionChecked && !figmaConnected && !figmaConnectionError;
191
200
 
192
201
  const handleFigmaUrlImport = useCallback(async () => {
202
+ setFigmaRateLimitError(null);
193
203
  const normalizedUrl = figmaUrl.trim();
194
204
  if (!normalizedUrl) {
195
205
  toast.error(t("designEditor.import.errors.figmaUrlRequired"));
@@ -218,9 +228,41 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
218
228
  asNewScreen: true,
219
229
  })) as ImportResult;
220
230
  await finishImport(result, t("designEditor.import.figmaUrlSuccess"));
231
+ setFigmaRateLimitError(null);
221
232
  } catch (error) {
222
233
  // A rejected credential should not linger in component state or the DOM.
223
234
  setFigmaAccessToken("");
235
+ const rateLimitDetails = error as Error & {
236
+ rateLimitRetryAfter?: number;
237
+ rateLimitPlanTier?: string;
238
+ figmaPlanTier?: string;
239
+ rateLimitType?: string;
240
+ figmaRateLimitType?: string;
241
+ rateLimitUpgradeUrl?: string;
242
+ figmaUpgradeUrl?: string;
243
+ };
244
+ const rateLimitResult: ImportResult = {
245
+ error:
246
+ typeof rateLimitDetails.message === "string"
247
+ ? rateLimitDetails.message
248
+ : t("common.genericError"),
249
+ rateLimitRetryAfter: rateLimitDetails.rateLimitRetryAfter,
250
+ rateLimitPlanTier:
251
+ rateLimitDetails.rateLimitPlanTier ?? rateLimitDetails.figmaPlanTier,
252
+ rateLimitType:
253
+ rateLimitDetails.rateLimitType ?? rateLimitDetails.figmaRateLimitType,
254
+ rateLimitUpgradeUrl:
255
+ rateLimitDetails.rateLimitUpgradeUrl ??
256
+ rateLimitDetails.figmaUpgradeUrl,
257
+ };
258
+ const isRateLimitError =
259
+ (error instanceof Error &&
260
+ /rate limit|429|quota/i.test(error.message)) ||
261
+ isFigmaRateLimitImportError(rateLimitResult);
262
+ if (isRateLimitError) {
263
+ setFigmaRateLimitError(rateLimitResult);
264
+ setActiveMode("fig-upload");
265
+ }
224
266
  toast.error(t("designEditor.import.errors.figmaImportFailed"), {
225
267
  description:
226
268
  error instanceof Error ? error.message : t("common.genericError"),
@@ -282,6 +324,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
282
324
  onProgress: ({ percent }) => setFigUploadProgress(percent),
283
325
  });
284
326
  await finishImport(result, t("designEditor.import.uploadSuccess"));
327
+ setFigmaRateLimitError(null);
285
328
  } catch (error) {
286
329
  toast.error(t("designEditor.import.errors.uploadFailed"), {
287
330
  description:
@@ -325,6 +368,64 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
325
368
 
326
369
  <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pb-4 pt-3">
327
370
  <div className="space-y-0.5">
371
+ {figmaRateLimitError ? (
372
+ <div className="space-y-2 rounded-md border border-destructive/30 bg-destructive/5 p-2.5 text-[11px] leading-snug">
373
+ <p className="font-medium text-destructive">
374
+ {t("designEditor.import.rateLimitTitle")}
375
+ </p>
376
+ <p className="text-muted-foreground">
377
+ {figmaRateLimitError.rateLimitType === "low"
378
+ ? t("designEditor.import.rateLimitLowSeat")
379
+ : t("designEditor.import.rateLimitGeneric")}
380
+ </p>
381
+ {figmaRateLimitError.rateLimitRetryAfter ? (
382
+ <p className="text-muted-foreground">
383
+ {t("designEditor.import.rateLimitRetryIn", {
384
+ time:
385
+ figmaRateLimitError.rateLimitRetryAfter >= 60
386
+ ? `${Math.ceil(figmaRateLimitError.rateLimitRetryAfter / 60)} min`
387
+ : `${figmaRateLimitError.rateLimitRetryAfter}s`,
388
+ })}
389
+ </p>
390
+ ) : null}
391
+ <div className="flex flex-col gap-1.5">
392
+ <p className="text-[10px] font-medium text-muted-foreground">
393
+ {t("designEditor.import.rateLimitAlternatives")}
394
+ </p>
395
+ <Button
396
+ size="sm"
397
+ variant="outline"
398
+ className="h-7 w-full px-2 text-[11px]"
399
+ onClick={() => {
400
+ setFigmaRateLimitError(null);
401
+ setActiveMode("figma-paste");
402
+ }}
403
+ >
404
+ {t("designEditor.import.rateLimitUsePaste")}
405
+ </Button>
406
+ <Button
407
+ size="sm"
408
+ variant="outline"
409
+ className="h-7 w-full px-2 text-[11px]"
410
+ onClick={() => {
411
+ setActiveMode("fig-upload");
412
+ }}
413
+ >
414
+ {t("designEditor.import.rateLimitUseFig")}
415
+ </Button>
416
+ </div>
417
+ {figmaRateLimitError.rateLimitUpgradeUrl ? (
418
+ <a
419
+ href={figmaRateLimitError.rateLimitUpgradeUrl}
420
+ target="_blank"
421
+ rel="noreferrer"
422
+ className="block text-[10px] text-foreground underline-offset-2 hover:underline"
423
+ >
424
+ {t("designEditor.import.rateLimitUpgrade")}
425
+ </a>
426
+ ) : null}
427
+ </div>
428
+ ) : null}
328
429
  <ImportSourceRow
329
430
  id="figma-url-import"
330
431
  icon={<IconBrandFigma className="size-3.5" />}
@@ -437,9 +538,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
437
538
  id="figma-paste-import"
438
539
  icon={<IconBrandFigma className="size-3.5" />}
439
540
  title={t("designEditor.import.figmaPasteTitle")}
440
- description={
441
- "Copy a frame in Figma, then paste into the canvas." /* i18n-ignore */
442
- }
541
+ description={t("designEditor.import.figmaPasteDescription")}
443
542
  isOpen={activeMode === "figma-paste"}
444
543
  onToggle={() =>
445
544
  setActiveMode((mode) =>
@@ -448,12 +547,8 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
448
547
  }
449
548
  >
450
549
  <div className="space-y-1.5 p-2 text-[11px] leading-snug text-muted-foreground">
451
- <p>{t("designEditor.import.figmaPasteDescription")}</p>
452
- <p>
453
- {
454
- "Click the canvas first, then paste with the same shortcut you use for copied Design content." /* i18n-ignore */
455
- }
456
- </p>
550
+ <p>{t("designEditor.import.figmaPasteBodyUnlimited")}</p>
551
+ <p>{t("designEditor.import.figmaPasteBodyImages")}</p>
457
552
  </div>
458
553
  </ImportSourceRow>
459
554
 
@@ -461,7 +556,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
461
556
  id="fig-file-import"
462
557
  icon={<IconUpload className="size-3.5" />}
463
558
  title={t("designEditor.import.figUploadTitle")}
464
- description={t("designEditor.import.figUploadDescription")}
559
+ description={t("designEditor.import.figUploadDescriptionShort")}
465
560
  isOpen={activeMode === "fig-upload"}
466
561
  onToggle={() =>
467
562
  setActiveMode((mode) =>