@agent-native/core 0.163.4 → 0.164.3

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 (228) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +78 -19
  3. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
  4. package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +4 -4
  5. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +6 -0
  7. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
  8. package/corpus/templates/analytics/app/routes/_index.tsx +3 -2
  9. package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +7 -2
  10. package/corpus/templates/analytics/app/routes/dashboard.tsx +3 -2
  11. package/corpus/templates/analytics/app/routes/overview.tsx +3 -2
  12. package/corpus/templates/analytics/app/routes/traffic.tsx +3 -2
  13. package/corpus/templates/analytics/changelog/2026-08-18-verify-cross-app-feature-flag-changes.md +6 -0
  14. package/corpus/templates/analytics/netlify.toml +2 -0
  15. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +196 -48
  16. package/corpus/templates/analytics/server/plugins/feature-flags.ts +5 -0
  17. package/corpus/templates/analytics/shared/feature-flags.ts +10 -0
  18. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +5 -1
  19. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +4 -1
  20. package/corpus/templates/assets/app/routes/libraries.tsx +4 -1
  21. package/corpus/templates/assets/app/routes/picker.tsx +4 -1
  22. package/corpus/templates/assets/netlify.toml +2 -0
  23. package/corpus/templates/brain/netlify.toml +2 -0
  24. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
  25. package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
  26. package/corpus/templates/calendar/netlify.toml +2 -0
  27. package/corpus/templates/chat/netlify.toml +2 -0
  28. package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
  29. package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
  30. package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
  31. package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
  32. package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
  33. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
  34. package/corpus/templates/clips/app/routes/_app.wispr.tsx +2 -1
  35. package/corpus/templates/clips/app/routes/_index.tsx +7 -3
  36. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
  37. package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
  38. package/corpus/templates/clips/netlify.toml +2 -0
  39. package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
  40. package/corpus/templates/content/netlify.toml +2 -0
  41. package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
  42. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
  43. package/corpus/templates/design/AGENTS.md +3 -1
  44. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
  45. package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
  46. package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
  47. package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
  48. package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
  49. package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
  50. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
  51. package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
  52. package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
  53. package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
  54. package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
  55. package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
  56. package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
  57. package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
  58. package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
  59. package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
  60. package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
  61. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
  62. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
  63. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
  64. package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
  65. package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
  66. package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
  67. package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
  68. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
  69. package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
  70. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
  71. package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
  72. package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
  73. package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
  74. package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
  75. package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
  76. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
  77. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
  78. package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
  79. package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
  80. package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
  81. package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
  82. package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
  83. package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
  84. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
  85. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
  86. package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
  87. package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
  88. package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
  89. package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
  90. package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
  91. package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
  92. package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
  93. package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
  94. package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
  95. package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
  96. package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
  97. package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
  98. package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
  99. package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
  100. package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
  101. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
  102. package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
  103. package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
  104. package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
  105. package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
  106. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
  107. package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
  108. package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
  109. package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
  110. package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
  111. package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
  112. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
  113. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
  114. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
  115. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
  116. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
  117. package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
  118. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
  119. package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
  120. package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
  121. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
  122. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
  123. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
  124. package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
  125. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
  126. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
  127. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
  128. package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
  129. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
  130. package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
  131. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
  132. package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
  133. package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
  134. package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
  135. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
  136. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
  137. package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
  138. package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
  139. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
  140. package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
  141. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
  142. package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
  143. package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
  144. package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
  145. package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
  146. package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
  147. package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
  148. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
  149. package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
  150. package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
  151. package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
  152. package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
  153. package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
  154. package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
  155. package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
  156. package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
  157. package/corpus/templates/design/app/routes/examples.tsx +2 -1
  158. package/corpus/templates/design/netlify.toml +2 -0
  159. package/corpus/templates/design/shared/board-file.ts +2 -1
  160. package/corpus/templates/dispatch/netlify.toml +2 -0
  161. package/corpus/templates/forms/app/routes/_index.tsx +3 -2
  162. package/corpus/templates/forms/netlify.toml +2 -0
  163. package/corpus/templates/mail/app/routes/_index.tsx +3 -2
  164. package/corpus/templates/mail/netlify.toml +2 -0
  165. package/corpus/templates/plan/netlify.toml +2 -0
  166. package/corpus/templates/slides/netlify.toml +2 -0
  167. package/corpus/templates/tasks/app/routes/_index.tsx +2 -1
  168. package/dist/agent/engine/builder-engine.d.ts +6 -4
  169. package/dist/agent/engine/builder-engine.js +91 -26
  170. package/dist/agent/engine/registry.js +16 -0
  171. package/dist/cli/code-agent-agent-tools.d.ts +2 -0
  172. package/dist/cli/code-agent-agent-tools.js +188 -0
  173. package/dist/cli/code-agent-collaboration.d.ts +34 -0
  174. package/dist/cli/code-agent-collaboration.js +104 -0
  175. package/dist/cli/code-agent-executor.js +6 -0
  176. package/dist/cli/code-agent-schedules.d.ts +54 -0
  177. package/dist/cli/code-agent-schedules.js +198 -0
  178. package/dist/client/AgentPanel.js +1 -1
  179. package/dist/client/ConnectBuilderCard.js +5 -2
  180. package/dist/client/EnvironmentBadge.js +6 -5
  181. package/dist/client/chat/run-recovery.js +3 -2
  182. package/dist/client/chat/tool-call-display.js +2 -3
  183. package/dist/client/components/CodeRequiredDialog.js +4 -2
  184. package/dist/client/i18n.js +5 -4
  185. package/dist/client/settings/SettingsPanel.js +1 -1
  186. package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
  187. package/dist/client/settings/useBuilderStatus.d.ts +6 -1
  188. package/dist/client/settings/useBuilderStatus.js +22 -33
  189. package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
  190. package/dist/code-agents/index.d.ts +2 -0
  191. package/dist/code-agents/index.js +2 -0
  192. package/dist/collab/awareness.d.ts +2 -2
  193. package/dist/collab/routes.d.ts +1 -1
  194. package/dist/deploy/build.js +10 -4
  195. package/dist/localization/shared.d.ts +2 -0
  196. package/dist/localization/shared.js +5 -0
  197. package/dist/mcp-client/oauth-client.d.ts +1 -0
  198. package/dist/mcp-client/oauth-client.js +50 -1
  199. package/dist/notifications/routes.d.ts +3 -3
  200. package/dist/observability/routes.d.ts +1 -1
  201. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  202. package/dist/server/auth-marketing-locales.d.ts +7 -0
  203. package/dist/server/auth-marketing-locales.js +1322 -0
  204. package/dist/server/auth-marketing.d.ts +1 -0
  205. package/dist/server/auth-marketing.js +35 -6
  206. package/dist/server/auth.js +4 -17
  207. package/dist/server/builder-browser.d.ts +22 -0
  208. package/dist/server/builder-browser.js +77 -1
  209. package/dist/server/builder-oauth.d.ts +50 -0
  210. package/dist/server/builder-oauth.js +263 -0
  211. package/dist/server/core-routes-plugin.d.ts +11 -0
  212. package/dist/server/core-routes-plugin.js +291 -415
  213. package/dist/server/credential-provider.d.ts +1 -1
  214. package/dist/server/credential-provider.js +1 -1
  215. package/dist/server/csrf.js +2 -2
  216. package/dist/server/onboarding-html.js +52 -120
  217. package/dist/server/ssr-handler.js +13 -4
  218. package/dist/settings/store.d.ts +9 -0
  219. package/dist/settings/store.js +19 -0
  220. package/dist/shared/cache-control.d.ts +22 -0
  221. package/dist/shared/cache-control.js +25 -0
  222. package/dist/shared/index.d.ts +1 -0
  223. package/dist/shared/index.js +1 -0
  224. package/dist/templates/chat/netlify.toml +2 -0
  225. package/docs/content/deployment.mdx +2 -0
  226. package/docs/content/multi-app-workspace.mdx +1 -1
  227. package/package.json +2 -2
  228. package/src/templates/chat/netlify.toml +2 -0
