@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,183 @@
1
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
2
+ import { getFrameGroupBounds } from "@shared/canvas-math";
3
+ import type { CodeLayerNode } from "@shared/code-layer";
4
+ import { buildCodeLayerProjection } from "@shared/code-layer";
5
+ import type { RefObject } from "react";
6
+
7
+ import { trace } from "@/components/design/design-trace";
8
+ import { getInitialFrameGeometry } from "@/components/design/multi-screen/frame-geometry";
9
+ import type { FrameGeometry } from "@/components/design/multi-screen/types";
10
+ import type { ElementInfo } from "@/components/design/types";
11
+ import type { DesignHotkeyAlignEdge } from "@/hooks/useDesignHotkeys";
12
+ import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
13
+ import {
14
+ cloneCanvasFrameGeometry,
15
+ getCanvasFrameGeometry,
16
+ } from "@/pages/design-editor/design-data-geometry-utils";
17
+ import type { AlignableRect } from "@/pages/design-editor/layout-operations";
18
+ import { computeAlignedPositions } from "@/pages/design-editor/layout-operations";
19
+ import { overviewSelectionTargetsElement } from "@/pages/design-editor/selection-state";
20
+ import type { DesignFile } from "@/pages/design-editor/types";
21
+
22
+ export interface AlignSelectionArgs {
23
+ activeFile: DesignFile;
24
+ boardFileId: string | undefined;
25
+ boardFrameGeometry: FrameGeometry | undefined;
26
+ canEditDesign: boolean;
27
+ commitNodePositions: (
28
+ baseContent: string,
29
+ positions: ReadonlyMap<string, { x: number; y: number }>,
30
+ ) => boolean;
31
+ designDataJsonRef: RefObject<Record<string, unknown>>;
32
+ files: DesignFile[];
33
+ getActiveFileSelectedNodeIds: (content: string) => string[];
34
+ getFreshActiveContent: () => string;
35
+ handleGeometryCommit: (
36
+ before: CanvasFrameGeometryById,
37
+ after: CanvasFrameGeometryById,
38
+ options?: { source?: "pointer" | "keyboard" },
39
+ ) => void;
40
+ overviewScreens: OverviewScreen[];
41
+ overviewSelectedScreenIds: string[];
42
+ rectFromCodeLayerNode: (node: CodeLayerNode) => AlignableRect;
43
+ selectedElement: ElementInfo | null;
44
+ selectedLayerIdsState: string[];
45
+ viewModeRef: RefObject<"single" | "overview">;
46
+ }
47
+
48
+ export function runAlignSelection(
49
+ {
50
+ activeFile,
51
+ boardFileId,
52
+ boardFrameGeometry,
53
+ canEditDesign,
54
+ commitNodePositions,
55
+ designDataJsonRef,
56
+ files,
57
+ getActiveFileSelectedNodeIds,
58
+ getFreshActiveContent,
59
+ handleGeometryCommit,
60
+ overviewScreens,
61
+ overviewSelectedScreenIds,
62
+ rectFromCodeLayerNode,
63
+ selectedElement,
64
+ selectedLayerIdsState,
65
+ viewModeRef,
66
+ }: AlignSelectionArgs,
67
+ edge: DesignHotkeyAlignEdge,
68
+ ) {
69
+ trace("structure", "align", { layers: selectedLayerIdsState.length });
70
+ if (!canEditDesign) return;
71
+
72
+ // Overview, 2+ selected SCREENS: align each screen's frame geometry to
73
+ // the selection's combined bounding box through the same
74
+ // handleGeometryCommit path drags/nudges use — one undo step for the
75
+ // whole align. A layer selection must fall through to the element path
76
+ // below instead, as Figma aligns whatever is selected.
77
+ if (
78
+ viewModeRef.current === "overview" &&
79
+ !overviewSelectionTargetsElement({
80
+ selectedElement,
81
+ selectedLayerIds: selectedLayerIdsState,
82
+ fileIds: files.map((file) => file.id),
83
+ })
84
+ ) {
85
+ if (overviewSelectedScreenIds.length < 2) return;
86
+ const before = getCanvasFrameGeometry(designDataJsonRef.current);
87
+ const screenRects: AlignableRect[] = [];
88
+ overviewSelectedScreenIds.forEach((screenId) => {
89
+ const screenIndex = overviewScreens.findIndex(
90
+ (screen) => screen.id === screenId,
91
+ );
92
+ const screen =
93
+ screenIndex >= 0 ? overviewScreens[screenIndex] : undefined;
94
+ const fallbackGeometry =
95
+ screenIndex >= 0
96
+ ? getInitialFrameGeometry(screenIndex, {
97
+ width: screen?.width ?? 1280,
98
+ height: screen?.height ?? 2560,
99
+ })
100
+ : boardFileId === screenId
101
+ ? boardFrameGeometry
102
+ : undefined;
103
+ if (!fallbackGeometry) return;
104
+ const geometry = { ...fallbackGeometry, ...before[screenId] };
105
+ screenRects.push({
106
+ id: screenId,
107
+ x: geometry.x,
108
+ y: geometry.y,
109
+ width: geometry.width,
110
+ height: geometry.height,
111
+ });
112
+ });
113
+ if (screenRects.length < 2) return;
114
+ const bounds = getFrameGroupBounds(screenRects);
115
+ if (!bounds) return;
116
+ const positions = computeAlignedPositions(
117
+ screenRects,
118
+ {
119
+ x: bounds.left,
120
+ y: bounds.top,
121
+ width: bounds.width,
122
+ height: bounds.height,
123
+ },
124
+ edge,
125
+ );
126
+ if (positions.size === 0) return;
127
+ const after = cloneCanvasFrameGeometry(before);
128
+ positions.forEach((position, screenId) => {
129
+ after[screenId] = { ...after[screenId]!, ...position };
130
+ });
131
+ handleGeometryCommit(before, after);
132
+ return;
133
+ }
134
+
135
+ // Single-screen mode: in-screen DOM-node layers.
136
+ if (!activeFile) return;
137
+ const baseContent = getFreshActiveContent();
138
+ const nodeIds = getActiveFileSelectedNodeIds(baseContent);
139
+ if (nodeIds.length === 0) return;
140
+ const projection = buildCodeLayerProjection(baseContent);
141
+ const nodesById = new Map(projection.nodes.map((node) => [node.id, node]));
142
+ const selectedNodes = nodeIds
143
+ .map((nodeId) => nodesById.get(nodeId))
144
+ .filter((node): node is CodeLayerNode => Boolean(node));
145
+ if (selectedNodes.length === 0) return;
146
+ const selectedRects = selectedNodes.map(rectFromCodeLayerNode);
147
+
148
+ if (selectedRects.length >= 2) {
149
+ // Multi-selection: align to the selection's own combined bbox.
150
+ const bounds = getFrameGroupBounds(selectedRects);
151
+ if (!bounds) return;
152
+ const positions = computeAlignedPositions(
153
+ selectedRects,
154
+ {
155
+ x: bounds.left,
156
+ y: bounds.top,
157
+ width: bounds.width,
158
+ height: bounds.height,
159
+ },
160
+ edge,
161
+ );
162
+ if (positions.size === 0) return;
163
+ commitNodePositions(baseContent, positions);
164
+ return;
165
+ }
166
+
167
+ // Single selection: align relative to the parent's content box. A
168
+ // single top-level screen (no code-layer parent) is a no-op, matching
169
+ // Figma (there's nothing to align a lone top-level frame against).
170
+ const soleNode = selectedNodes[0]!;
171
+ const parentId = soleNode.parentId;
172
+ if (!parentId) return;
173
+ const parentNode = nodesById.get(parentId);
174
+ if (!parentNode) return;
175
+ const parentRect = rectFromCodeLayerNode(parentNode);
176
+ const positions = computeAlignedPositions(
177
+ [selectedRects[0]!],
178
+ { x: 0, y: 0, width: parentRect.width, height: parentRect.height },
179
+ edge,
180
+ );
181
+ if (positions.size === 0) return;
182
+ commitNodePositions(baseContent, positions);
183
+ }
@@ -0,0 +1,243 @@
1
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
2
+ import type { Dispatch, RefObject, SetStateAction } from "react";
3
+
4
+ import type { InspectorTab } from "@/components/design/EditPanel";
5
+ import { getScreenPreviewViewport } from "@/components/design/multi-screen/frame-geometry";
6
+ import type { ElementInfo } from "@/components/design/types";
7
+ import type { DesignEditorCommand } from "@/hooks/use-navigation-state";
8
+ import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
9
+ import {
10
+ clampZoom,
11
+ shouldDeferOverviewZoomCommand,
12
+ } from "@/pages/design-editor/overview-camera";
13
+ import {
14
+ DEFAULT_INTERACT_DEVICE_PRESET,
15
+ resolveInteractDeviceForScreen,
16
+ } from "@/pages/design-editor/responsive-interact";
17
+ import { findDesignFileByScreenTarget } from "@/pages/design-editor/screen-command-utils";
18
+ import {
19
+ getDesignToolActivationState,
20
+ isSingleScreenAnnotationTool,
21
+ normalizeDesignLeftPanel,
22
+ normalizeDesignTool,
23
+ } from "@/pages/design-editor/tool-state";
24
+ import type {
25
+ DesignFile,
26
+ DesignLeftPanel,
27
+ DesignTool,
28
+ EditorMode,
29
+ } from "@/pages/design-editor/types";
30
+ import { FOCUSED_SCREEN_ZOOM } from "@/pages/design-editor/types";
31
+
32
+ export interface ApplyDesignEditorCommandArgs {
33
+ canEditDesign: boolean;
34
+ canvasFrameGeometryById: CanvasFrameGeometryById;
35
+ files: DesignFile[];
36
+ id: string | undefined;
37
+ overviewScreens: OverviewScreen[];
38
+ setActiveFileId: Dispatch<SetStateAction<string | null>>;
39
+ setActiveInspectorTab: Dispatch<SetStateAction<InspectorTab>>;
40
+ setActiveLeftPanel: Dispatch<SetStateAction<DesignLeftPanel>>;
41
+ setActiveTool: Dispatch<SetStateAction<DesignTool>>;
42
+ setDrawMode: Dispatch<SetStateAction<boolean>>;
43
+ setInteractDeviceName: Dispatch<SetStateAction<string>>;
44
+ setInteractDeviceSize: Dispatch<
45
+ SetStateAction<{ width: number; height: number }>
46
+ >;
47
+ setMode: Dispatch<SetStateAction<EditorMode>>;
48
+ setPinMode: Dispatch<SetStateAction<boolean>>;
49
+ setScreenZoom: Dispatch<SetStateAction<number>>;
50
+ setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
51
+ setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
52
+ setViewMode: Dispatch<SetStateAction<"single" | "overview">>;
53
+ setZoomForView: (
54
+ targetView: "single" | "overview",
55
+ update: SetStateAction<number>,
56
+ ) => void;
57
+ viewModeRef: RefObject<"single" | "overview">;
58
+ }
59
+
60
+ export function runApplyDesignEditorCommand(
61
+ {
62
+ canEditDesign,
63
+ canvasFrameGeometryById,
64
+ files,
65
+ id,
66
+ overviewScreens,
67
+ setActiveFileId,
68
+ setActiveInspectorTab,
69
+ setActiveLeftPanel,
70
+ setActiveTool,
71
+ setDrawMode,
72
+ setInteractDeviceName,
73
+ setInteractDeviceSize,
74
+ setMode,
75
+ setPinMode,
76
+ setScreenZoom,
77
+ setSelectedElement,
78
+ setSelectedLayerIdsState,
79
+ setViewMode,
80
+ setZoomForView,
81
+ viewModeRef,
82
+ }: ApplyDesignEditorCommandArgs,
83
+ command: DesignEditorCommand | Record<string, unknown>,
84
+ ) {
85
+ if (!id || command.designId !== id) return true;
86
+ const commandRecord = command as Record<string, unknown>;
87
+ const editorView =
88
+ command.editorView === "overview" || command.editorView === "single"
89
+ ? command.editorView
90
+ : command.viewMode === "overview" || command.viewMode === "single"
91
+ ? command.viewMode
92
+ : undefined;
93
+ const target =
94
+ typeof command.fileId === "string"
95
+ ? command.fileId
96
+ : typeof command.screenId === "string"
97
+ ? command.screenId
98
+ : typeof command.filename === "string"
99
+ ? command.filename
100
+ : typeof command.screen === "string"
101
+ ? command.screen
102
+ : null;
103
+ const selectionId =
104
+ typeof command.selection === "string"
105
+ ? command.selection
106
+ : typeof commandRecord.nodeId === "string"
107
+ ? commandRecord.nodeId
108
+ : typeof commandRecord.layerId === "string"
109
+ ? commandRecord.layerId
110
+ : null;
111
+ const targetFile = findDesignFileByScreenTarget(files, target);
112
+ // A navigate command can name a screen the agent just created that the
113
+ // get-design query hasn't refetched yet. Treat any unresolved named target
114
+ // as not-yet-applied (return false) so the app-state key is preserved and
115
+ // re-applied on the next tick once the file loads — not just when there are
116
+ // zero files. Otherwise the navigate is silently consumed and dropped.
117
+ if (target && !targetFile) return false;
118
+
119
+ const inspectorTab =
120
+ command.inspectorTab === "design" ||
121
+ command.inspectorTab === "comments" ||
122
+ command.inspectorTab === "tweaks" ||
123
+ command.inspectorTab === "code"
124
+ ? command.inspectorTab
125
+ : command.inspector === "design" ||
126
+ command.inspector === "comments" ||
127
+ command.inspector === "tweaks" ||
128
+ command.inspector === "code"
129
+ ? command.inspector
130
+ : undefined;
131
+ if (inspectorTab) setActiveInspectorTab(inspectorTab);
132
+ const leftPanel =
133
+ normalizeDesignLeftPanel(command.leftPanel) ??
134
+ normalizeDesignLeftPanel(command.panel) ??
135
+ normalizeDesignLeftPanel(command.inspectorTab) ??
136
+ normalizeDesignLeftPanel(command.inspector);
137
+ if (leftPanel) setActiveLeftPanel(leftPanel);
138
+
139
+ const commandTool = normalizeDesignTool(command.tool);
140
+ const effectiveCommandTool =
141
+ editorView === "overview" &&
142
+ commandTool &&
143
+ isSingleScreenAnnotationTool(commandTool)
144
+ ? "move"
145
+ : commandTool;
146
+ const applyCommandTool = (fallback: DesignTool) => {
147
+ if (!canEditDesign) return;
148
+ const nextTool = effectiveCommandTool ?? fallback;
149
+ const activation = getDesignToolActivationState(nextTool);
150
+ setActiveTool(nextTool);
151
+ setMode(activation.mode);
152
+ setDrawMode(activation.drawMode);
153
+ setPinMode(activation.pinMode);
154
+ };
155
+
156
+ if (targetFile) {
157
+ setActiveFileId(targetFile.id);
158
+ }
159
+ if (selectionId) {
160
+ setSelectedLayerIdsState([selectionId]);
161
+ }
162
+
163
+ const commandZoom =
164
+ typeof command.zoom === "number" && Number.isFinite(command.zoom)
165
+ ? clampZoom(command.zoom)
166
+ : null;
167
+ const targetView = editorView ?? viewModeRef.current;
168
+ // Zoom-compounding fix — see shouldDeferOverviewZoomCommand's doc
169
+ // comment: converting a persisted overview zoom back to canvas units
170
+ // needs the REAL overviewZoomScale (known only once `files` loads), so
171
+ // defer (return "not yet applicable", same contract as the
172
+ // `target && !targetFile` bailout above) instead of committing the
173
+ // conversion against the transient pre-load fallback scale. The mount
174
+ // effect retries automatically once `files` populates and this
175
+ // callback's identity changes.
176
+ if (
177
+ shouldDeferOverviewZoomCommand({
178
+ hasZoomCommand: commandZoom !== null,
179
+ targetView,
180
+ filesLoaded: files.length > 0,
181
+ })
182
+ ) {
183
+ return false;
184
+ }
185
+ if (commandZoom !== null) {
186
+ setZoomForView(targetView, commandZoom);
187
+ }
188
+
189
+ if (editorView === "overview") {
190
+ viewModeRef.current = "overview";
191
+ if (!selectionId) setSelectedElement(null);
192
+ applyCommandTool("move");
193
+ setViewMode("overview");
194
+ } else if (editorView === "single") {
195
+ viewModeRef.current = "single";
196
+ if (!selectionId) setSelectedElement(null);
197
+ const targetScreen = targetFile
198
+ ? overviewScreens.find((screen) => screen.id === targetFile.id)
199
+ : undefined;
200
+ const targetMetadataSize = targetScreen
201
+ ? {
202
+ width: targetScreen.width ?? DEFAULT_INTERACT_DEVICE_PRESET.width,
203
+ height: targetScreen.height ?? DEFAULT_INTERACT_DEVICE_PRESET.height,
204
+ }
205
+ : undefined;
206
+ const targetViewport =
207
+ targetFile && targetMetadataSize
208
+ ? getScreenPreviewViewport(targetMetadataSize, {
209
+ width:
210
+ canvasFrameGeometryById[targetFile.id]?.width ??
211
+ targetMetadataSize.width,
212
+ height:
213
+ canvasFrameGeometryById[targetFile.id]?.height ??
214
+ targetMetadataSize.height,
215
+ })
216
+ : undefined;
217
+ const interactDevice = resolveInteractDeviceForScreen(
218
+ targetViewport
219
+ ? {
220
+ width: targetViewport.viewportWidth,
221
+ height: targetViewport.viewportHeight,
222
+ }
223
+ : undefined,
224
+ );
225
+ setInteractDeviceName(interactDevice.name);
226
+ setInteractDeviceSize({
227
+ width: interactDevice.width,
228
+ height: interactDevice.height,
229
+ });
230
+ setActiveTool("move");
231
+ setDrawMode(false);
232
+ setPinMode(false);
233
+ setMode("interact");
234
+ if (commandZoom === null) {
235
+ setScreenZoom(FOCUSED_SCREEN_ZOOM);
236
+ }
237
+ setViewMode("single");
238
+ } else if (effectiveCommandTool) {
239
+ applyCommandTool("move");
240
+ }
241
+
242
+ return true;
243
+ }
@@ -0,0 +1,236 @@
1
+ import { assertDesignHtmlEditIntegrity } from "@shared/html-integrity";
2
+ import { sourceContentHash } from "@shared/source-workspace";
3
+ import type { QueryClient } from "@tanstack/react-query";
4
+ import type { RefObject } from "react";
5
+ import { toast } from "sonner";
6
+ import * as Y from "yjs";
7
+
8
+ import {
9
+ isShaderWriteInFlight,
10
+ waitForShaderWriteToSettle,
11
+ } from "@/components/design/inspector/GlslShaderPanel";
12
+ import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
13
+ import { resolveScreenCollabSyncTarget } from "@/pages/design-editor/collab-sync";
14
+ import { TAB_ID } from "@/pages/design-editor/editor-session";
15
+ import type { FileContentSaveRequest } from "@/pages/design-editor/editor-state";
16
+ import type { ContentHistoryEntry } from "@/pages/design-editor/history";
17
+ import { designSaveErrorMessage } from "@/pages/design-editor/save-failure";
18
+ import type { DesignFile } from "@/pages/design-editor/types";
19
+
20
+ export interface ApplyFileContentUpdateArgs {
21
+ acknowledgeAuthoritativeClipboardMutation: (args: {
22
+ fileId: string;
23
+ nextContent: string;
24
+ publication?: ClipboardContentMutationPublication;
25
+ }) => void;
26
+ activeFile: DesignFile;
27
+ applyFileContentUpdate: (
28
+ fileId: string,
29
+ nextContent: string,
30
+ options?: {
31
+ refreshPreview?: boolean;
32
+ skipPreview?: boolean;
33
+ forcePreviewFullDocument?: boolean;
34
+ persist?: boolean;
35
+ recordHistory?: boolean;
36
+ updatedAt?: string;
37
+ clipboardMutation?: ClipboardContentMutationPublication;
38
+ },
39
+ ) => void;
40
+ applyLocalContentUpdate: (
41
+ nextContent: string,
42
+ options?: {
43
+ refreshPreview?: boolean;
44
+ skipPreview?: boolean;
45
+ forcePreviewFullDocument?: boolean;
46
+ immediateSave?: boolean;
47
+ persist?: boolean;
48
+ recordHistory?: boolean;
49
+ historyBeforeContent?: string;
50
+ updatedAt?: string;
51
+ clipboardMutation?: ClipboardContentMutationPublication;
52
+ },
53
+ ) => void;
54
+ canEditDesignRef: RefObject<boolean>;
55
+ cancelQueuedFileContentSave: (fileId: string) => void;
56
+ clearPendingLocalFileContent: (
57
+ fileId: string,
58
+ expectedContent?: string,
59
+ ) => void;
60
+ createFileContentSaveRequest: (
61
+ fileId: string,
62
+ content: string,
63
+ syncCollab: boolean,
64
+ ) => FileContentSaveRequest;
65
+ files: DesignFile[];
66
+ getScreenContent: (screenId: string) => string;
67
+ id: string | undefined;
68
+ lastAckedFileContentHashRef: RefObject<Record<string, string>>;
69
+ markPendingLocalFileContent: (
70
+ fileId: string,
71
+ content: string,
72
+ baseUpdatedAt?: string | null,
73
+ ) => void;
74
+ overviewIsSynced: boolean;
75
+ overviewPresenceFileId: string | null;
76
+ overviewYdoc: Y.Doc | null;
77
+ queryClient: QueryClient;
78
+ recordContentHistoryEntry: (entry: ContentHistoryEntry) => void;
79
+ saveFileContent: (pending: FileContentSaveRequest) => void;
80
+ suppressContentHistoryRef: RefObject<boolean>;
81
+ t: (key: string, options?: Record<string, unknown>) => string;
82
+ }
83
+
84
+ export function runApplyFileContentUpdate(
85
+ {
86
+ acknowledgeAuthoritativeClipboardMutation,
87
+ activeFile,
88
+ applyFileContentUpdate,
89
+ applyLocalContentUpdate,
90
+ canEditDesignRef,
91
+ cancelQueuedFileContentSave,
92
+ clearPendingLocalFileContent,
93
+ createFileContentSaveRequest,
94
+ files,
95
+ getScreenContent,
96
+ id,
97
+ lastAckedFileContentHashRef,
98
+ markPendingLocalFileContent,
99
+ overviewIsSynced,
100
+ overviewPresenceFileId,
101
+ overviewYdoc,
102
+ queryClient,
103
+ recordContentHistoryEntry,
104
+ saveFileContent,
105
+ suppressContentHistoryRef,
106
+ t,
107
+ }: ApplyFileContentUpdateArgs,
108
+ fileId: string,
109
+ nextContent: string,
110
+ options: {
111
+ refreshPreview?: boolean;
112
+ skipPreview?: boolean;
113
+ forcePreviewFullDocument?: boolean;
114
+ persist?: boolean;
115
+ recordHistory?: boolean;
116
+ updatedAt?: string;
117
+ clipboardMutation?: ClipboardContentMutationPublication;
118
+ } = {},
119
+ ) {
120
+ if (!canEditDesignRef.current) return;
121
+ if (fileId === activeFile?.id) {
122
+ applyLocalContentUpdate(nextContent, options);
123
+ return;
124
+ }
125
+ // Cross-pipeline write race guard — same hazard commitVisualStyles
126
+ // already defends against (see its withShaderWriteLock note): a shader
127
+ // apply/remove/knob-commit for this same file runs a separate
128
+ // read-source-file -> apply-source-edit round trip, and the overview
129
+ // writeLiveDoc rewrite below replays FULL content into the connected
130
+ // overviewYdoc. Racing the two corrupts the doc (server-side diff vs
131
+ // synchronous untracked full rewrite). Defer the whole update until the
132
+ // in-flight shader write settles; the common no-shader case stays fully
133
+ // synchronous.
134
+ if (isShaderWriteInFlight(fileId)) {
135
+ void waitForShaderWriteToSettle(fileId).then(() => {
136
+ applyFileContentUpdate(fileId, nextContent, options);
137
+ });
138
+ return;
139
+ }
140
+ const previousFile = files.find((file) => file.id === fileId);
141
+ const previousContent =
142
+ getScreenContent(fileId) ?? previousFile?.content ?? "";
143
+ try {
144
+ assertDesignHtmlEditIntegrity({
145
+ previousContent,
146
+ nextContent,
147
+ fileType: previousFile?.fileType ?? "html",
148
+ });
149
+ } catch (error) {
150
+ toast.error(designSaveErrorMessage(error) ?? t("common.genericError"), {
151
+ id: `design-source-integrity:${fileId}`,
152
+ });
153
+ return;
154
+ }
155
+ acknowledgeAuthoritativeClipboardMutation({
156
+ fileId,
157
+ nextContent,
158
+ publication: options.clipboardMutation,
159
+ });
160
+ const shouldRecordHistory =
161
+ options.recordHistory !== false && !options.updatedAt;
162
+ if (
163
+ !suppressContentHistoryRef.current &&
164
+ shouldRecordHistory &&
165
+ previousContent !== nextContent
166
+ ) {
167
+ recordContentHistoryEntry({
168
+ fileId,
169
+ before: previousContent,
170
+ after: nextContent,
171
+ });
172
+ }
173
+ if (options.updatedAt) {
174
+ clearPendingLocalFileContent(fileId);
175
+ // Server-persisted content (see the matching note in
176
+ // applyLocalContentUpdate) — refresh the acked-hash base for the
177
+ // guarded update-file saves.
178
+ lastAckedFileContentHashRef.current[fileId] =
179
+ sourceContentHash(nextContent);
180
+ } else {
181
+ markPendingLocalFileContent(fileId, nextContent, previousFile?.updatedAt);
182
+ }
183
+ queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
184
+ if (!old || typeof old !== "object" || !Array.isArray(old.files)) {
185
+ return old;
186
+ }
187
+ return {
188
+ ...old,
189
+ files: old.files.map((file: DesignFile) =>
190
+ file.id === fileId
191
+ ? {
192
+ ...file,
193
+ content: nextContent,
194
+ ...(options.updatedAt ? { updatedAt: options.updatedAt } : {}),
195
+ }
196
+ : file,
197
+ ),
198
+ };
199
+ });
200
+ // §gesture-persistence — mirror applyLocalContentUpdate's collab-doc
201
+ // write. This screen isn't the active file, but overview mode can still
202
+ // hold a LIVE connected Yjs doc for it via the presence-only
203
+ // `overviewYdoc` subscription (keyed on `overviewPresenceFileId`, the
204
+ // selected/worked screen in overview — see its declaration doc comment).
205
+ // Before this fix, per-screen gesture commits only ever wrote SQL and
206
+ // relied entirely on the server-side `syncCollab: true` -> applyText
207
+ // round-trip to keep that connected doc in step; any gap between the
208
+ // SQL write and the next collab poll/state fetch left the connected
209
+ // client holding pre-edit Yjs text, which a subsequent doc connect
210
+ // (Code panel open) could read back as the seed snapshot. Writing the
211
+ // ydoc directly here — the same untracked-full-rewrite pattern used
212
+ // throughout this file — closes that gap the same way the active-file
213
+ // path already does, and lets syncCollab be skipped for the
214
+ // server-side round-trip since the client push already covers it.
215
+ const { writeLiveDoc, syncCollab } = resolveScreenCollabSyncTarget({
216
+ fileId,
217
+ overviewPresenceFileId,
218
+ overviewDocConnected: !!(overviewYdoc && overviewIsSynced),
219
+ });
220
+ if (writeLiveDoc && overviewYdoc) {
221
+ const ytext = overviewYdoc.getText("content");
222
+ if (ytext.toString() !== nextContent) {
223
+ overviewYdoc.transact(() => {
224
+ ytext.delete(0, ytext.length);
225
+ ytext.insert(0, nextContent);
226
+ }, TAB_ID);
227
+ }
228
+ }
229
+ if (options.persist === false) {
230
+ cancelQueuedFileContentSave(fileId);
231
+ } else {
232
+ saveFileContent(
233
+ createFileContentSaveRequest(fileId, nextContent, syncCollab),
234
+ );
235
+ }
236
+ }