@agent-native/core 0.119.0 → 0.119.2

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 (274) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +23 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/client.ts +4 -0
  5. package/corpus/core/src/agent/production-agent.ts +1 -0
  6. package/corpus/core/src/client/AssistantChat.tsx +11 -50
  7. package/corpus/core/src/client/ErrorBoundary.tsx +25 -19
  8. package/corpus/core/src/client/agent-page/AgentTabFrame.tsx +1 -1
  9. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +23 -0
  10. package/corpus/core/src/client/components/ui/message-scroller.tsx +1 -9
  11. package/corpus/core/src/client/resources/ResourceTree.tsx +3 -3
  12. package/corpus/core/src/client/resources/ResourcesPanel.tsx +64 -60
  13. package/corpus/core/src/client/settings/AccountSettingsCard.tsx +228 -0
  14. package/corpus/core/src/client/settings/SecretsSection.tsx +7 -5
  15. package/corpus/core/src/client/settings/SettingsPanel.tsx +67 -244
  16. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +88 -21
  17. package/corpus/core/src/client/settings/agent-settings-search.ts +0 -2
  18. package/corpus/core/src/client/settings/index.ts +7 -0
  19. package/corpus/core/src/client/vite-dev-recovery-script.ts +18 -1
  20. package/corpus/core/src/db/client.ts +65 -15
  21. package/corpus/core/src/db/index.ts +2 -0
  22. package/corpus/core/src/ingestion/figma-node-to-html.ts +20 -99
  23. package/corpus/core/src/ingestion/figma-paint-math.ts +371 -0
  24. package/corpus/core/src/ingestion/index.ts +20 -0
  25. package/corpus/core/src/localization/default-messages.ts +1 -0
  26. package/corpus/core/src/server/email.ts +33 -1
  27. package/corpus/core/src/styles/agent-native.css +23 -0
  28. package/corpus/core/src/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
  29. package/corpus/core/src/templates/default/app/routes/settings.tsx +2 -0
  30. package/corpus/core/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
  31. package/corpus/core/src/vite/client.ts +76 -8
  32. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +28 -0
  33. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +3 -1
  34. package/corpus/templates/analytics/app/components/layout/Layout.tsx +9 -13
  35. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +2 -2
  36. package/corpus/templates/analytics/app/global.css +0 -5
  37. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -13
  38. package/corpus/templates/analytics/app/lib/sql-query.ts +15 -2
  39. package/corpus/templates/analytics/app/pages/Settings.tsx +2 -24
  40. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +6 -0
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
  42. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +1 -1
  43. package/corpus/templates/analytics/changelog/2026-07-23-ask-analytics-is-better-centered-with-a-quieter-chat-rail-an.md +6 -0
  44. package/corpus/templates/analytics/changelog/2026-07-23-daily-dashboard-emails-load-read-heavy-panels-reliably.md +6 -0
  45. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +2 -2
  46. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +39 -5
  47. package/corpus/templates/analytics/server/lib/dashboard-report.ts +186 -62
  48. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +3 -0
  49. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +18 -2
  50. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +3 -0
  51. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +28 -0
  52. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -4
  53. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +1 -1
  54. package/corpus/templates/assets/app/global.css +0 -5
  55. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +0 -22
  56. package/corpus/templates/assets/app/routes/library.tsx +17 -9
  57. package/corpus/templates/assets/app/routes/settings.tsx +2 -0
  58. package/corpus/templates/assets/changelog/2026-07-23-create-and-chat-surfaces-are-better-centered-with-quieter-ch.md +6 -0
  59. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +28 -0
  60. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  61. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +1 -1
  62. package/corpus/templates/brain/app/global.css +0 -9
  63. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +0 -12
  64. package/corpus/templates/brain/app/i18n-data.ts +1 -0
  65. package/corpus/templates/brain/app/lib/settings-navigation.ts +1 -1
  66. package/corpus/templates/brain/app/routes/settings.tsx +21 -22
  67. package/corpus/templates/brain/changelog/2026-07-23-ask-brain-is-better-centered-with-quieter-chat-history-and-a.md +6 -0
  68. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +28 -0
  69. package/corpus/templates/calendar/app/pages/Settings.tsx +2 -0
  70. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
  71. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  72. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +1 -1
  73. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +0 -15
  74. package/corpus/templates/chat/app/routes/settings.tsx +2 -0
  75. package/corpus/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
  76. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +28 -0
  77. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +2 -0
  78. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +28 -0
  79. package/corpus/templates/content/app/routes/_app.settings.tsx +2 -0
  80. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +28 -0
  81. package/corpus/templates/design/AGENTS.md +18 -5
  82. package/corpus/templates/design/FIGMA_INTEROPERABILITY.md +25 -12
  83. package/corpus/templates/design/actions/hydrate-figma-paste-images.ts +115 -0
  84. package/corpus/templates/design/actions/import-figma-clipboard.ts +83 -18
  85. package/corpus/templates/design/actions/import-figma-frame.ts +34 -21
  86. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +28 -1
  87. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +106 -11
  88. package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +273 -0
  89. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -7
  90. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +57 -9
  91. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +10 -0
  92. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +8 -1
  93. package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
  94. package/corpus/templates/design/app/i18n-data.ts +150 -2
  95. package/corpus/templates/design/app/lib/design-file-upload.ts +85 -0
  96. package/corpus/templates/design/app/lib/design-import.ts +19 -1
  97. package/corpus/templates/design/app/lib/figma-clipboard.ts +110 -10
  98. package/corpus/templates/design/app/routes/settings.tsx +2 -0
  99. package/corpus/templates/design/changelog/2026-07-20-paste-figma-frames-directly-onto-the-canvas-no-token-needed.md +6 -0
  100. package/corpus/templates/design/changelog/2026-07-21-fig-upload-frame-url-and-rate-limit-errors.md +5 -0
  101. package/corpus/templates/design/changelog/2026-07-22-connecting-a-figma-token-now-actually-fills-in-a-pasted-desi.md +6 -0
  102. package/corpus/templates/design/changelog/2026-07-22-figma-line-arrow-vectors-and-stroked-icons-now-render-instea.md +6 -0
  103. package/corpus/templates/design/changelog/2026-07-22-fill-a-no-token-figma-paste-s-images-by-dropping-the-origina.md +6 -0
  104. package/corpus/templates/design/changelog/2026-07-22-imported-and-pasted-figma-frames-now-render-faithfully-fixed.md +6 -0
  105. package/corpus/templates/design/server/handlers/import-design-file.ts +50 -0
  106. package/corpus/templates/design/server/lib/fig-file-decoder.ts +90 -49
  107. package/corpus/templates/design/server/lib/fig-file-import.ts +19 -5
  108. package/corpus/templates/design/server/lib/fig-file-to-html.ts +583 -78
  109. package/corpus/templates/design/server/lib/figma-clipboard-local-decode.ts +238 -0
  110. package/corpus/templates/design/server/lib/figma-image-hydration.ts +389 -0
  111. package/corpus/templates/design/server/lib/figma-node-import.ts +136 -36
  112. package/corpus/templates/design/server/register-secrets.ts +14 -2
  113. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +28 -0
  114. package/corpus/templates/dispatch/app/routes/settings.tsx +2 -0
  115. package/corpus/templates/dispatch/changelog/2026-07-23-quieter-progressive-disclosure.md +6 -0
  116. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +28 -0
  117. package/corpus/templates/forms/app/global.css +0 -5
  118. package/corpus/templates/forms/app/hooks/use-navigation-state.ts +0 -7
  119. package/corpus/templates/forms/app/root.tsx +1 -7
  120. package/corpus/templates/forms/app/routes/_app.settings.tsx +2 -0
  121. package/corpus/templates/forms/changelog/2026-07-23-ask-forms-is-better-centered-with-quieter-chat-history-and-a.md +6 -0
  122. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +28 -0
  123. package/corpus/templates/macros/app/routes/settings.tsx +2 -0
  124. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +28 -0
  125. package/corpus/templates/mail/app/pages/SettingsPage.tsx +3 -1
  126. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +28 -0
  127. package/corpus/templates/plan/app/components/layout/Layout.tsx +2 -2
  128. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +1 -13
  129. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +0 -4
  130. package/corpus/templates/plan/app/routes/settings.tsx +2 -0
  131. package/corpus/templates/plan/changelog/2026-07-23-ask-plan-is-now-a-clean-full-page-chat-surface-with-better-c.md +6 -0
  132. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +28 -0
  133. package/corpus/templates/slides/app/routes/settings.tsx +2 -0
  134. package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +28 -0
  135. package/corpus/toolkit/CHANGELOG.md +6 -0
  136. package/corpus/toolkit/package.json +1 -1
  137. package/corpus/toolkit/src/chat-history.css +2 -2
  138. package/corpus/toolkit/src/composer/PromptComposer.tsx +0 -31
  139. package/corpus/toolkit/src/composer/RealtimeVoiceMode.tsx +7 -2
  140. package/corpus/toolkit/src/styles.css +183 -36
  141. package/dist/a2a/client.d.ts +2 -0
  142. package/dist/a2a/client.d.ts.map +1 -1
  143. package/dist/a2a/client.js +2 -0
  144. package/dist/a2a/client.js.map +1 -1
  145. package/dist/agent/production-agent.d.ts.map +1 -1
  146. package/dist/agent/production-agent.js +1 -0
  147. package/dist/agent/production-agent.js.map +1 -1
  148. package/dist/client/AssistantChat.d.ts.map +1 -1
  149. package/dist/client/AssistantChat.js +11 -42
  150. package/dist/client/AssistantChat.js.map +1 -1
  151. package/dist/client/ErrorBoundary.d.ts.map +1 -1
  152. package/dist/client/ErrorBoundary.js +3 -2
  153. package/dist/client/ErrorBoundary.js.map +1 -1
  154. package/dist/client/agent-page/AgentTabFrame.js +1 -1
  155. package/dist/client/agent-page/AgentTabFrame.js.map +1 -1
  156. package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
  157. package/dist/client/agent-page/AgentTabsPage.js +13 -1
  158. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  159. package/dist/client/components/ui/message-scroller.d.ts.map +1 -1
  160. package/dist/client/components/ui/message-scroller.js +2 -2
  161. package/dist/client/components/ui/message-scroller.js.map +1 -1
  162. package/dist/client/resources/ResourceTree.js +2 -2
  163. package/dist/client/resources/ResourceTree.js.map +1 -1
  164. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  165. package/dist/client/resources/ResourcesPanel.js +11 -11
  166. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  167. package/dist/client/settings/AccountSettingsCard.d.ts +9 -0
  168. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -0
  169. package/dist/client/settings/AccountSettingsCard.js +78 -0
  170. package/dist/client/settings/AccountSettingsCard.js.map +1 -0
  171. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  172. package/dist/client/settings/SecretsSection.js +2 -2
  173. package/dist/client/settings/SecretsSection.js.map +1 -1
  174. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  175. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  176. package/dist/client/settings/SettingsPanel.js +25 -77
  177. package/dist/client/settings/SettingsPanel.js.map +1 -1
  178. package/dist/client/settings/SettingsTabsPage.d.ts +5 -1
  179. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  180. package/dist/client/settings/SettingsTabsPage.js +35 -9
  181. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  182. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  183. package/dist/client/settings/agent-settings-search.js +0 -2
  184. package/dist/client/settings/agent-settings-search.js.map +1 -1
  185. package/dist/client/settings/index.d.ts +2 -1
  186. package/dist/client/settings/index.d.ts.map +1 -1
  187. package/dist/client/settings/index.js +2 -1
  188. package/dist/client/settings/index.js.map +1 -1
  189. package/dist/client/vite-dev-recovery-script.d.ts.map +1 -1
  190. package/dist/client/vite-dev-recovery-script.js +18 -1
  191. package/dist/client/vite-dev-recovery-script.js.map +1 -1
  192. package/dist/collab/routes.d.ts +1 -1
  193. package/dist/db/client.d.ts +18 -8
  194. package/dist/db/client.d.ts.map +1 -1
  195. package/dist/db/client.js +34 -11
  196. package/dist/db/client.js.map +1 -1
  197. package/dist/db/index.d.ts +1 -1
  198. package/dist/db/index.d.ts.map +1 -1
  199. package/dist/db/index.js.map +1 -1
  200. package/dist/ingestion/figma-node-to-html.d.ts.map +1 -1
  201. package/dist/ingestion/figma-node-to-html.js +14 -88
  202. package/dist/ingestion/figma-node-to-html.js.map +1 -1
  203. package/dist/ingestion/figma-paint-math.d.ts +157 -0
  204. package/dist/ingestion/figma-paint-math.d.ts.map +1 -0
  205. package/dist/ingestion/figma-paint-math.js +274 -0
  206. package/dist/ingestion/figma-paint-math.js.map +1 -0
  207. package/dist/ingestion/index.d.ts +1 -0
  208. package/dist/ingestion/index.d.ts.map +1 -1
  209. package/dist/ingestion/index.js +1 -0
  210. package/dist/ingestion/index.js.map +1 -1
  211. package/dist/localization/default-messages.d.ts +1 -0
  212. package/dist/localization/default-messages.d.ts.map +1 -1
  213. package/dist/localization/default-messages.js +1 -0
  214. package/dist/localization/default-messages.js.map +1 -1
  215. package/dist/notifications/routes.d.ts +2 -2
  216. package/dist/observability/routes.d.ts +3 -3
  217. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  218. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  219. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  220. package/dist/secrets/routes.d.ts +6 -6
  221. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  222. package/dist/server/email.d.ts +1 -0
  223. package/dist/server/email.d.ts.map +1 -1
  224. package/dist/server/email.js +29 -1
  225. package/dist/server/email.js.map +1 -1
  226. package/dist/server/transcribe-voice.d.ts +1 -1
  227. package/dist/styles/agent-native.css +23 -0
  228. package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
  229. package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
  230. package/dist/templates/chat/app/components/layout/Sidebar.tsx +1 -1
  231. package/dist/templates/chat/app/hooks/use-navigation-state.ts +0 -15
  232. package/dist/templates/chat/app/routes/settings.tsx +2 -0
  233. package/dist/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
  234. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
  235. package/dist/templates/default/app/routes/settings.tsx +2 -0
  236. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
  237. package/dist/vite/client.d.ts.map +1 -1
  238. package/dist/vite/client.js +68 -6
  239. package/dist/vite/client.js.map +1 -1
  240. package/package.json +2 -2
  241. package/src/a2a/client.ts +4 -0
  242. package/src/agent/production-agent.ts +1 -0
  243. package/src/client/AssistantChat.tsx +11 -50
  244. package/src/client/ErrorBoundary.tsx +25 -19
  245. package/src/client/agent-page/AgentTabFrame.tsx +1 -1
  246. package/src/client/agent-page/AgentTabsPage.tsx +23 -0
  247. package/src/client/components/ui/message-scroller.tsx +1 -9
  248. package/src/client/resources/ResourceTree.tsx +3 -3
  249. package/src/client/resources/ResourcesPanel.tsx +64 -60
  250. package/src/client/settings/AccountSettingsCard.tsx +228 -0
  251. package/src/client/settings/SecretsSection.tsx +7 -5
  252. package/src/client/settings/SettingsPanel.tsx +67 -244
  253. package/src/client/settings/SettingsTabsPage.tsx +88 -21
  254. package/src/client/settings/agent-settings-search.ts +0 -2
  255. package/src/client/settings/index.ts +7 -0
  256. package/src/client/vite-dev-recovery-script.ts +18 -1
  257. package/src/db/client.ts +65 -15
  258. package/src/db/index.ts +2 -0
  259. package/src/ingestion/figma-node-to-html.ts +20 -99
  260. package/src/ingestion/figma-paint-math.ts +371 -0
  261. package/src/ingestion/index.ts +20 -0
  262. package/src/localization/default-messages.ts +1 -0
  263. package/src/server/email.ts +33 -1
  264. package/src/styles/agent-native.css +23 -0
  265. package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
  266. package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
  267. package/src/templates/chat/app/components/layout/Sidebar.tsx +1 -1
  268. package/src/templates/chat/app/hooks/use-navigation-state.ts +0 -15
  269. package/src/templates/chat/app/routes/settings.tsx +2 -0
  270. package/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
  271. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
  272. package/src/templates/default/app/routes/settings.tsx +2 -0
  273. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
  274. package/src/vite/client.ts +76 -8
