@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
@@ -0,0 +1,273 @@
1
+ /**
2
+ * FigmaHydrationDialog — shown after a no-token local-kiwi clipboard import
3
+ * when IMAGE fills couldn't be resolved. Collects a Figma access token, saves
4
+ * it, then calls `hydrate-figma-paste-images` for each imported file to
5
+ * replace the `url("about:blank")` placeholders with real durable images.
6
+ */
7
+
8
+ import { callAction } from "@agent-native/core/client/hooks";
9
+ import { useT } from "@agent-native/core/client/i18n";
10
+ import { useEffect, useRef, useState } from "react";
11
+ import { toast } from "sonner";
12
+
13
+ import { Button } from "@/components/ui/button";
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogDescription,
18
+ DialogFooter,
19
+ DialogHeader,
20
+ DialogTitle,
21
+ } from "@/components/ui/dialog";
22
+ import { Input } from "@/components/ui/input";
23
+ import { Label } from "@/components/ui/label";
24
+ import {
25
+ hydrateImagesFromFig,
26
+ validateFigUploadFile,
27
+ } from "@/lib/design-file-upload";
28
+ import {
29
+ getFigmaConnectionStatus,
30
+ saveFigmaAccessToken,
31
+ } from "@/lib/figma-connection";
32
+
33
+ interface FigmaHydrationDialogProps {
34
+ open: boolean;
35
+ onOpenChange: (open: boolean) => void;
36
+ designId: string;
37
+ fileIds: string[];
38
+ imageCount: number;
39
+ onHydrated: () => void;
40
+ }
41
+
42
+ export function FigmaHydrationDialog({
43
+ open,
44
+ onOpenChange,
45
+ designId,
46
+ fileIds,
47
+ imageCount,
48
+ onHydrated,
49
+ }: FigmaHydrationDialogProps) {
50
+ const t = useT();
51
+ const [token, setToken] = useState("");
52
+ const [busy, setBusy] = useState(false);
53
+ const [error, setError] = useState<string | null>(null);
54
+ const [docsUrl, setDocsUrl] = useState<string | null>(null);
55
+ const figInputRef = useRef<HTMLInputElement>(null);
56
+
57
+ const screensPlural = fileIds.length === 1 ? "" : "s";
58
+ const imagePlural = imageCount === 1 ? "" : "s";
59
+
60
+ useEffect(() => {
61
+ if (!open) return;
62
+ getFigmaConnectionStatus()
63
+ .then((status) => {
64
+ if (status.docsUrl) setDocsUrl(status.docsUrl);
65
+ })
66
+ .catch(() => {});
67
+ }, [open]);
68
+
69
+ async function handleFigSelected(e: React.ChangeEvent<HTMLInputElement>) {
70
+ const file = e.target.files?.[0];
71
+ e.target.value = "";
72
+ if (!file) return;
73
+ if (validateFigUploadFile(file)) {
74
+ setError(t("designEditor.import.figmaHydrationInvalidFig"));
75
+ return;
76
+ }
77
+ setBusy(true);
78
+ setError(null);
79
+ try {
80
+ const result = await hydrateImagesFromFig({
81
+ designId,
82
+ file,
83
+ fileIds,
84
+ fallbackErrorMessage: t("designEditor.import.figmaHydrationFigError"),
85
+ });
86
+ if (result.error) {
87
+ setError(result.error);
88
+ return;
89
+ }
90
+ const totalResolved = result.totalResolved ?? 0;
91
+ onOpenChange(false);
92
+ setToken("");
93
+ onHydrated();
94
+ toast.success(t("designEditor.import.figmaHydrationSuccess"), {
95
+ description: t(
96
+ "designEditor.import.figmaHydrationFigSuccessDescription",
97
+ { count: totalResolved, plural: totalResolved === 1 ? "" : "s" },
98
+ ),
99
+ });
100
+ } catch (err) {
101
+ setError(
102
+ err instanceof Error
103
+ ? err.message
104
+ : t("designEditor.import.figmaHydrationFigError"),
105
+ );
106
+ } finally {
107
+ setBusy(false);
108
+ }
109
+ }
110
+
111
+ async function handleSubmit(e: React.FormEvent) {
112
+ e.preventDefault();
113
+ if (!token.trim()) return;
114
+ setBusy(true);
115
+ setError(null);
116
+ try {
117
+ const status = await saveFigmaAccessToken(token.trim());
118
+ if (status.docsUrl) setDocsUrl(status.docsUrl);
119
+
120
+ let totalResolved = 0;
121
+ for (const fileId of fileIds) {
122
+ const result = await callAction<{
123
+ resolved?: number;
124
+ missing?: number;
125
+ }>("hydrate-figma-paste-images", { fileId });
126
+ totalResolved += result?.resolved ?? 0;
127
+ }
128
+
129
+ onOpenChange(false);
130
+ setToken("");
131
+ onHydrated();
132
+ toast.success(t("designEditor.import.figmaHydrationSuccess"), {
133
+ description: t("designEditor.import.figmaHydrationSuccessDescription", {
134
+ count: totalResolved,
135
+ plural: totalResolved === 1 ? "" : "s",
136
+ }),
137
+ });
138
+ } catch (err) {
139
+ const message =
140
+ err instanceof Error ? err.message : t("common.genericError");
141
+ const is403 =
142
+ message.includes("403") || message.toLowerCase().includes("forbidden");
143
+ const isServerError = /internal server error/i.test(message);
144
+ setError(
145
+ is403
146
+ ? 'Token rejected (403). In Figma\'s token settings, enable the "File content" and "Current user" scopes, then generate a new token.'
147
+ : isServerError
148
+ ? "Server error — Figma's API may be rate-limited. Wait ~1 minute then try again; repeated retries extend the cooldown."
149
+ : message,
150
+ );
151
+ } finally {
152
+ setBusy(false);
153
+ }
154
+ }
155
+
156
+ return (
157
+ <Dialog open={open} onOpenChange={onOpenChange}>
158
+ <DialogContent className="sm:max-w-md">
159
+ <form
160
+ onSubmit={(e) => {
161
+ void handleSubmit(e);
162
+ }}
163
+ >
164
+ <DialogHeader>
165
+ <DialogTitle>
166
+ {t("designEditor.import.figmaHydrationDialogTitle")}
167
+ </DialogTitle>
168
+ <DialogDescription>
169
+ {t("designEditor.import.figmaHydrationDialogDescription", {
170
+ count: imageCount,
171
+ plural: imagePlural,
172
+ screensPlural,
173
+ })}
174
+ </DialogDescription>
175
+ </DialogHeader>
176
+
177
+ <div className="mt-4 rounded-md border border-border bg-muted/30 p-3">
178
+ <div className="flex items-center gap-2">
179
+ <p className="text-xs font-medium text-foreground">
180
+ {t("designEditor.import.figmaHydrationFigTitle")}
181
+ </p>
182
+ <span className="rounded-full bg-primary/10 px-1.5 py-px text-[9px] font-semibold uppercase tracking-wide text-primary">
183
+ {t("designEditor.import.figmaHydrationRecommended")}
184
+ </span>
185
+ </div>
186
+ <p className="mt-1 text-[10px] leading-snug text-muted-foreground">
187
+ {t("designEditor.import.figmaHydrationFigOption")}
188
+ </p>
189
+ <input
190
+ ref={figInputRef}
191
+ type="file"
192
+ accept=".fig"
193
+ className="hidden"
194
+ onChange={(e) => void handleFigSelected(e)}
195
+ />
196
+ <Button
197
+ type="button"
198
+ size="sm"
199
+ variant="secondary"
200
+ className="mt-2 w-full"
201
+ disabled={busy}
202
+ onClick={() => figInputRef.current?.click()}
203
+ >
204
+ {t("designEditor.import.figmaHydrationChooseFig")}
205
+ </Button>
206
+ </div>
207
+
208
+ <div className="mt-3 text-center text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
209
+ {t("designEditor.import.figmaHydrationOrToken")}
210
+ </div>
211
+
212
+ <div className="mt-2 space-y-2">
213
+ <div className="flex items-center justify-between gap-2">
214
+ <Label htmlFor="figma-hydration-token" className="text-xs">
215
+ {t("designEditor.import.figmaTokenLabel")}
216
+ </Label>
217
+ {docsUrl ? (
218
+ <a
219
+ href={docsUrl}
220
+ target="_blank"
221
+ rel="noreferrer"
222
+ className="shrink-0 text-[10px] font-medium text-foreground underline-offset-2 hover:underline"
223
+ >
224
+ {t("designEditor.import.figmaTokenDocs")}
225
+ </a>
226
+ ) : null}
227
+ </div>
228
+ <Input
229
+ id="figma-hydration-token"
230
+ type="password"
231
+ value={token}
232
+ onChange={(e) => setToken(e.target.value)}
233
+ placeholder={t("designEditor.import.figmaTokenPlaceholder")}
234
+ autoComplete="new-password"
235
+ aria-invalid={error ? true : undefined}
236
+ className="h-8 text-xs"
237
+ disabled={busy}
238
+ />
239
+ {error ? (
240
+ <p className="rounded-md border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-[10px] leading-snug text-destructive">
241
+ {error}
242
+ </p>
243
+ ) : (
244
+ <div className="space-y-1">
245
+ <p className="text-[10px] leading-snug text-muted-foreground">
246
+ {t("designEditor.import.figmaHydrationTokenDescription")}
247
+ </p>
248
+ <p className="text-[10px] leading-snug text-muted-foreground/70">
249
+ {t("designEditor.import.figmaHydrationRateLimit")}
250
+ </p>
251
+ </div>
252
+ )}
253
+ </div>
254
+
255
+ <DialogFooter className="mt-4">
256
+ <Button
257
+ type="button"
258
+ variant="ghost"
259
+ size="sm"
260
+ onClick={() => onOpenChange(false)}
261
+ disabled={busy}
262
+ >
263
+ {t("home.cancel")}
264
+ </Button>
265
+ <Button type="submit" size="sm" disabled={busy || !token.trim()}>
266
+ {t("designEditor.import.figmaHydrationConnectAndLoad")}
267
+ </Button>
268
+ </DialogFooter>
269
+ </form>
270
+ </DialogContent>
271
+ </Dialog>
272
+ );
273
+ }
@@ -455,6 +455,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
455
455
  onBoardElementDblClickText,
