@agent-native/core 0.163.4 → 0.164.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 (200) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
  3. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
  4. package/corpus/templates/analytics/netlify.toml +2 -0
  5. package/corpus/templates/assets/netlify.toml +2 -0
  6. package/corpus/templates/brain/netlify.toml +2 -0
  7. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
  8. package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
  9. package/corpus/templates/calendar/netlify.toml +2 -0
  10. package/corpus/templates/chat/netlify.toml +2 -0
  11. package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
  12. package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
  13. package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
  14. package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
  15. package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
  16. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
  17. package/corpus/templates/clips/app/routes/_index.tsx +10 -2
  18. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
  19. package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
  20. package/corpus/templates/clips/netlify.toml +2 -0
  21. package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
  22. package/corpus/templates/content/netlify.toml +2 -0
  23. package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
  24. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
  25. package/corpus/templates/design/AGENTS.md +3 -1
  26. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
  27. package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
  28. package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
  29. package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
  30. package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
  31. package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
  32. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
  33. package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
  34. package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
  35. package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
  36. package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
  37. package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
  38. package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
  39. package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
  40. package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
  41. package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
  42. package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
  43. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
  44. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
  45. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
  46. package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
  47. package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
  48. package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
  49. package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
  50. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
  51. package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
  52. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
  53. package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
  54. package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
  55. package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
  56. package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
  57. package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
  58. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
  59. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
  60. package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
  61. package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
  62. package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
  63. package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
  64. package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
  65. package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
  66. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
  67. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
  68. package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
  69. package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
  70. package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
  71. package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
  72. package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
  73. package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
  74. package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
  75. package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
  76. package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
  77. package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
  78. package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
  79. package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
  80. package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
  81. package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
  82. package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
  83. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
  84. package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
  85. package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
  86. package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
  87. package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
  88. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
  89. package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
  90. package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
  91. package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
  92. package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
  93. package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
  94. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
  95. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
  96. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
  97. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
  98. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
  99. package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
  100. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
  101. package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
  102. package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
  103. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
  104. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
  105. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
  106. package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
  107. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
  108. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
  109. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
  110. package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
  111. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
  112. package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
  113. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
  114. package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
  115. package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
  116. package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
  117. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
  118. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
  119. package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
  120. package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
  121. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
  122. package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
  123. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
  124. package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
  125. package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
  126. package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
  127. package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
  128. package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
  129. package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
  130. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
  131. package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
  132. package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
  133. package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
  134. package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
  135. package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
  136. package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
  137. package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
  138. package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
  139. package/corpus/templates/design/netlify.toml +2 -0
  140. package/corpus/templates/design/shared/board-file.ts +2 -1
  141. package/corpus/templates/dispatch/netlify.toml +2 -0
  142. package/corpus/templates/forms/netlify.toml +2 -0
  143. package/corpus/templates/mail/netlify.toml +2 -0
  144. package/corpus/templates/plan/netlify.toml +2 -0
  145. package/corpus/templates/slides/netlify.toml +2 -0
  146. package/dist/agent/engine/builder-engine.d.ts +6 -4
  147. package/dist/agent/engine/builder-engine.js +91 -26
  148. package/dist/agent/engine/registry.js +16 -0
  149. package/dist/cli/code-agent-agent-tools.d.ts +2 -0
  150. package/dist/cli/code-agent-agent-tools.js +188 -0
  151. package/dist/cli/code-agent-collaboration.d.ts +34 -0
  152. package/dist/cli/code-agent-collaboration.js +104 -0
  153. package/dist/cli/code-agent-executor.js +6 -0
  154. package/dist/cli/code-agent-schedules.d.ts +54 -0
  155. package/dist/cli/code-agent-schedules.js +198 -0
  156. package/dist/client/AgentPanel.js +1 -1
  157. package/dist/client/ConnectBuilderCard.js +5 -2
  158. package/dist/client/EnvironmentBadge.js +6 -5
  159. package/dist/client/chat/run-recovery.js +3 -2
  160. package/dist/client/chat/tool-call-display.js +2 -3
  161. package/dist/client/components/CodeRequiredDialog.js +4 -2
  162. package/dist/client/i18n.js +5 -4
  163. package/dist/client/settings/SettingsPanel.js +1 -1
  164. package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
  165. package/dist/client/settings/useBuilderStatus.d.ts +6 -1
  166. package/dist/client/settings/useBuilderStatus.js +22 -33
  167. package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
  168. package/dist/code-agents/index.d.ts +2 -0
  169. package/dist/code-agents/index.js +2 -0
  170. package/dist/collab/routes.d.ts +1 -1
  171. package/dist/localization/shared.d.ts +2 -0
  172. package/dist/localization/shared.js +5 -0
  173. package/dist/mcp-client/oauth-client.d.ts +1 -0
  174. package/dist/mcp-client/oauth-client.js +50 -1
  175. package/dist/observability/routes.d.ts +1 -1
  176. package/dist/progress/routes.d.ts +1 -1
  177. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  178. package/dist/resources/handlers.d.ts +1 -1
  179. package/dist/server/auth-marketing-locales.d.ts +7 -0
  180. package/dist/server/auth-marketing-locales.js +1322 -0
  181. package/dist/server/auth-marketing.d.ts +1 -0
  182. package/dist/server/auth-marketing.js +35 -6
  183. package/dist/server/auth.js +4 -17
  184. package/dist/server/builder-browser.d.ts +22 -0
  185. package/dist/server/builder-browser.js +77 -1
  186. package/dist/server/builder-oauth.d.ts +50 -0
  187. package/dist/server/builder-oauth.js +263 -0
  188. package/dist/server/core-routes-plugin.d.ts +11 -0
  189. package/dist/server/core-routes-plugin.js +291 -415
  190. package/dist/server/credential-provider.d.ts +1 -1
  191. package/dist/server/credential-provider.js +1 -1
  192. package/dist/server/csrf.js +2 -2
  193. package/dist/server/onboarding-html.js +52 -120
  194. package/dist/settings/store.d.ts +9 -0
  195. package/dist/settings/store.js +19 -0
  196. package/dist/templates/chat/netlify.toml +2 -0
  197. package/docs/content/deployment.mdx +2 -0
  198. package/docs/content/multi-app-workspace.mdx +1 -1
  199. package/package.json +2 -2
  200. package/src/templates/chat/netlify.toml +2 -0