@@ -35,6 +35,34 @@ Then implement working code that is cohesive, accessible, responsive, and polish
35
35
 
36
36
  Default to Apple/Linear-level restraint: make the primary workflow obvious, then remove everything that does not help that workflow right now. A polished UI often has fewer visible controls, fewer borders, fewer labels, and fewer explanatory surfaces than the first reasonable implementation.
37
37
 
38
+ ### Progressive Disclosure Is A Design Requirement
39
+
40
+ Treat an all-at-once interface as a defect to fix during design, not as a
41
+ styling preference. Before coding, inventory every piece of content and action
42
+ on the surface, then assign each one to the smallest useful visibility level:
43
+
44
+ 1. **Immediate** — the page title, current state, one primary action, and the
45
+ compact context needed to choose what to do next.
46
+ 2. **Expanded** — the details needed for the selected item or active workflow.
47
+ 3. **On demand** — advanced settings, diagnostics, credentials, metadata,
48
+ destructive actions, documentation, and rarely used tools.
49
+
50
+ Use single-select accordions or simple disclosure rows for sibling panels when
51
+ the user is choosing one item at a time. Inside an expanded panel, keep another
52
+ layer for independent concerns instead of dumping every form, explanation, and
53
+ secondary action into the first reveal. Prefer one flat panel with alignment,
54
+ dividers, and whitespace over nested cards; provider or product icons should
55
+ not receive decorative borders or containers unless the container communicates
56
+ state or interaction. A collapsed row should still show the item name, status,
57
+ and a concise summary so the user can scan the whole surface without opening
58
+ everything.
59
+
60
+ Before shipping, ask: “What can disappear until the user asks for it?” Then
61
+ verify collapsed, expanded, loading, empty, error, and narrow-width states.
62
+ If the first viewport contains multiple forms, repeated explanatory copy,
63
+ documentation links, and controls for unrelated tasks, the surface has not
64
+ passed this requirement yet.
65
+
38
66
  - **Start by subtracting**: Before adding a visible control, banner, toolbar row, card, or explanatory block, ask what can be removed, merged, renamed, or moved into an existing affordance.