@@ -0,0 +1,99 @@
1
+ import { useActionMutation } from "@agent-native/core/client/hooks";
2
+ import type { QueryClient } from "@tanstack/react-query";
3
+ import type { Dispatch, RefObject, SetStateAction } from "react";
4
+ import { toast } from "sonner";
5
+
6
+ import type { DesignMigrationResult } from "@/components/design/editor/MakeRealDialog";
7
+ import type { DesignDataOperation } from "@/pages/design-editor/data-operations";
8
+ import { applyDesignDataOperations } from "@/pages/design-editor/data-operations";
9
+
10
+ export interface ConfirmMakeRealArgs {
11
+ designDataJsonRef: RefObject<Record<string, unknown>>;
12
+ id: string | undefined;
13
+ migrateMutation: ReturnType<
14
+ typeof useActionMutation<
15
+ undefined,
16
+ undefined,
17
+ "migrate-inline-design-to-app"
18
+ >
19
+ >;
20
+ queryClient: QueryClient;
21
+ setMigrationResult: Dispatch<SetStateAction<DesignMigrationResult | null>>;
22
+ updateDesignMutation: ReturnType<
23
+ typeof useActionMutation<undefined, undefined, "update-design">
24
+ >;
25
+ }
26
+
27
+ export async function runConfirmMakeReal({
28
+ designDataJsonRef,
29
+ id,
30
+ migrateMutation,
31
+ queryClient,
32
+ setMigrationResult,
33
+ updateDesignMutation,
34
+ }: ConfirmMakeRealArgs) {
35
+ if (!id) return;
36
+ try {
37
+ const result = await migrateMutation.mutateAsync({ designId: id } as any);
38
+ const r = result as any;
39
+ setMigrationResult({
40
+ branchName: r?.branchName,
41
+ url: r?.url,
42
+ versionId: r?.versionId,
43
+ seedFileCount: r?.seedFileCount,
44
+ status: r?.status,
45
+ projectId: r?.projectId,
46
+ cta: r?.cta,
47
+ });
48
+
49
+ // When the Builder agent accepted the job (status = "processing"),
50
+ // flip the design data to sourceType "fusion" so capability-gated
51
+ // panels (branches, deploy) light up on refresh.
52
+ if (r?.status === "processing" && r?.url) {
53
+ const dataOperations: DesignDataOperation[] = [
54
+ { op: "set", path: ["sourceType"], value: "fusion" },
55
+ { op: "set", path: ["fusionUrl"], value: r.url },
56
+ r.branchName === undefined
57
+ ? { op: "delete", path: ["fusionBranchName"] }
58
+ : {
59
+ op: "set",
60
+ path: ["fusionBranchName"],
61
+ value: r.branchName,
62
+ },
63
+ r.projectId === undefined
64
+ ? { op: "delete", path: ["fusionProjectId"] }
65
+ : {
66
+ op: "set",
67
+ path: ["fusionProjectId"],
68
+ value: r.projectId,
69
+ },
70
+ ];
71
+ const nextData = applyDesignDataOperations(
72
+ designDataJsonRef.current,
73
+ dataOperations,
74
+ );
75
+ designDataJsonRef.current = nextData;
76
+ queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
77
+ if (!old || typeof old !== "object") return old;
78
+ return { ...old, data: JSON.stringify(nextData) };
79
+ });
80
+ updateDesignMutation.mutate({ id, dataOperations } as any, {
81
+ onSuccess: () => {
82
+ queryClient.invalidateQueries({
83
+ queryKey: ["action", "get-design"],
84
+ });
85
+ },
86
+ onError: () => {
87
+ queryClient.invalidateQueries({
88
+ queryKey: ["action", "get-design"],
89
+ });
90
+ },
91
+ });
92
+ } else if (r?.status === "not-configured") {
93
+ // Builder not connected — leave dialog open to show the CTA.
94
+ }
95
+ } catch (err) {
96
+ const message = err instanceof Error ? err.message : "Migration failed";
97
+ toast.error(message);
98
+ }
99
+ }
@@ -0,0 +1,95 @@
1
+ import type { Dispatch, RefObject, SetStateAction } from "react";
2
+ import { toast } from "sonner";
3
+
4
+ import type {
5
+ LiveFigmaSvgSnapshot,
6
+ LiveFigmaSvgSource,
7
+ } from "@/lib/figma-svg-copy";
8
+ import {
9
+ FigmaSvgCopyError,
10
+ canCopyFigmaSvgToClipboard,
11
+ copyDesignAsFigmaSvg,
12
+ } from "@/lib/figma-svg-copy";
13
+ import type { DesignFile } from "@/pages/design-editor/types";
14
+
15
+ export interface CopyAsFigmaSvgArgs {
16
+ activeFile: DesignFile;
17
+ figmaSvgExportingRef: RefObject<boolean>;
18
+ id: string | undefined;
19
+ resolveLiveFigmaSvgSnapshot: (
20
+ targetFileId: string | undefined,
21
+ ) => LiveFigmaSvgSnapshot | null;
22
+ resolveLiveFigmaSvgSource: (
23
+ targetFileId: string | undefined,
24
+ ) => LiveFigmaSvgSource | null;
25
+ selectedElementLayerId: string | null;
26
+ selectedScreenIds: string[];
27
+ setFigmaSvgExporting: Dispatch<SetStateAction<boolean>>;
28
+ t: (key: string, options?: Record<string, unknown>) => string;
29
+ }
30
+
31
+ export async function runCopyAsFigmaSvg({
32
+ activeFile,
33
+ figmaSvgExportingRef,
34
+ id,
35
+ resolveLiveFigmaSvgSnapshot,
36
+ resolveLiveFigmaSvgSource,
37
+ selectedElementLayerId,
38
+ selectedScreenIds,
39
+ setFigmaSvgExporting,
40
+ t,
41
+ }: CopyAsFigmaSvgArgs) {
42
+ if (figmaSvgExportingRef.current) return;
43
+ if (!canCopyFigmaSvgToClipboard()) {
44
+ toast.error(t("designEditor.toasts.figmaSvgUnsupported"));
45
+ return;
46
+ }
47
+
48
+ figmaSvgExportingRef.current = true;
49
+ setFigmaSvgExporting(true);
50
+ try {
51
+ // The export action defaults to `filename: "index.html"` when no
52
+ // fileId is given -- that only happens to work for a design whose
53
+ // screen is literally named index.html. This context menu is shared
54
+ // between single-screen edit view and overview mode, so resolve the
55
+ // actual target screen the same way PNG export does (selectedScreenIds
56
+ // covers both: activeFile.id in single-screen view, the
57
+ // overview-selected screen id(s) in overview mode) instead of letting
58
+ // the action guess. Regression: "Copy as SVG" on any non-index.html
59
+ // screen failed with "Design file not found".
60
+ const targetFileId = activeFile?.id ?? selectedScreenIds[0] ?? undefined;
61
+ await copyDesignAsFigmaSvg(
62
+ {
63
+ designId: id,
64
+ fileId: targetFileId,
65
+ nodeId: selectedElementLayerId ?? undefined,
66
+ },
67
+ {
68
+ liveSource: resolveLiveFigmaSvgSource(targetFileId),
69
+ liveSnapshot: resolveLiveFigmaSvgSnapshot(targetFileId),
70
+ },
71
+ );
72
+ toast.success(t("designEditor.toasts.figmaSvgCopied"));
73
+ } catch (error) {
74
+ if (error instanceof FigmaSvgCopyError) {
75
+ const key =
76
+ error.code === "blocked"
77
+ ? "designEditor.toasts.figmaSvgBlocked"
78
+ : error.code === "unsupported"
79
+ ? "designEditor.toasts.figmaSvgUnsupported"
80
+ : error.code === "render-failed"
81
+ ? "designEditor.toasts.figmaSvgRenderError"
82
+ : "designEditor.toasts.figmaSvgWriteError";
83
+ toast.error(t(key));
84
+ } else {
85
+ toast.error(
86
+ error instanceof Error
87
+ ? error.message
88
+ : t("designEditor.toasts.figmaSvgExportError"),
89
+ );
90
+ }
91
+ } finally {
92
+ figmaSvgExportingRef.current = false;
93
+ setFigmaSvgExporting(false);
94
+ }
95
+ }
@@ -0,0 +1,156 @@
1
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
2
+ import type { Dispatch, RefObject, SetStateAction } from "react";
3
+ import { toast } from "sonner";
4
+
5
+ import {
6
+ getDesignClipboardTrustToken,
7
+ plainTextFromDesignHtml,
8
+ writeDesignClipboard,
9
+ } from "@/lib/design-clipboard";
10
+ import type {
11
+ DesignClipboardPayload,
12
+ DesignClipboardScreenEntry,
13
+ } from "@/lib/design-import";
14
+ import { serializeDesignClipboardPayload } from "@/lib/design-import";
15
+ import { resolveClipboardLayerSourceHtml } from "@/pages/design-editor/clipboard-layer-source";
16
+ import { preserveClipboardLayerName } from "@/pages/design-editor/clone-and-pen-edit";
17
+ import type {
18
+ CanvasLayerClipboardEntry,
19
+ LiveScreenSnapshot,
20
+ RuntimeLayerSnapshot,
21
+ SelectedCanvasLayerSnapshot,
22
+ } from "@/pages/design-editor/command-types";
23
+ import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
24
+ import { shouldUseRuntimeLayerProjection } from "@/pages/design-editor/pending-edits";
25
+ import type { DesignFile } from "@/pages/design-editor/types";
26
+
27
+ export interface CopySelectionArgs {
28
+ canvasFrameGeometryById: CanvasFrameGeometryById;
29
+ copiedLayerEntriesRef: RefObject<CanvasLayerClipboardEntry[]>;
30
+ copiedLayerHtmlRef: RefObject<string | null>;
31
+ copiedScreenEntriesRef: RefObject<DesignClipboardScreenEntry[] | undefined>;
32
+ designSourceType: "inline" | "localhost" | "fusion";
33
+ files: DesignFile[];
34
+ getScreenContent: (screenId: string) => string;
35
+ getSelectedLayerSnapshots: () => SelectedCanvasLayerSnapshot[];
36
+ lastWrittenClipboardMarkerRef: RefObject<string | null>;
37
+ lastWrittenClipboardPlainTextRef: RefObject<string | null>;
38
+ liveScreenSnapshotsById: Record<string, LiveScreenSnapshot>;
39
+ overviewScreens: OverviewScreen[];
40
+ overviewSelectedScreenIds: string[];
41
+ pasteCascadeRef: RefObject<number>;
42
+ runtimeLayerSnapshotsById: Record<string, RuntimeLayerSnapshot>;
43
+ setHasCanvasClipboard: Dispatch<SetStateAction<boolean>>;
44
+ t: (key: string, options?: Record<string, unknown>) => string;
45
+ viewModeRef: RefObject<"single" | "overview">;
46
+ }
47
+
48
+ export async function runCopySelection({
49
+ canvasFrameGeometryById,
50
+ copiedLayerEntriesRef,
51
+ copiedLayerHtmlRef,
52
+ copiedScreenEntriesRef,
53
+ designSourceType,
54
+ files,
55
+ getScreenContent,
56
+ getSelectedLayerSnapshots,
57
+ lastWrittenClipboardMarkerRef,
58
+ lastWrittenClipboardPlainTextRef,
59
+ liveScreenSnapshotsById,
60
+ overviewScreens,
61
+ overviewSelectedScreenIds,
62
+ pasteCascadeRef,
63
+ runtimeLayerSnapshotsById,
64
+ setHasCanvasClipboard,
65
+ t,
66
+ viewModeRef,
67
+ }: CopySelectionArgs) {
68
+ const entries = getSelectedLayerSnapshots().map((snapshot) => ({
69
+ html: preserveClipboardLayerName(snapshot.html, snapshot.node.layerName),
70
+ rootNodeId: snapshot.rootNodeId,
71
+ sourceFileId: snapshot.sourceFileId,
72
+ portableStyleSnapshot: snapshot.portableStyleSnapshot,
73
+ managedStyleSnapshot: snapshot.managedStyleSnapshot,
74
+ }));
75
+ // Whole-screen copy (U6): getSelectedLayerSnapshots explicitly excludes
76
+ // file/screen ids from layer candidates, so selecting one or more whole
77
+ // screens/frames in the overview and pressing Cmd+C previously produced
78
+ // zero entries and silently no-opped, leaving the clipboard unchanged.
79
+ // Fall back to screen-level snapshots (full file content + geometry) when
80
+ // there is no deeper layer selection to copy.
81
+ const screens: DesignClipboardPayload["screens"] =
82
+ entries.length === 0 && viewModeRef.current === "overview"
83
+ ? overviewSelectedScreenIds
84
+ .map((screenId): DesignClipboardScreenEntry | null => {
85
+ const file = files.find((candidate) => candidate.id === screenId);
86
+ const runtimeProjectionEligible =
87
+ file &&
88
+ shouldUseRuntimeLayerProjection({
89
+ screen: overviewScreens.find(
90
+ (screen) => screen.id === screenId,
91
+ ),
92
+ fallbackSourceType: designSourceType,
93
+ content: file.content ?? "",
94
+ });
95
+ const runtimeSnapshot = runtimeProjectionEligible
96
+ ? runtimeLayerSnapshotsById[screenId]
97
+ : undefined;
98
+ const content = resolveClipboardLayerSourceHtml({
99
+ runtimeProjectionEligible: Boolean(runtimeProjectionEligible),
100
+ runtimeSnapshot,
101
+ liveSnapshotHtml: liveScreenSnapshotsById[screenId]?.html,
102
+ storedContent: getScreenContent(screenId) ?? file?.content,
103
+ });
104
+ if (!file || typeof content !== "string") return null;
105
+ return {
106
+ filename: file.filename,
107
+ fileType: file.fileType,
108
+ content,
109
+ canvasFrame: canvasFrameGeometryById[screenId],
110
+ };
111
+ })
112
+ .filter((entry): entry is DesignClipboardScreenEntry =>
113
+ Boolean(entry),
114
+ )
115
+ : [];
116
+ if (entries.length === 0 && screens.length === 0) return false;
117
+ const copiedHtml =
118
+ entries.length > 0
119
+ ? entries.map((entry) => entry.html).join("\n")
120
+ : screens.map((screen) => screen.content).join("\n");
121
+ const plainText = plainTextFromDesignHtml(
122
+ entries.length > 0
123
+ ? entries.map((entry) => entry.html)
124
+ : screens.map((screen) => screen.content),
125
+ );
126
+ // The lossless layer payload stays in text/html while text/plain contains
127
+ // only readable content. This mirrors Figma's clipboard behavior: Design
128
+ // can round-trip structure across tabs without dumping source and marker
129
+ // data into ordinary text destinations.
130
+ const clipboardHtml = serializeDesignClipboardPayload(
131
+ copiedHtml,
132
+ {
133
+ version: 1,
134
+ entries,
135
+ screens,
136
+ },
137
+ getDesignClipboardTrustToken() ?? undefined,
138
+ );
139
+ copiedLayerEntriesRef.current = entries;
140
+ copiedScreenEntriesRef.current = screens;
141
+ copiedLayerHtmlRef.current = clipboardHtml;
142
+ lastWrittenClipboardMarkerRef.current = clipboardHtml;
143
+ lastWrittenClipboardPlainTextRef.current = plainText;
144
+ pasteCascadeRef.current = 0;
145
+ setHasCanvasClipboard(true);
146
+ try {
147
+ await writeDesignClipboard({ plainText, html: clipboardHtml });
148
+ } catch {
149
+ // The OS clipboard write failing is tolerated: the in-memory refs
150
+ // above are already populated, so in-app cut-then-paste still works.
151
+ // Only the true "nothing was captured at all" case (the early return
152
+ // above) should abort a cut (see U15).
153
+ toast.error(t("designEditor.toasts.clipboardBlocked"));
154
+ }
155
+ return true;
156
+ }
@@ -0,0 +1,291 @@
1
+ import { buildCodeLayerProjection } from "@shared/code-layer";
2
+ import { shouldUseLiveFileContent } from "@shared/html-content";
3
+ import type { QueryClient } from "@tanstack/react-query";
4
+ import type { Dispatch, RefObject, SetStateAction } from "react";
5
+ import { toast } from "sonner";
6
+ import * as Y from "yjs";
7
+
8
+ import type { CanvasPrimitiveInsert } from "@/components/design/multi-screen/types";
9
+ import type { RuntimeStructureInsertRequest } from "@/components/design/types";
10
+ import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
11
+ import {
12
+ appendCanvasPrimitiveToHtml,
13
+ blankScreenHtml,
14
+ extractCanvasPrimitiveHtml,
15
+ uniqueLayerId,
16
+ } from "@/pages/design-editor/canvas-primitive-insert";
17
+ import { parsePenPathFromSerializedD } from "@/pages/design-editor/canvas-primitives";
18
+ import { setPenNodesAttributeOnElement } from "@/pages/design-editor/clone-and-pen-edit";
19
+ import type { FileContentSaveRequest } from "@/pages/design-editor/editor-state";
20
+ import { isStandaloneHttpUrl } from "@/pages/design-editor/editor-state";
21
+ import type {
22
+ ContentHistoryEntry,
23
+ PendingTextCreationHistory,
24
+ } from "@/pages/design-editor/history";
25
+ import type { DesignFile } from "@/pages/design-editor/types";
26
+
27
+ export interface CreatePrimitiveArgs {
28
+ activeContent: string;
29
+ activeFile: DesignFile;
30
+ applyLocalContentUpdate: (
31
+ nextContent: string,
32
+ options?: {
33
+ refreshPreview?: boolean;
34
+ skipPreview?: boolean;
35
+ forcePreviewFullDocument?: boolean;
36
+ immediateSave?: boolean;
37
+ persist?: boolean;
38
+ recordHistory?: boolean;
39
+ historyBeforeContent?: string;
40
+ updatedAt?: string;
41
+ clipboardMutation?: ClipboardContentMutationPublication;
42
+ },
43
+ ) => void;
44
+ boardFileId: string | undefined;
45
+ canEditDesign: boolean;
46
+ collabContentFileIdRef: RefObject<string | null>;
47
+ collabContentRef: RefObject<string | null>;
48
+ createFileContentSaveRequest: (
49
+ fileId: string,
50
+ content: string,
51
+ syncCollab: boolean,
52
+ ) => FileContentSaveRequest;
53
+ files: DesignFile[];
54
+ id: string | undefined;
55
+ isSynced: boolean;
56
+ markPendingLocalFileContent: (
57
+ fileId: string,
58
+ content: string,
59
+ baseUpdatedAt?: string | null,
60
+ ) => void;
61
+ pendingLocalFileContentsRef: RefObject<
62
+ Map<
63
+ string,
64
+ { content: string; startedAt: number; baseUpdatedAt?: string | null }
65
+ >
66
+ >;
67
+ pendingTextCreationHistoryRef: RefObject<PendingTextCreationHistory | null>;
68
+ pendingTextEditNodeIdRef: RefObject<string | null>;
69
+ queryClient: QueryClient;
70
+ recordContentHistoryEntry: (entry: ContentHistoryEntry) => void;
71
+ runtimeStructureInsertRevisionRef: RefObject<number>;
72
+ saveFileContent: (pending: FileContentSaveRequest) => void;
73
+ setRuntimeStructureInsertRequest: Dispatch<
74
+ SetStateAction<
75
+ (RuntimeStructureInsertRequest & { screenId: string }) | null
76
+ >
77
+ >;
78
+ t: (key: string, options?: Record<string, unknown>) => string;
79
+ viewModeRef: RefObject<"single" | "overview">;
80
+ ydoc: Y.Doc | null;
81
+ }
82
+
83
+ export function runCreatePrimitive(
84
+ {
85
+ activeContent,
86
+ activeFile,
87
+ applyLocalContentUpdate,
88
+ boardFileId,
89
+ canEditDesign,
90
+ collabContentFileIdRef,
91
+ collabContentRef,
92
+ createFileContentSaveRequest,
93
+ files,
94
+ id,
95
+ isSynced,
96
+ markPendingLocalFileContent,
97
+ pendingLocalFileContentsRef,
98
+ pendingTextCreationHistoryRef,
99
+ pendingTextEditNodeIdRef,
100
+ queryClient,
101
+ recordContentHistoryEntry,
102
+ runtimeStructureInsertRevisionRef,
103
+ saveFileContent,
104
+ setRuntimeStructureInsertRequest,
105
+ t,
106
+ viewModeRef,
107
+ ydoc,
108
+ }: CreatePrimitiveArgs,
109
+ screenId: string,
110
+ primitive: CanvasPrimitiveInsert,
111
+ ) {
112
+ if (!canEditDesign) return false;
113
+ const targetFile = files.find((file) => file.id === screenId);
114
+ if (!targetFile) return false;
115
+ const pendingContent = pendingLocalFileContentsRef.current.get(
116
+ targetFile.id,
117
+ )?.content;
118
+ const storedContent = targetFile.content ?? "";
119
+ const baseContent =
120
+ pendingContent ??
121
+ (targetFile.id === activeFile?.id
122
+ ? (() => {
123
+ const liveContent =
124
+ ydoc && isSynced
125
+ ? ydoc.getText("content").toString()
126
+ : ((collabContentFileIdRef.current === activeFile.id
127
+ ? collabContentRef.current
128
+ : null) ?? activeContent);
129
+ return shouldUseLiveFileContent({
130
+ liveContent,
131
+ storedContent,
132
+ fileType: targetFile.fileType,
133
+ })
134
+ ? liveContent
135
+ : storedContent;
136
+ })()
137
+ : storedContent);
138
+ // A localhost screen's stored content is its route URL, not an editable
139
+ // document. Keep that URL intact and send one serialized primitive
140
+ // through the same live insert bridge used by board-to-screen drops.
141
+ // The bridge echo records the pending source handoff and owns the
142
+ // optimistic DOM/history lifecycle (selection, Layers, undo, and redo).
143
+ if (isStandaloneHttpUrl(baseContent)) {
144
+ const nodeId =
145
+ primitive.nodeId ?? uniqueLayerId(primitive.kind || "primitive");
146
+ const livePrimitive = { ...primitive, nodeId };
147
+ const temporaryDocument = appendCanvasPrimitiveToHtml(
148
+ blankScreenHtml("Live insert"),
149
+ livePrimitive,
150
+ );
151
+ if (!temporaryDocument) {
152
+ toast.error(t("designEditor.toasts.primitiveInsertFailed"));
153
+ return false;
154
+ }
155
+ const enrichedDocument =
156
+ primitive.kind === "path" && primitive.pathData
157
+ ? (() => {
158
+ const reconstructed = parsePenPathFromSerializedD(
159
+ primitive.pathData!,
160
+ );
161
+ return reconstructed
162
+ ? setPenNodesAttributeOnElement(
163
+ temporaryDocument,
164
+ nodeId,
165
+ reconstructed,
166
+ )
167
+ : temporaryDocument;
168
+ })()
169
+ : temporaryDocument;
170
+ const insertedHtml = extractCanvasPrimitiveHtml(enrichedDocument, nodeId);
171
+ if (!insertedHtml) {
172
+ toast.error(t("designEditor.toasts.primitiveInsertFailed"));
173
+ return false;
174
+ }
175
+ pendingTextEditNodeIdRef.current =
176
+ primitive.kind === "text" ? nodeId : null;
177
+ runtimeStructureInsertRevisionRef.current += 1;
178
+ setRuntimeStructureInsertRequest({
179
+ requestId: runtimeStructureInsertRevisionRef.current,
180
+ screenId: targetFile.id,
181
+ html: insertedHtml,
182
+ anchor: { selector: "body" },
183
+ placement: "inside",
184
+ });
185
+ return nodeId;
186
+ }
187
+ const insertedContent = appendCanvasPrimitiveToHtml(baseContent, primitive, {
188
+ preserveNegativePosition: targetFile.id === boardFileId,
189
+ isBoardTarget: targetFile.id === boardFileId,
190
+ });
191
+ if (!insertedContent) {
192
+ toast.error(t("designEditor.toasts.primitiveInsertFailed"));
193
+ return false;
194
+ }
195
+ // Vector-edit foundations: stash the structured pen path (nodes +
196
+ // handles) alongside the flattened `d` so a later double-click/Enter
197
+ // can re-hydrate it into an editable path instead of only having the
198
+ // already-flattened curve. `primitive.pathData` is the only carrier of
199
+ // pen geometry that crosses the MultiScreenCanvas -> DesignEditor
200
+ // boundary for an OVERVIEW-drawn pen path (see
201
+ // parsePenPathFromSerializedD's doc comment for why this reconstructs
202
+ // rather than receives the structured path directly).
203
+ const nextContent =
204
+ primitive.kind === "path" && primitive.pathData && primitive.nodeId
205
+ ? (() => {
206
+ const reconstructed = parsePenPathFromSerializedD(
207
+ primitive.pathData!,
208
+ );
209
+ return reconstructed
210
+ ? setPenNodesAttributeOnElement(
211
+ insertedContent,
212
+ primitive.nodeId!,
213
+ reconstructed,
214
+ )
215
+ : insertedContent;
216
+ })()
217
+ : insertedContent;
218
+ const projectedNodeId = primitive.nodeId
219
+ ? buildCodeLayerProjection(nextContent).nodes.find(
220
+ (node) =>
221
+ node.dataAttributes["data-agent-native-node-id"] === primitive.nodeId,
222
+ )?.id
223
+ : null;
224
+
225
+ pendingTextCreationHistoryRef.current =
226
+ primitive.kind === "text" &&
227
+ primitive.nodeId &&
228
+ viewModeRef.current === "overview"
229
+ ? {
230
+ fileId: targetFile.id,
231
+ nodeId: primitive.nodeId,
232
+ before: baseContent,
233
+ created: nextContent,
234
+ }
235
+ : null;
236
+
237
+ if (targetFile.id === activeFile?.id) {
238
+ applyLocalContentUpdate(nextContent, {
239
+ forcePreviewFullDocument: true,
240
+ historyBeforeContent: baseContent,
241
+ immediateSave: true,
242
+ });
243
+ } else {
244
+ recordContentHistoryEntry({
245
+ fileId: targetFile.id,
246
+ before: baseContent,
247
+ after: nextContent,
248
+ });
249
+ // Stamp the server-clock base the same way applyFileContentUpdate
250
+ // does. Without it the reconcile effect reads the optimistic cache
251
+ // write below as a server acknowledgement and retires the pending
252
+ // entry immediately, leaving that cache the only carrier of the
253
+ // insert — so any get-design response already in flight (the board
254
+ // file's own lazy migration invalidates on success, so one usually
255
+ // is) overwrites it with pre-insert content and the primitive
256
+ // disappears from the canvas until a reload.
257
+ markPendingLocalFileContent(
258
+ targetFile.id,
259
+ nextContent,
260
+ targetFile.updatedAt,
261
+ );
262
+ queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
263
+ if (!old || typeof old !== "object" || !Array.isArray(old.files)) {
264
+ return old;
265
+ }
266
+ return {
267
+ ...old,
268
+ files: old.files.map((file: DesignFile) =>
269
+ file.id === targetFile.id ? { ...file, content: nextContent } : file,
270
+ ),
271
+ };
272
+ });
273
+ saveFileContent(
274
+ createFileContentSaveRequest(targetFile.id, nextContent, true),
275
+ );
276
+ }
277
+
278
+ const result = projectedNodeId ?? primitive.nodeId ?? true;
279
+
280
+ // Record the nodeId when a TEXT primitive is created so the next
281
+ // handlePrimitiveCreated (or handleBoardDrawPrimitive) can immediately
282
+ // enter text-edit mode — fixing the "click to add text should let me
283
+ // type immediately" bug. The ref is read once and cleared.
284
+ if (primitive.kind === "text") {
285
+ pendingTextEditNodeIdRef.current = primitive.nodeId ?? null;
286
+ } else {
287
+ pendingTextEditNodeIdRef.current = null;
288
+ }
289
+
290
+ return result;
291
+ }