@@ -0,0 +1,209 @@
1
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
2
+ import type { RefObject } from "react";
3
+ import { toast } from "sonner";
4
+
5
+ import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
6
+ import { uniqueLayerId } from "@/pages/design-editor/canvas-primitive-insert";
7
+ import { cloneHtmlLayerAtPosition } from "@/pages/design-editor/clone-and-pen-edit";
8
+ import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
9
+ import { escapeHtmlAttributeValue } from "@/pages/design-editor/dom-utils";
10
+ import {
11
+ findScreenFrameAtCanvasPoint,
12
+ getAllScreenFrameEntries,
13
+ } from "@/pages/design-editor/overview-camera";
14
+ import type { DesignFile } from "@/pages/design-editor/types";
15
+
16
+ export interface PastedImageFilesTarget {
17
+ fileId: string;
18
+ point: { x: number; y: number };
19
+ }
20
+
21
+ export interface PastedImageFilesArgs {
22
+ activeFile: DesignFile;
23
+ applyFileContentUpdate: (
24
+ fileId: string,
25
+ nextContent: string,
26
+ options?: {
27
+ refreshPreview?: boolean;
28
+ skipPreview?: boolean;
29
+ forcePreviewFullDocument?: boolean;
30
+ persist?: boolean;
31
+ recordHistory?: boolean;
32
+ updatedAt?: string;
33
+ clipboardMutation?: ClipboardContentMutationPublication;
34
+ },
35
+ ) => void;
36
+ applyLocalContentUpdate: (
37
+ nextContent: string,
38
+ options?: {
39
+ refreshPreview?: boolean;
40
+ skipPreview?: boolean;
41
+ forcePreviewFullDocument?: boolean;
42
+ immediateSave?: boolean;
43
+ persist?: boolean;
44
+ recordHistory?: boolean;
45
+ historyBeforeContent?: string;
46
+ updatedAt?: string;
47
+ clipboardMutation?: ClipboardContentMutationPublication;
48
+ },
49
+ ) => void;
50
+ boardFileId: string | undefined;
51
+ canEditDesign: boolean;
52
+ canvasContainerRef: RefObject<HTMLDivElement | null>;
53
+ canvasFrameGeometryById: CanvasFrameGeometryById;
54
+ getFreshActiveContent: () => string;
55
+ getScreenContent: (screenId: string) => string;
56
+ overviewScreens: OverviewScreen[];
57
+ overviewSelectedScreenIds: string[];
58
+ pasteCascadeRef: RefObject<number>;
59
+ selectInsertedLayers: (
60
+ screenId: string,
61
+ content: string,
62
+ rootNodeIds: string[],
63
+ ) => void;
64
+ t: (key: string, options?: Record<string, unknown>) => string;
65
+ uploadImageFileForHtml: (file: File) => Promise<string>;
66
+ viewModeRef: RefObject<"single" | "overview">;
67
+ zoom: number;
68
+ }
69
+
70
+ export function runPastedImageFiles(
71
+ {
72
+ activeFile,
73
+ applyFileContentUpdate,
74
+ applyLocalContentUpdate,
75
+ boardFileId,
76
+ canEditDesign,
77
+ canvasContainerRef,
78
+ canvasFrameGeometryById,
79
+ getFreshActiveContent,
80
+ getScreenContent,
81
+ overviewScreens,
82
+ overviewSelectedScreenIds,
83
+ pasteCascadeRef,
84
+ selectInsertedLayers,
85
+ t,
86
+ uploadImageFileForHtml,
87
+ viewModeRef,
88
+ zoom,
89
+ }: PastedImageFilesArgs,
90
+ files: File[],
91
+ target?: PastedImageFilesTarget,
92
+ ) {
93
+ if (files.length === 0 || !canEditDesign) return false;
94
+
95
+ const insertFilesAtPoint = (
96
+ targetFileId: string,
97
+ localPoint: { x: number; y: number } | (() => { x: number; y: number }),
98
+ ) => {
99
+ void (async () => {
100
+ for (const file of files) {
101
+ const imageUrl = await uploadImageFileForHtml(file);
102
+ if (!imageUrl) continue;
103
+ const baseContent =
104
+ targetFileId === activeFile?.id
105
+ ? getFreshActiveContent()
106
+ : (getScreenContent(targetFileId) ?? "");
107
+ const resolvedPoint =
108
+ typeof localPoint === "function" ? localPoint() : localPoint;
109
+ const cascadeOffset = pasteCascadeRef.current * 16;
110
+ pasteCascadeRef.current += 1;
111
+ const nodeId = uniqueLayerId("pasted-image");
112
+ const html = `<img src="${imageUrl}" alt="${escapeHtmlAttributeValue(file.name || "Pasted image")}" data-agent-native-node-id="${nodeId}" data-agent-native-layer-name="Pasted image" style="position:absolute;width:320px;height:auto;" />`;
113
+ const nextContent = cloneHtmlLayerAtPosition(baseContent, html, {
114
+ x: resolvedPoint.x + cascadeOffset,
115
+ y: resolvedPoint.y + cascadeOffset,
116
+ });
117
+ if (!nextContent) {
118
+ toast.error(t("designEditor.toasts.duplicateElementFailed"));
119
+ continue;
120
+ }
121
+ if (targetFileId === activeFile?.id) {
122
+ applyLocalContentUpdate(nextContent, {
123
+ forcePreviewFullDocument: true,
124
+ });
125
+ } else {
126
+ applyFileContentUpdate(targetFileId, nextContent, {
127
+ forcePreviewFullDocument: true,
128
+ });
129
+ }
130
+ selectInsertedLayers(targetFileId, nextContent, [nodeId]);
131
+ }
132
+ })();
133
+ };
134
+
135
+ if (target) {
136
+ insertFilesAtPoint(target.fileId, target.point);
137
+ return true;
138
+ }
139
+
140
+ if (viewModeRef.current !== "overview") {
141
+ const targetFileId = activeFile?.id;
142
+ if (!targetFileId) return false;
143
+ const getCenter = () => {
144
+ const iframe = canvasContainerRef.current?.querySelector<HTMLElement>(
145
+ "[data-design-preview-iframe]",
146
+ );
147
+ if (iframe) {
148
+ const iframeRect = iframe.getBoundingClientRect();
149
+ const factor = zoom / 100;
150
+ return {
151
+ x: Math.max(0, iframeRect.width / 2 / factor),
152
+ y: Math.max(0, iframeRect.height / 2 / factor),
153
+ };
154
+ }
155
+ const rect = canvasContainerRef.current?.getBoundingClientRect();
156
+ return rect
157
+ ? {
158
+ x: Math.max(0, rect.width / 2),
159
+ y: Math.max(0, rect.height / 2),
160
+ }
161
+ : { x: 120, y: 120 };
162
+ };
163
+ insertFilesAtPoint(targetFileId, getCenter);
164
+ return true;
165
+ }
166
+
167
+ // Overview mode: resolve a canvas-space anchor point, then hit-test it
168
+ // against real screen frames.
169
+ if (!boardFileId) return false;
170
+ const frames = getAllScreenFrameEntries({
171
+ overviewScreens,
172
+ canvasFrameGeometryById,
173
+ });
174
+ const anchorCanvasPoint = (() => {
175
+ if (overviewSelectedScreenIds.length === 1) {
176
+ const screenId = overviewSelectedScreenIds[0]!;
177
+ const frame = frames.find((entry) => entry.id === screenId);
178
+ if (frame) {
179
+ return {
180
+ x: frame.geometry.x + frame.geometry.width / 2,
181
+ y: frame.geometry.y + frame.geometry.height / 2,
182
+ };
183
+ }
184
+ }
185
+ // Best-effort fallback (matches the prior single-image behavior):
186
+ // container-relative pixels as a stand-in canvas point. Overview pan/
187
+ // zoom camera state lives inside MultiScreenCanvas, not here, so this
188
+ // can't account for the live camera transform — see FINAL REPORT.
189
+ const rect = canvasContainerRef.current?.getBoundingClientRect();
190
+ return rect
191
+ ? { x: Math.max(0, rect.width / 2), y: Math.max(0, rect.height / 2) }
192
+ : { x: 120, y: 120 };
193
+ })();
194
+ const hitFrame = findScreenFrameAtCanvasPoint(
195
+ anchorCanvasPoint,
196
+ frames,
197
+ boardFileId,
198
+ );
199
+ const targetFileId = hitFrame?.id ?? boardFileId;
200
+ const localAnchor = hitFrame
201
+ ? {
202
+ x: anchorCanvasPoint.x - hitFrame.geometry.x,
203
+ y: anchorCanvasPoint.y - hitFrame.geometry.y,
204
+ }
205
+ : anchorCanvasPoint;
206
+
207
+ insertFilesAtPoint(targetFileId, localAnchor);
208
+ return true;
209
+ }
@@ -0,0 +1,113 @@
1
+ import { tryCallActionKeepalive } from "@agent-native/core/client/hooks";
2
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
3
+ import type { QueryClient } from "@tanstack/react-query";
4
+ import type { RefObject } from "react";
5
+
6
+ import type { DesignSaveOutboxEntry } from "@/lib/design-save-outbox";
7
+ import type {
8
+ DesignDataOperation,
9
+ PendingDesignDataOperations,
10
+ } from "@/pages/design-editor/data-operations";
11
+ import {
12
+ applyDesignDataOperations,
13
+ buildFrameGeometryDataOperations,
14
+ compactDesignDataOperations,
15
+ pendingDesignDataOperations,
16
+ } from "@/pages/design-editor/data-operations";
17
+ import { sanitizeCanvasFrameGeometryForPersist } from "@/pages/design-editor/geometry-persistence";
18
+
19
+ export interface PersistFrameGeometrySaveArgs {
20
+ acknowledgeOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<void>;
21
+ boardFileId: string | undefined;
22
+ canEditDesignRef: RefObject<boolean>;
23
+ createFrameGeometryOutboxEntry: (
24
+ dataOperations: readonly DesignDataOperation[],
25
+ revision: number,
26
+ ) => DesignSaveOutboxEntry | null;
27
+ designDataJsonRef: RefObject<Record<string, unknown>>;
28
+ enqueueFrameGeometryDataSave: (
29
+ dataOperations: DesignDataOperation[],
30
+ ) => boolean;
31
+ frameGeometryOperationRevisionRef: RefObject<number>;
32
+ id: string | undefined;
33
+ journalOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<boolean>;
34
+ pendingFrameGeometryOperationsForUnloadRef: RefObject<PendingDesignDataOperations>;
35
+ queryClient: QueryClient;
36
+ warnChangesWillRetry: () => void;
37
+ }
38
+
39
+ export function runPersistFrameGeometrySave(
40
+ {
41
+ acknowledgeOutboxEntry,
42
+ boardFileId,
43
+ canEditDesignRef,
44
+ createFrameGeometryOutboxEntry,
45
+ designDataJsonRef,
46
+ enqueueFrameGeometryDataSave,
47
+ frameGeometryOperationRevisionRef,
48
+ id,
49
+ journalOutboxEntry,
50
+ pendingFrameGeometryOperationsForUnloadRef,
51
+ queryClient,
52
+ warnChangesWillRetry,
53
+ }: PersistFrameGeometrySaveArgs,
54
+ pending: {
55
+ geometryById: CanvasFrameGeometryById;
56
+ previousGeometry: CanvasFrameGeometryById;
57
+ },
58
+ keepalive = false,
59
+ ): boolean {
60
+ if (!id || !canEditDesignRef.current) return false;
61
+ // Geometry-persist guard: refuse absurd per-frame geometry (see
62
+ // sanitizeCanvasFrameGeometryForPersist) so a corrupted transient
63
+ // zoom basis can never shred the persisted layout. The board frame
64
+ // is exempt — its surface is a legitimate 131k square.
65
+ const { geometryById: safeGeometryById } =
66
+ sanitizeCanvasFrameGeometryForPersist(
67
+ pending.geometryById,
68
+ pending.previousGeometry,
69
+ boardFileId ? [boardFileId] : [],
70
+ );
71
+ const dataOperations = buildFrameGeometryDataOperations({
72
+ previousGeometry: pending.previousGeometry,
73
+ nextGeometry: safeGeometryById,
74
+ designData: designDataJsonRef.current,
75
+ });
76
+ if (keepalive) {
77
+ const combinedOperations = compactDesignDataOperations([
78
+ ...pendingDesignDataOperations(
79
+ pendingFrameGeometryOperationsForUnloadRef.current,
80
+ ),
81
+ ...dataOperations,
82
+ ]);
83
+ if (combinedOperations.length === 0) return true;
84
+ const operationRevision = frameGeometryOperationRevisionRef.current + 1;
85
+ frameGeometryOperationRevisionRef.current = operationRevision;
86
+ const entry = createFrameGeometryOutboxEntry(
87
+ combinedOperations,
88
+ operationRevision,
89
+ );
90
+ if (!entry) return false;
91
+ void journalOutboxEntry(entry);
92
+ const attempt = tryCallActionKeepalive(
93
+ "update-design",
94
+ entry.payload as any,
95
+ );
96
+ if (!attempt.accepted) return false;
97
+ void attempt.completion
98
+ .then(() => acknowledgeOutboxEntry(entry))
99
+ .catch(warnChangesWillRetry);
100
+ return true;
101
+ }
102
+ if (dataOperations.length === 0) return true;
103
+ const nextData = applyDesignDataOperations(
104
+ designDataJsonRef.current,
105
+ dataOperations,
106
+ );
107
+ designDataJsonRef.current = nextData;
108
+ queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
109
+ if (!old || typeof old !== "object") return old;
110
+ return { ...old, data: JSON.stringify(nextData) };
111
+ });
112
+ return enqueueFrameGeometryDataSave(dataOperations);
113
+ }
@@ -0,0 +1,160 @@
1
+ import { useActionMutation } from "@agent-native/core/client/hooks";
2
+ import { tweakSelectionsHash } from "@shared/resolve-tweaks";
3
+ import type { QueryClient } from "@tanstack/react-query";
4
+ import type { Dispatch, RefObject, SetStateAction } from "react";
5
+ import { toast } from "sonner";
6
+
7
+ import type { DesignSaveOutboxEntry } from "@/lib/design-save-outbox";
8
+ import type { PendingTweakSave } from "@/pages/design-editor/tweak-save";
9
+ import {
10
+ classifyTweakSaveFailure,
11
+ clearCompletedTweakSave,
12
+ rebaseTweakSaveForSend,
13
+ retainLatestFailedTweakSave,
14
+ } from "@/pages/design-editor/tweak-save";
15
+
16
+ export interface PersistTweakSaveArgs {
17
+ acknowledgeOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<void>;
18
+ applyTweaksAsync: ReturnType<
19
+ typeof useActionMutation<undefined, undefined, "apply-tweaks">
20
+ >["mutateAsync"];
21
+ canEditDesignRef: RefObject<boolean>;
22
+ confirmedTweakSelectionsHashRef: RefObject<string>;
23
+ createTweakSaveOutboxEntry: (
24
+ pending: PendingTweakSave,
25
+ ) => DesignSaveOutboxEntry | null;
26
+ id: string | undefined;
27
+ journalTweakOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<boolean>;
28
+ pendingTweakSaveRef: RefObject<PendingTweakSave | null>;
29
+ queryClient: QueryClient;
30
+ setTweakSaveActive: Dispatch<SetStateAction<boolean>>;
31
+ t: (key: string, options?: Record<string, unknown>) => string;
32
+ tweakSaveChainRef: RefObject<Promise<void>>;
33
+ tweakSaveInFlightRef: RefObject<boolean>;
34
+ tweakSaveRevisionRef: RefObject<number>;
35
+ warnChangesWillRetry: () => void;
36
+ }
37
+
38
+ export function runPersistTweakSave(
39
+ {
40
+ acknowledgeOutboxEntry,
41
+ applyTweaksAsync,
42
+ canEditDesignRef,
43
+ confirmedTweakSelectionsHashRef,
44
+ createTweakSaveOutboxEntry,
45
+ id,
46
+ journalTweakOutboxEntry,
47
+ pendingTweakSaveRef,
48
+ queryClient,
49
+ setTweakSaveActive,
50
+ t,
51
+ tweakSaveChainRef,
52
+ tweakSaveInFlightRef,
53
+ tweakSaveRevisionRef,
54
+ warnChangesWillRetry,
55
+ }: PersistTweakSaveArgs,
56
+ pending: PendingTweakSave,
57
+ ) {
58
+ if (!id || !canEditDesignRef.current) return;
59
+ const previous = tweakSaveChainRef.current;
60
+ const current = previous
61
+ .catch(() => {})
62
+ .then(async () => {
63
+ tweakSaveInFlightRef.current = true;
64
+ const sendPending = rebaseTweakSaveForSend(
65
+ pending,
66
+ confirmedTweakSelectionsHashRef.current,
67
+ );
68
+ const entry = createTweakSaveOutboxEntry(sendPending);
69
+ if (!entry) {
70
+ tweakSaveInFlightRef.current = false;
71
+ return;
72
+ }
73
+ const journaled = await journalTweakOutboxEntry(entry);
74
+ try {
75
+ const result = (await applyTweaksAsync(entry.payload as any)) as {
76
+ appliedTweaks?: Record<string, unknown>;
77
+ selectionsHash?: string;
78
+ };
79
+ const confirmedHash =
80
+ typeof result?.selectionsHash === "string"
81
+ ? result.selectionsHash
82
+ : tweakSelectionsHash(
83
+ result?.appliedTweaks ?? sendPending.selections,
84
+ );
85
+ confirmedTweakSelectionsHashRef.current = confirmedHash;
86
+ if (result?.appliedTweaks) {
87
+ queryClient.setQueryData(
88
+ ["action", "get-design", { id }],
89
+ (old: any) => {
90
+ if (!old || typeof old !== "object") return old;
91
+ let data: Record<string, unknown>;
92
+ try {
93
+ const parsed = JSON.parse(old.data ?? "{}");
94
+ data =
95
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
96
+ ? parsed
97
+ : {};
98
+ } catch {
99
+ return old;
100
+ }
101
+ return {
102
+ ...old,
103
+ data: JSON.stringify({
104
+ ...data,
105
+ tweakSelections: result.appliedTweaks,
106
+ }),
107
+ };
108
+ },
109
+ );
110
+ }
111
+ if (journaled) await acknowledgeOutboxEntry(entry);
112
+
113
+ const queuedAfterSuccess = clearCompletedTweakSave(
114
+ pendingTweakSaveRef.current,
115
+ pending.revision,
116
+ );
117
+ if (
118
+ queuedAfterSuccess &&
119
+ queuedAfterSuccess.revision > pending.revision
120
+ ) {
121
+ const rebasedQueued = rebaseTweakSaveForSend(
122
+ queuedAfterSuccess,
123
+ confirmedHash,
124
+ );
125
+ pendingTweakSaveRef.current = rebasedQueued;
126
+ const rebasedEntry = createTweakSaveOutboxEntry(rebasedQueued);
127
+ if (rebasedEntry) void journalTweakOutboxEntry(rebasedEntry);
128
+ } else {
129
+ pendingTweakSaveRef.current = queuedAfterSuccess;
130
+ }
131
+ if (tweakSaveRevisionRef.current === pending.revision) {
132
+ setTweakSaveActive(false);
133
+ }
134
+ } catch (error) {
135
+ pendingTweakSaveRef.current = retainLatestFailedTweakSave(
136
+ pendingTweakSaveRef.current,
137
+ sendPending,
138
+ );
139
+ if (tweakSaveRevisionRef.current === pending.revision) {
140
+ setTweakSaveActive(true);
141
+ }
142
+ const failureKind = classifyTweakSaveFailure(error, journaled);
143
+ if (failureKind === "conflict") {
144
+ toast.error(t("designEditor.toasts.tweakConflict"));
145
+ } else if (failureKind === "durable-retry") {
146
+ warnChangesWillRetry();
147
+ } else {
148
+ toast.error(t("designEditor.toasts.tweakSaveNotDurable"));
149
+ }
150
+ } finally {
151
+ tweakSaveInFlightRef.current = false;
152
+ }
153
+ });
154
+ tweakSaveChainRef.current = current;
155
+ void current.finally(() => {
156
+ if (tweakSaveChainRef.current === current) {
157
+ tweakSaveChainRef.current = Promise.resolve();
158
+ }
159
+ });
160
+ }
@@ -0,0 +1,168 @@
1
+ import type { Dispatch, RefObject, SetStateAction } from "react";
2
+ import { flushSync } from "react-dom";
3
+
4
+ import type { ElementInfo } from "@/components/design/types";
5
+ import {
6
+ isTextEditSessionOutcome,
7
+ scheduleBeginTextEditForScreen,
8
+ } from "@/pages/design-editor/text-edit-utils";
9
+ import type { DesignTool, EditorMode } from "@/pages/design-editor/types";
10
+
11
+ export interface PrimitiveCreatedArgs {
12
+ boardFileId: string | undefined;
13
+ clearPendingOverviewLayerSelectionTimer: () => void;
14
+ pendingEmptyTextEditRef: RefObject<{
15
+ screenId: string | null;
16
+ nodeId: string;
17
+ cancel: () => void;
18
+ settled: boolean;
19
+ } | null>;
20
+ pendingOverviewLayerSelectionRef: RefObject<string | null>;
21
+ pendingOverviewScreenSelectionRef: RefObject<string | null>;
22
+ pendingTextEditNodeIdRef: RefObject<string | null>;
23
+ removeEmptyTextNodeWithRetry: (
24
+ screenId: string | null,
25
+ nodeId: string,
26
+ ) => void;
27
+ setActiveFileId: Dispatch<SetStateAction<string | null>>;
28
+ setActiveTool: Dispatch<SetStateAction<DesignTool>>;
29
+ setCreatedOverviewLayerSelection: Dispatch<
30
+ SetStateAction<{ screenId: string; layerId: string } | null>
31
+ >;
32
+ setHoveredElement: Dispatch<SetStateAction<ElementInfo | null>>;
33
+ setMode: Dispatch<SetStateAction<EditorMode>>;
34
+ setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
35
+ setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
36
+ setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
37
+ }
38
+
39
+ export function runPrimitiveCreated(
40
+ {
41
+ boardFileId,
42
+ clearPendingOverviewLayerSelectionTimer,
43
+ pendingEmptyTextEditRef,
44
+ pendingOverviewLayerSelectionRef,
45
+ pendingOverviewScreenSelectionRef,
46
+ pendingTextEditNodeIdRef,
47
+ removeEmptyTextNodeWithRetry,
48
+ setActiveFileId,
49
+ setActiveTool,
50
+ setCreatedOverviewLayerSelection,
51
+ setHoveredElement,
52
+ setMode,
53
+ setOverviewSelectedScreenIds,
54
+ setSelectedElement,
55
+ setSelectedLayerIdsState,
56
+ }: PrimitiveCreatedArgs,
57
+ screenId: string,
58
+ nodeId: string,
59
+ options?: {
60
+ nextTool?: "move" | "pen";
61
+ preserveActiveTool?: boolean;
62
+ },
63
+ ) {
64
+ // B2/B4 fix: stay in overview mode after drawing a primitive. The user
65
+ // drew a shape on the board — they should remain on the board with the
66
+ // new primitive selected, matching Figma behaviour. We activate the
67
+ // target screen (so the layers panel shows its content) and select the
68
+ // new node, but do NOT switch to single/full view.
69
+ //
70
+ // Board guard (overview-zoom corruption fix): the board file is NOT a
71
+ // screen — it never appears in `overviewScreens` and has no
72
+ // `canvasFrames` entry, so activating it flipped the overview zoom
73
+ // basis onto double-fallback inputs (scale snapped to 0.25) while
74
+ // `explicitOverviewCanvasZoom` stayed pinned to the previous screen's
75
+ // scale — the displayed zoom showed garbage (observed: 10241.49%).
76
+ // For a board-created primitive we still select the node and honor the
77
+ // pending text-edit intent below (both are screen-id scoped and work
78
+ // for the board), but keep the previous active FILE and never put the
79
+ // board id into the overview screen-frame selection.
80
+ const isBoardTarget = Boolean(boardFileId && screenId === boardFileId);
81
+ pendingOverviewScreenSelectionRef.current = null;
82
+ pendingOverviewLayerSelectionRef.current = nodeId;
83
+ clearPendingOverviewLayerSelectionTimer();
84
+ flushSync(() => {
85
+ setCreatedOverviewLayerSelection({ screenId, layerId: nodeId });
86
+ if (!isBoardTarget) {
87
+ setActiveFileId(screenId);
88
+ }
89
+ setSelectedElement(null);
90
+ setHoveredElement(null);
91
+ setSelectedLayerIdsState([nodeId]);
92
+ // The new node is the selection; leaving its parent frame selected too
93
+ // hands Cmd+D and alt-drag to the screen-level duplicate instead.
94
+ setOverviewSelectedScreenIds([]);
95
+ if (!options?.preserveActiveTool) {
96
+ setActiveTool(options?.nextTool ?? "move");
97
+ }
98
+ setMode("edit");
99
+ });
100
+ // viewMode stays at "overview" — no setViewMode("single") call here.
101
+
102
+ // Immediately enter text-editing for newly created TEXT primitives. In
103
+ // overview mode the target iframe may become active and receive the
104
+ // inserted HTML over separate renders, so post directly to the target
105
+ // iframe with a few short retries instead of relying on a single global
106
+ // bridge callback.
107
+ const textNodeId = pendingTextEditNodeIdRef.current;
108
+ pendingTextEditNodeIdRef.current = null;
109
+ if (textNodeId) {
110
+ // Cancel any still-pending retry loop from a PREVIOUS text primitive
111
+ // (e.g. the user drew a second text box before the first one settled)
112
+ // before starting a new one, so stale timers can't fight over which
113
+ // node to clean up.
114
+ pendingEmptyTextEditRef.current?.cancel();
115
+ // Creation-race fast path: the retry loop below fires its FIRST
116
+ // begin-text-edit attempt at 180ms and its status-query round trips
117
+ // can leave a multi-second window where keystrokes still hit HOST
118
+ // shortcuts (Delete deleted the just-created text layer). The
119
+ // DesignCanvas runtime bridge's beginTextEdit (registered as
120
+ // window.__designCanvasBeginTextEdit by the active surface's canvas
121
+ // — the flushSync above just pointed activeFileId at this screenId
122
+ // for non-board targets) queues begin-text-edit through the
123
+ // bridge-ready one-shot queue AND arms the host keystroke buffer
124
+ // SYNCHRONOUSLY, so typing is captured from the very first keydown.
125
+ // Activation itself waits until the pointer gesture's trailing click:
126
+ // focusing during mouseup lets that click blur the empty editable one
127
+ // frame later, producing the visible caret blink and losing typing.
128
+ // Best-effort only: if the registered bridge still belongs to a
129
+ // different surface, its iframe no-ops on the unknown nodeId while
130
+ // the buffer still swallows destructive shortcuts, and the
131
+ // screen-id-scoped scheduleBeginTextEditForScreen loop below remains
132
+ // the authoritative per-iframe fallback (resolved through
133
+ // findCanvasIframeForScreen, so board-space text works too).
134
+ if (typeof window !== "undefined") {
135
+ const beginTextEditNow = (window as any).__designCanvasBeginTextEdit;
136
+ if (typeof beginTextEditNow === "function") {
137
+ beginTextEditNow(textNodeId, { afterPointerGesture: true });
138
+ }
139
+ }
140
+ const cancel = scheduleBeginTextEditForScreen(screenId, textNodeId, {
141
+ boardFileId,
142
+ onExhausted: (finalStatus) => {
143
+ const pending = pendingEmptyTextEditRef.current;
144
+ if (!pending || pending.nodeId !== textNodeId || pending.settled) {
145
+ return;
146
+ }
147
+ pending.settled = true;
148
+ if (isTextEditSessionOutcome(finalStatus)) return;
149
+ if (finalStatus === "no-iframe" || finalStatus === "no-reply") {
150
+ // Never reached an editing surface at all. That is a targeting
151
+ // defect, not the user declining to type, so make it audible —
152
+ // but the persisted document, not this outcome, still decides
153
+ // whether the node is empty and should go.
154
+ console.warn(
155
+ `[design] text edit never reached a surface for ${screenId}/${textNodeId} (${finalStatus})`,
156
+ );
157
+ }
158
+ removeEmptyTextNodeWithRetry(screenId, textNodeId);
159
+ },
160
+ });
161
+ pendingEmptyTextEditRef.current = {
162
+ screenId,
163
+ nodeId: textNodeId,
164
+ cancel,
165
+ settled: false,
166
+ };
167
+ }
168
+ }