39
67
  - **One primary action**: Each surface should have one dominant next action. Secondary actions belong in menus, popovers, command palettes, disclosure rows, or contextual hover/focus states unless they are used constantly.
40
68
  - **Progressively disclose rare work**: Advanced options, diagnostics, metadata, settings, import/export, destructive actions, and inspection tools should stay tucked away until requested. Prefer small icon triggers with tooltips, popovers, drawers, or detail panels over permanent chrome.
@@ -109,8 +109,11 @@ ladder.
109
109
  `design-systems` skill's "Import from Figma" section.
110
110
  - Uploading a raw `.fig` file in the Design editor's Import panel decodes the
111
111
  container/Kiwi document locally into editable screens — no Builder
112
- connection needed — and is scoped to screens only; it never creates or
113
- updates a design system. This is separate from uploading `.fig` on the
112
+ connection needed — and accepts an optional Figma frame URL alongside the
113
+ file. When the URL contains a `node-id`, only that frame or its containing
114
+ top-level frame is imported; a mismatch falls back to all frames. This path
115
+ uses no Figma REST API quota and is scoped to screens only; it never creates
116
+ or updates a design system. This is separate from uploading `.fig` on the
114
117
  Design System Setup page, which still indexes tokens/brand-kit data through
115
118
  Builder and does not parse `.fig` locally. See the `design-systems` skill