456
456
  onBoardIframeHotkey,
457
457
  onBoardFigmaClipboardPaste,
458
+ onBoardImagePaste,
458
459
  onBoardIframeContextMenu,
459
460
  onBoardTextEditingStateChange,
460
461
  boardClearSelectionRequest,
@@ -7670,6 +7671,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7670
7671
  onClearSelection={onBoardElementClear}
7671
7672
  onIframeHotkey={onBoardIframeHotkey}
7672
7673
  onFigmaClipboardPaste={onBoardFigmaClipboardPaste}
7674
+ onImagePaste={onBoardImagePaste}
7673
7675
  onIframeContextMenu={onBoardIframeContextMenu}
7674
7676
  onVisualStructureChange={onBoardVisualStructureChange}
7675
7677
  onVisualStyleChange={onBoardVisualStyleChange}
@@ -9170,14 +9172,12 @@ const Screen = memo(function Screen({
9170
9172
  // Memoize the srcdoc with the hit-test responder injected so we don't
9171
9173
  // rebuild the string every render (that would reload the iframe).
9172
9174
  // Keyed only on screen.content; the hit-test script itself is constant.
9173
- const srcdocWithHitTest = useMemo(
9174
- () =>
9175
- injectSessionReplayIframeBootstrap(
9176
- appendHitTestResponder(screen.content),
9177
- ),
9175
+ const srcdocWithHitTest = useMemo(() => {
9176
+ return injectSessionReplayIframeBootstrap(
9177
+ appendHitTestResponder(screen.content),
9178
+ );
9178
9179
  // eslint-disable-next-line react-hooks/exhaustive-deps
9179
- [screen.content],
9180
- );
9180
+ }, [screen.content]);
9181
9181
 
9182
9182
  const updateDirectHover = useCallback((next: boolean) => {
9183
9183
  setDirectlyHovered((current) => (current === next ? current : next));
@@ -10447,15 +10447,63 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
10447
10447
  }
10448
10448
  var content = getFigmaClipboardContent(e.clipboardData);
10449
10449
  clearPendingPlainPasteHotkey();
10450
- if (!content) return;
10451
- stopNativeInteraction(e);
10452
- (window.parent as Window).postMessage(
10453
- {
10454
- type: "figma-clipboard-paste",
10455
- content: content,
10456
- },
10457
- "*",
10458
- );
10450
+ if (content) {
10451
+ stopNativeInteraction(e);
10452
+ (window.parent as Window).postMessage(
10453
+ { type: "figma-clipboard-paste", content: content },
10454
+ "*",
10455
+ );
10456
+ return;
10457
+ }
10458
+ // Relay image files pasted while the canvas has focus (e.g. "Copy as PNG"
10459
+ // from Figma, or a screenshot). The parent's handleEditorPaste cannot see
10460
+ // these because paste events inside an iframe don't bubble to the parent
10461
+ // document — the bridge reads each file as a data URL and relays it so
10462
+ // the parent's handlePastedImageFiles can insert an <img> layer.
10463
+ var imageFiles = Array.from(e.clipboardData?.items ?? [])
10464
+ .filter(function (item) {
10465
+ return item.kind === "file" && item.type.startsWith("image/");
10466
+ })
10467
+ .map(function (item) {
10468
+ return item.getAsFile();
10469
+ })
10470
+ .filter(function (f): f is File {
10471
+ return Boolean(f);
10472
+ });
10473
+ if (imageFiles.length > 0) {
10474
+ stopNativeInteraction(e);
10475
+ var readPromises = imageFiles.map(function (file) {
10476
+ return new Promise<{
10477
+ dataUrl: string;
10478
+ type: string;
10479
+ name: string;
10480
+ } | null>(function (resolve) {
10481
+ var reader = new FileReader();
10482
+ reader.onload = function () {
10483
+ resolve({
10484
+ dataUrl: typeof reader.result === "string" ? reader.result : "",
10485
+ type: file.type,
10486
+ name: file.name,
10487
+ });
10488
+ };
10489
+ reader.onerror = function () {
10490
+ resolve(null);
10491
+ };
10492
+ reader.readAsDataURL(file);
10493
+ });
10494
+ });
10495
+ void Promise.all(readPromises).then(function (results) {
10496
+ var valid = results.filter(function (r) {
10497
+ return r && r.dataUrl;
10498
+ });
10499
+ if (valid.length > 0) {
10500
+ (window.parent as Window).postMessage(
10501
+ { type: "canvas-image-paste", files: valid },
10502
+ "*",
10503
+ );
10504
+ }
10505
+ });
10506
+ }
10459
10507
  },
10460
10508
  true,
10461
10509
  );
@@ -14,6 +14,16 @@ export interface IframeFigmaClipboardPastePayload {
14
14
  content: string;
15
15
  }
16
16
 
17
+ export interface IframeImagePasteFile {
18
+ dataUrl: string;
19
+ type: string;
20
+ name: string;
21
+ }
22
+
23
+ export interface IframeImagePastePayload {
24
+ files: IframeImagePasteFile[];
25
+ }
26
+
17
27
  export interface IframeContextMenuPayload {
18
28
  screenId?: string;
19
29
  clientX: number;
@@ -279,7 +279,14 @@ function stripExecutableStaticPreviewContent(html: string) {
279
279
  .replace(/<(?:iframe|object|embed|audio|video|source)\b[^>]*\/?\s*>/gi, "")
280
280
  .replace(/<(?:link|meta|base)\b[^>]*>/gi, "")
281
281
  .replace(/@import\s+(?:url\([^)]*\)|["'][^"']*["'])\s*[^;]*;/gi, "")
282
- .replace(/url\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi, "none")
282
+ .replace(/url\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi, (match) => {
283
+ // Strip data:, blob:, and unknown-scheme url() references to prevent
284
+ // large embedded payloads or local-resource leaks. Allow https:// URLs
285
+ // (CDN images/fonts) — they can only fetch inert assets, not execute code.
286
+ const raw = match.slice(4, -1).trim();
287
+ const inner = raw.replace(/^['"]|['"]$/g, "").trim();
288
+ return /^https:\/\//i.test(inner) ? match : "none";
289
+ })
283
290
  .replace(/<(?:img|image|use)\b[^>]*>/gi, (tag) =>
284
291
  tag.replace(
285
292
  /\s+(?:src|srcset|href|xlink:href)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi,
@@ -10,6 +10,7 @@ import type {
10
10
  IframeContextMenuPayload,
11
11
  IframeFigmaClipboardPastePayload,
12
12
  IframeHotkeyPayload,
13
+ IframeImagePastePayload,
13
14
  } from "../design-canvas/iframe-events";
14
15
  import type {
15
16
  DeviceFrameType,
@@ -363,6 +364,7 @@ export interface MultiScreenCanvasProps {
363
364
  onBoardFigmaClipboardPaste?: (
364
365
  event: IframeFigmaClipboardPastePayload,
365
366
  ) => void;
367
+ onBoardImagePaste?: (event: IframeImagePastePayload) => void;
366
368
  onBoardIframeContextMenu?: (event: IframeContextMenuPayload) => void;
367
369
  onBoardTextEditingStateChange?: (state: {
368
370
  active: boolean;
@@ -940,6 +942,7 @@ export type {
940
942
  IframeContextMenuPayload,
941
943
  IframeFigmaClipboardPastePayload,
942
944
  IframeHotkeyPayload,
945
+ IframeImagePastePayload,
943
946
  };
944
947
 
945
948
  export interface ResolvedScreenMetadata {