@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/a2a-protocol.mdx +15 -0
- package/corpus/core/docs/content/template-chat.mdx +1 -1
- package/corpus/core/docs/content/template-plan.mdx +3 -3
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/activity.ts +371 -0
- package/corpus/core/src/a2a/client.ts +3 -1
- package/corpus/core/src/a2a/index.ts +21 -0
- package/corpus/core/src/a2a/types.ts +8 -0
- package/corpus/core/src/agent/model-config.ts +19 -23
- package/corpus/core/src/agent/production-agent.ts +167 -100
- package/corpus/core/src/agent/types.ts +16 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +190 -35
- package/corpus/core/src/client/chat-model-groups.ts +67 -27
- package/corpus/core/src/client/sharing/ShareButton.tsx +1 -27
- package/corpus/core/src/client/sharing/ShareDialog.tsx +0 -16
- package/corpus/core/src/client/sharing/useShareButtonController.ts +0 -7
- package/corpus/core/src/client/sharing/useShareDialogController.ts +0 -2
- package/corpus/core/src/client/sse-event-processor.ts +85 -1
- package/corpus/core/src/ingestion/figma-node-to-html.ts +20 -99
- package/corpus/core/src/ingestion/figma-paint-math.ts +371 -0
- package/corpus/core/src/ingestion/index.ts +20 -0
- package/corpus/core/src/localization/default-messages.ts +6 -0
- package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -18
- package/corpus/core/src/scripts/call-agent.ts +49 -6
- package/corpus/core/src/server/agent-chat-plugin.ts +50 -10
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -0
- package/corpus/templates/assets/app/routes/library.tsx +17 -9
- package/corpus/templates/clips/changelog/2026-07-23-fixed-recordings-failing-to-save-with-a-cancelled-or-cleanup.md +6 -0
- package/corpus/templates/clips/server/plugins/db.ts +9 -1
- package/corpus/templates/design/AGENTS.md +18 -5
- package/corpus/templates/design/FIGMA_INTEROPERABILITY.md +25 -12
- package/corpus/templates/design/actions/hydrate-figma-paste-images.ts +115 -0
- package/corpus/templates/design/actions/import-figma-clipboard.ts +83 -18
- package/corpus/templates/design/actions/import-figma-frame.ts +34 -21
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +28 -1
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +106 -11
- package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +273 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -7
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +57 -9
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +10 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +8 -1
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +150 -2
- package/corpus/templates/design/app/lib/design-file-upload.ts +85 -0
- package/corpus/templates/design/app/lib/design-import.ts +19 -1
- package/corpus/templates/design/app/lib/figma-clipboard.ts +110 -10
- package/corpus/templates/design/changelog/2026-07-20-paste-figma-frames-directly-onto-the-canvas-no-token-needed.md +6 -0
- package/corpus/templates/design/changelog/2026-07-21-fig-upload-frame-url-and-rate-limit-errors.md +5 -0
- package/corpus/templates/design/changelog/2026-07-22-connecting-a-figma-token-now-actually-fills-in-a-pasted-desi.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-figma-line-arrow-vectors-and-stroked-icons-now-render-instea.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-fill-a-no-token-figma-paste-s-images-by-dropping-the-origina.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-imported-and-pasted-figma-frames-now-render-faithfully-fixed.md +6 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +50 -0
- package/corpus/templates/design/server/lib/fig-file-decoder.ts +90 -49
- package/corpus/templates/design/server/lib/fig-file-import.ts +19 -5
- package/corpus/templates/design/server/lib/fig-file-to-html.ts +583 -78
- package/corpus/templates/design/server/lib/figma-clipboard-local-decode.ts +238 -0
- package/corpus/templates/design/server/lib/figma-image-hydration.ts +389 -0
- package/corpus/templates/design/server/lib/figma-node-import.ts +136 -36
- package/corpus/templates/design/server/register-secrets.ts +14 -2
- package/dist/a2a/activity.d.ts +50 -0
- package/dist/a2a/activity.d.ts.map +1 -0
- package/dist/a2a/activity.js +236 -0
- package/dist/a2a/activity.js.map +1 -0
- package/dist/a2a/client.d.ts +2 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +1 -1
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/index.d.ts +2 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -0
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/types.d.ts +1 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts +2 -2
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +16 -16
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +19 -23
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/production-agent.d.ts +26 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +137 -80
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/types.d.ts +10 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +63 -14
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/chat-model-groups.d.ts.map +1 -1
- package/dist/client/chat-model-groups.js +44 -21
- package/dist/client/chat-model-groups.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +1 -1
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +3 -4
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/sharing/ShareDialog.d.ts.map +1 -1
- package/dist/client/sharing/ShareDialog.js +1 -2
- package/dist/client/sharing/ShareDialog.js.map +1 -1
- package/dist/client/sharing/useShareButtonController.d.ts +0 -1
- package/dist/client/sharing/useShareButtonController.d.ts.map +1 -1
- package/dist/client/sharing/useShareButtonController.js +0 -6
- package/dist/client/sharing/useShareButtonController.js.map +1 -1
- package/dist/client/sharing/useShareDialogController.d.ts +0 -1
- package/dist/client/sharing/useShareDialogController.d.ts.map +1 -1
- package/dist/client/sharing/useShareDialogController.js +0 -1
- package/dist/client/sharing/useShareDialogController.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +12 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +63 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/ingestion/figma-node-to-html.d.ts.map +1 -1
- package/dist/ingestion/figma-node-to-html.js +14 -88
- package/dist/ingestion/figma-node-to-html.js.map +1 -1
- package/dist/ingestion/figma-paint-math.d.ts +157 -0
- package/dist/ingestion/figma-paint-math.d.ts.map +1 -0
- package/dist/ingestion/figma-paint-math.js +274 -0
- package/dist/ingestion/figma-paint-math.js.map +1 -0
- package/dist/ingestion/index.d.ts +1 -0
- package/dist/ingestion/index.d.ts.map +1 -1
- package/dist/ingestion/index.js +1 -0
- package/dist/ingestion/index.js.map +1 -1
- package/dist/localization/default-messages.d.ts +6 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +6 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.js +1 -18
- package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +49 -6
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +38 -10
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -0
- package/docs/content/a2a-protocol.mdx +15 -0
- package/docs/content/template-chat.mdx +1 -1
- package/docs/content/template-plan.mdx +3 -3
- package/package.json +1 -1
- package/src/a2a/activity.ts +371 -0
- package/src/a2a/client.ts +3 -1
- package/src/a2a/index.ts +21 -0
- package/src/a2a/types.ts +8 -0
- package/src/agent/model-config.ts +19 -23
- package/src/agent/production-agent.ts +167 -100
- package/src/agent/types.ts +16 -1
- package/src/client/chat/tool-call-display.tsx +190 -35
- package/src/client/chat-model-groups.ts +67 -27
- package/src/client/sharing/ShareButton.tsx +1 -27
- package/src/client/sharing/ShareDialog.tsx +0 -16
- package/src/client/sharing/useShareButtonController.ts +0 -7
- package/src/client/sharing/useShareDialogController.ts +0 -2
- package/src/client/sse-event-processor.ts +85 -1
- package/src/ingestion/figma-node-to-html.ts +20 -99
- package/src/ingestion/figma-paint-math.ts +371 -0
- package/src/ingestion/index.ts +20 -0
- package/src/localization/default-messages.ts +6 -0
- package/src/scripts/agent-engines/list-agent-engines.ts +1 -18
- package/src/scripts/call-agent.ts +49 -6
- package/src/server/agent-chat-plugin.ts +50 -10
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -0
- package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
- package/dist/observability/hosted-model-experiment.d.ts +0 -39
- package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
- package/dist/observability/hosted-model-experiment.js +0 -90
- package/dist/observability/hosted-model-experiment.js.map +0 -1
- 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
|
-
|
|
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 } =
|
|
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 } =
|
|
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
|
|
184
|
-
//
|
|
185
|
-
//
|
|
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
|
-
|
|
82
|
-
|
|
82
|
+
try {
|
|
83
|
+
const nodeId = await resolveTargetNodeId(fileKey, requestedNodeId);
|
|
84
|
+
const rootNode = await fetchFigmaNode(fileKey, nodeId);
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const saved = await saveImportedDesignFiles({
|
|
95
|
+
designId,
|
|
96
|
+
sourceType: "figma-import",
|
|
97
|
+
files,
|
|
98
|
+
});
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
559
|
+
description={t("designEditor.import.figUploadDescriptionShort")}
|
|
465
560
|
isOpen={activeMode === "fig-upload"}
|
|
466
561
|
onToggle={() =>
|
|
467
562
|
setActiveMode((mode) =>
|