116
119
  for both paths.
@@ -118,9 +121,19 @@ ladder.
118
121
  `figmeta.selectedNodeData`; `import-figma-clipboard` uses those before any
119
122
  heuristic matching and supports multi-selection. Clipboard metadata is not a
120
123
  public Figma contract, so a copied frame link remains the stable exact path
121
- if Figma changes that field. Without a token, current Figma's binary-only
122
- clipboard has no browser-readable HTML fallback; give setup guidance instead
123
- of claiming a successful import.
124
+ if Figma changes that field. Without a token, `import-figma-clipboard` falls
125
+ back to a local Kiwi binary decode: geometry, auto-layout, text, solid fills,
126
+ and strokes are editable immediately; image fills become annotated
127
+ placeholders (`data-figma-image-ref="<sha1>"`) that can be filled in later two
128
+ ways. (1) Token-free: upload the original `.fig` — the paste-result dialog's
129
+ "Fill images from .fig" option, or `hydrateFileIds` on the `.fig` upload route
130
+ (`/api/import-design-file`) — which matches each placeholder's SHA-1 hash to
131
+ the `.fig`'s embedded `images/` bytes, mirrors them to durable storage, and
132
+ needs no Figma API. (2) With a token: `hydrate-figma-paste-images` resolves the
133
+ same placeholders through Figma's REST image endpoint. This is not a
134
+ full-fidelity import — report which image fills are still unresolved and offer
135
+ to hydrate them. Clipboard paste is the fast no-quota path; the original `.fig`
136
+ is what carries the real image bytes for both upload and paste-hydration.
124
137
  - For "what's in this Figma file/frame?" or "show me a screenshot of this
125
138
  frame" without importing anything, use `get-figma-design-context` — no
126
139
  `nodeId` lists pages/top-level frames (like the official Figma MCP's
@@ -15,18 +15,19 @@ product must report them instead of claiming success.
15
15
 
16
16
  ## Capability matrix
17
17
 
18
- | Workflow or feature | Current behavior | Fidelity | Required verification |
19
- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
20
- | Figma frame URL / file key | Reads the exact node through `file_content:read`, converts it to a new Design screen, mirrors expiring images into durable storage, and returns a per-node fidelity report. | Mixed; see node matrix below. | REST fixture, authenticated file, screenshot comparison. |
21
- | Figma URL without a node id | Imports the first top-level object on the first page. A specific frame URL is recommended for deterministic results. | Same as node import. | Multi-page and empty-page fixtures. |
22
- | Figma branch URL | Uses the branch key and imports that branch's node. | Same as node import. | Main/branch pair with divergent content. |
23
- | Figma clipboard to Design | Uses private `figmeta.selectedNodeData` ids when present, then the same REST converter. Older visible HTML is only a fallback. Binary-only clipboard data without ids/token is not decoded. | Exact selection identity while Figma's private metadata shape remains compatible; node fidelity is mixed. | Real Chrome copy from single, multi, nested, and 100+ node selections. |
24
- | `.fig` upload | Bounded best-effort decoding of known Kiwi/ZIP variants into editable HTML. Embedded images are moved to durable storage. | Experimental. The format is proprietary and has no compatibility guarantee. | Corpus of real files from multiple Figma versions; never only generated containers. |
25
- | Design to Figma clipboard | Copies an SVG built from the live rendered DOM. Figma imports supported SVG primitives as editable layers. | Visual/vector handoff, not a native semantic round trip. Auto layout, variables, components, prototypes, HTML state, and code identity are not recreated by SVG. | Paste into real Figma and inspect layer types, text, images, effects, clipping, and bounds. |
26
- | Design SVG download | Same conversion as clipboard, with a server-render fallback when a live DOM is unavailable. | Same SVG limits; the export report lists approximations and omissions. | Live and server paths, selected layer and whole screen. |
27
- | Native Design to Figma write | Use Figma's official MCP `use_figma` write-to-canvas path when the connected client/account supports it. | Native Figma structures, subject to Figma MCP beta limitations and permissions. | Full-seat/edit-permission account and a real destination file. |
28
- | `.fig` download | Not supported. There is no documented public `.fig` authoring contract. | Unsupported. | Do not label SVG/ZIP as `.fig`. |
29
- | Open-ended Figma chat | Provider catalog/docs/request expose the REST surface allowed by the user's scoped token; non-read calls require approval. Native canvas authoring requires official Figma MCP, not a personal access token alone. | Endpoint-dependent. | Read scopes, expired/revoked token, rate limiting, Enterprise-variable permissions, MCP connection. |
18
+ | Workflow or feature | Current behavior | Fidelity | Required verification |
19
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | Figma frame URL / file key | Reads the exact node through `file_content:read`, converts it to a new Design screen, mirrors expiring images into durable storage, and returns a per-node fidelity report. | Mixed; see node matrix below. | REST fixture, authenticated file, screenshot comparison. |
21
+ | Figma URL without a node id | Imports the first top-level object on the first page. A specific frame URL is recommended for deterministic results. | Same as node import. | Multi-page and empty-page fixtures. |
22
+ | Figma branch URL | Uses the branch key and imports that branch's node. | Same as node import. | Main/branch pair with divergent content. |
23
+ | Figma clipboard to Design | Uses private `figmeta.selectedNodeData` ids when present, then the same REST converter. With a token: full fidelity matching `import-figma-frame`. Without a token: local Kiwi binary decode — geometry, auto-layout, text, solid fills, and strokes are editable; image fills are stamped with `data-figma-image-ref="<sha1>"` placeholders and can be resolved retroactively two ways: token-free by uploading the original `.fig` (the paste dialog's "Fill images from .fig" / `hydrateFileIds` on the `.fig` upload route), which matches each placeholder hash to the `.fig`'s embedded image bytes; or with a token via `hydrate-figma-paste-images`. | Exact selection identity while Figma's private metadata shape remains compatible; node fidelity is mixed. No-token imports resolve images retroactively — from the `.fig` (no quota) or a connected token. | Real Chrome copy from single, multi, nested, and 100+ node selections; token-less copy followed by `.fig` hydration and by deferred token connect, verifying image resolution both ways. |
24
+ | `.fig` upload | Bounded best-effort decoding of known Kiwi/ZIP variants into editable HTML. Embedded images are moved to durable storage. Optionally accepts a Figma frame URL: when its `node-id` matches the decoded file, Design imports only that top-level frame (or its ancestor for a nested node); a mismatch imports all frames with a warning. No Figma REST API calls are made. | Experimental. The format is proprietary and has no compatibility guarantee. | Corpus of real files from multiple Figma versions; never only generated containers. |
25
+ | `.fig` upload + frame URL | Accepts an optional Figma frame link. Normalizes the node-id and matches the decoded .fig GUID (`sessionID:localID`) to the matching top-level frame. Nested node IDs resolve to their top-level frame. On mismatch, all frames are imported. No Figma API quota is used. | Best-effort. The GUID mapping is reliable for frames in the same file but undocumented test with real files before relying on it. | Real .fig/frame-link pairs from Figma across file versions. |
26
+ | Design to Figma clipboard | Copies an SVG built from the live rendered DOM. Figma imports supported SVG primitives as editable layers. | Visual/vector handoff, not a native semantic round trip. Auto layout, variables, components, prototypes, HTML state, and code identity are not recreated by SVG. | Paste into real Figma and inspect layer types, text, images, effects, clipping, and bounds. |
27
+ | Design SVG download | Same conversion as clipboard, with a server-render fallback when a live DOM is unavailable. | Same SVG limits; the export report lists approximations and omissions. | Live and server paths, selected layer and whole screen. |
28
+ | Native Design to Figma write | Use Figma's official MCP `use_figma` write-to-canvas path when the connected client/account supports it. | Native Figma structures, subject to Figma MCP beta limitations and permissions. | Full-seat/edit-permission account and a real destination file. |
29
+ | `.fig` download | Not supported. There is no documented public `.fig` authoring contract. | Unsupported. | Do not label SVG/ZIP as `.fig`. |
30
+ | Open-ended Figma chat | Provider catalog/docs/request expose the REST surface allowed by the user's scoped token; non-read calls require approval. Native canvas authoring requires official Figma MCP, not a personal access token alone. | Endpoint-dependent. | Read scopes, expired/revoked token, rate limiting, Enterprise-variable permissions, MCP connection. |
30
31
 
31
32
  ## REST node conversion matrix
32
33
 
@@ -54,6 +55,18 @@ product must report them instead of claiming success.
54
55
  | Videos, emoji paints, FigJam-only and unknown node types | Rendered fallback when Figma can render the node. | Visual fallback only. |
55
56
  | Hidden or 0%-opacity subtrees | Omitted without downloading their assets. | Visually exact and avoids unnecessary work. |
56
57
 
58
+ ## Figma REST rate limits
59
+
60
+ - Viewer and Collab seats may receive up to 6 Tier 1 requests per month for
61
+ file, node, and image endpoints. The actual limit may be lower.
62
+ - Dev and Full seats receive 10–20 Tier 1 requests per minute, depending on the
63
+ resource's plan.
64
+ - On HTTP 429, Figma returns `Retry-After` in seconds plus
65
+ `X-Figma-Plan-Tier`, `X-Figma-Rate-Limit-Type`, and
66
+ `X-Figma-Upgrade-Link` metadata.
67
+ - Figma does not expose a requests-remaining counter.
68
+ - Clipboard paste and `.fig` upload are zero-quota local alternatives.
69
+
57
70
  ## Safety and scale limits
58
71
 
59
72
  - REST responses are capped at 4 MB. Multi-selection requests split
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Retroactively resolve unresolved Figma image fills for a screen that was
3
+ * imported via the local-kiwi clipboard path, using Figma's REST image
4
+ * endpoint (requires a saved FIGMA_ACCESS_TOKEN).
5
+ *
6
+ * When a Figma paste is imported without an access token, IMAGE fills render as
7
+ * `url("about:blank")` placeholders. The originating elements are annotated
8
+ * with `data-figma-image-ref="hash1 hash2 …"` so this action can find them
9
+ * without a full re-parse. Each Nth hash in the attribute maps to the Nth
10
+ * `url(&quot;about:blank&quot;)` occurrence in the element's style attribute.
11
+ *
12
+ * The read → collect → resolve → persist pipeline lives in
13
+ * `server/lib/figma-image-hydration.ts` and is shared with the token-free
14
+ * `.fig` hydration path (see `hydrateFileImagesFromFig`). This action only
15
+ * supplies the REST resolver.
16
+ */
17
+
18
+ import { defineAction } from "@agent-native/core";
19
+ import { z } from "zod";
20
+
21
+ import {
22
+ applyHydration,
23
+ collectImageRefHashes,
24
+ hydrateImageRefsInHtml,
25
+ loadHydratableFile,
26
+ } from "../server/lib/figma-image-hydration.js";
27
+ import { resolveImageFillRefs } from "../server/lib/figma-node-import.js";
28
+ import { readLiveSourceFile } from "../server/source-workspace.js";
29
+
30
+ // Re-exported for direct unit testing of the pure HTML helpers.
31
+ export { collectImageRefHashes, hydrateImageRefsInHtml };
32
+
33
+ export default defineAction({
34
+ description:
35
+ 'Retroactively resolve unresolved Figma image fills for a screen imported via the no-token local-kiwi path (import-figma-clipboard returned strategy:"localKiwi"). Requires a saved FIGMA_ACCESS_TOKEN. Fetches CDN URLs from Figma\'s /files/:key/images endpoint, mirrors them to durable blob storage, and replaces every url("about:blank") placeholder stamped by the local-kiwi decoder with the real durable URL. Fully resolved elements have their data-figma-image-ref annotation removed; partially resolved elements retain it for a future retry. Returns resolved/missing/skipped counts. Call after connecting Figma in Settings to fill in images from a no-token paste. For a token-free path when the original .fig file is available, upload the .fig with hydrateFileIds via /api/import-design-file instead.',
36
+ schema: z.object({
37
+ fileId: z
38
+ .string()
39
+ .describe(
40
+ "ID of the design_files row to hydrate. Use the fileId returned by import-figma-clipboard.",
41
+ ),
42
+ }),
43
+ run: async ({ fileId }) => {
44
+ const { workspaceFile, designId, figmaFileKey } =
45
+ await loadHydratableFile(fileId);
46
+
47
+ if (!figmaFileKey) {
48
+ throw new Error(
49
+ `No Figma file key found for file ${fileId}. This file may not have been imported via a Figma clipboard paste.`,
50
+ );
51
+ }
52
+
53
+ const live = await readLiveSourceFile(workspaceFile);
54
+
55
+ const hashesToResolve = collectImageRefHashes(live.content);
56
+ if (hashesToResolve.length === 0) {
57
+ return {
58
+ fileId,
59
+ resolved: 0,
60
+ missing: 0,
61
+ skipped: 0,
62
+ message: "No unresolved image refs found in this file.",
63
+ };
64
+ }
65
+
66
+ let resolvedUrls: Map<string, string>;
67
+ try {
68
+ resolvedUrls = await resolveImageFillRefs(figmaFileKey, hashesToResolve);
69
+ } catch (err) {
70
+ const msg = err instanceof Error ? err.message : String(err);
71
+ if (/quota cooldown|provider.*quota/i.test(msg)) {
72
+ const retryAfterSeconds =
73
+ (err as { retryAfterSeconds?: number }).retryAfterSeconds ?? 0;
74
+ const waitHint =
75
+ retryAfterSeconds > 0
76
+ ? retryAfterSeconds >= 60
77
+ ? `${Math.ceil(retryAfterSeconds / 60)} min`
78
+ : `${retryAfterSeconds}s`
79
+ : "~1 min";
80
+ throw Object.assign(
81
+ new Error(`Figma API rate limited — try again in ${waitHint}.`),
82
+ { statusCode: 429 },
83
+ );
84
+ }
85
+ throw err;
86
+ }
87
+
88
+ if (resolvedUrls.size === 0) {
89
+ return {
90
+ fileId,
91
+ resolved: 0,
92
+ missing: hashesToResolve.length,
93
+ skipped: 0,
94
+ message: `Figma returned no image URLs for ${hashesToResolve.length} hash${hashesToResolve.length === 1 ? "" : "es"}. The images may have been deleted from the Figma file or the access token lacks file_content:read scope.`,
95
+ };
96
+ }
97
+
98
+ const result = await applyHydration({
99
+ file: workspaceFile,
100
+ designId,
101
+ fileId,
102
+ liveContent: live.content,
103
+ liveVersionHash: live.versionHash,
104
+ requestedHashes: hashesToResolve,
105
+ resolvedUrls,
106
+ });
107
+
108
+ return {
109
+ fileId,
110
+ resolved: result.resolved,
111
+ missing: result.missing,
112
+ skipped: result.skipped,
113
+ };
114
+ },
115
+ });
@@ -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,