@agent-native/core 0.163.3 → 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 (202) 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/provider-api/actions/provider-api.d.ts +2 -2
  177. package/dist/resources/handlers.d.ts +1 -1
  178. package/dist/server/auth-marketing-locales.d.ts +7 -0
  179. package/dist/server/auth-marketing-locales.js +1322 -0
  180. package/dist/server/auth-marketing.d.ts +1 -0
  181. package/dist/server/auth-marketing.js +35 -6
  182. package/dist/server/auth.js +4 -17
  183. package/dist/server/builder-browser.d.ts +22 -0
  184. package/dist/server/builder-browser.js +77 -1
  185. package/dist/server/builder-oauth.d.ts +50 -0
  186. package/dist/server/builder-oauth.js +263 -0
  187. package/dist/server/core-routes-plugin.d.ts +11 -0
  188. package/dist/server/core-routes-plugin.js +291 -415
  189. package/dist/server/credential-provider.d.ts +1 -1
  190. package/dist/server/credential-provider.js +1 -1
  191. package/dist/server/csrf.js +2 -2
  192. package/dist/server/onboarding-html.js +52 -120
  193. package/dist/server/realtime-token.d.ts +1 -1
  194. package/dist/server/transcribe-voice.d.ts +1 -1
  195. package/dist/settings/store.d.ts +9 -0
  196. package/dist/settings/store.js +19 -0
  197. package/dist/templates/chat/netlify.toml +2 -0
  198. package/dist/vite/client.js +35 -37
  199. package/docs/content/deployment.mdx +2 -0
  200. package/docs/content/multi-app-workspace.mdx +1 -1
  201. package/package.json +2 -2
  202. package/src/templates/chat/netlify.toml +2 -0
@@ -0,0 +1,183 @@
1
+ import { getBrowserTabId } from "@agent-native/core/client/hooks";
2
+ import { buildSignInReturnHref } from "@agent-native/core/client/ui";
3
+ import { buildCodeLayerProjection } from "@shared/code-layer";
4
+
5
+ import type { ElementInfo } from "@/components/design/types";
6
+ import { designSelectionStateKeysForTab } from "@/hooks/use-navigation-state";
7
+ import type {
8
+ PostAuthDesignIntent,
9
+ RuntimeLayerSnapshot,
10
+ } from "@/pages/design-editor/command-types";
11
+
12
+ // Selection is tab-scoped (like navigation) so a second editor tab cannot
13
+ // overwrite this tab's selection context. The global key is mirrored as a
14
+ // fallback for CLI/external agents that do not send a browser tab id.
15
+ export function designSelectionStateKeys(): string[] {
16
+ return designSelectionStateKeysForTab(getBrowserTabId());
17
+ }
18
+
19
+ export function runtimeMultiplicityForElementProvenance(
20
+ snapshots: Record<string, RuntimeLayerSnapshot>,
21
+ info: ElementInfo | null | undefined,
22
+ ): number {
23
+ const provenance = info?.provenance;
24
+ if (!provenance?.sourceFile || !provenance.line || !provenance.column) {
25
+ return 1;
26
+ }
27
+ let count = 0;
28
+ for (const snapshot of Object.values(snapshots)) {
29
+ const projection = buildCodeLayerProjection(snapshot.html);
30
+ for (const node of projection.nodes) {
31
+ const attrs = node.dataAttributes;
32
+ if (
33
+ attrs["data-source-file"] === provenance.sourceFile &&
34
+ Number(attrs["data-source-line"]) === provenance.line &&
35
+ Number(attrs["data-source-column"]) === provenance.column &&
36
+ (!provenance.component ||
37
+ attrs["data-component-name"] === provenance.component)
38
+ ) {
39
+ count += 1;
40
+ }
41
+ }
42
+ }
43
+ return Math.max(1, count);
44
+ }
45
+
46
+ export function buildSignInHrefForDesignIntent(
47
+ intent: PostAuthDesignIntent,
48
+ ): string {
49
+ if (typeof window === "undefined") return buildSignInReturnHref();
50
+ return buildSignInReturnHref({
51
+ returnTo: `${window.location.pathname}?intent=${encodeURIComponent(intent)}`,
52
+ });
53
+ }
54
+
55
+ export function buildSignInHrefForComment(): string {
56
+ if (typeof window === "undefined") return buildSignInReturnHref();
57
+ return buildSignInReturnHref({ returnTo: window.location.pathname });
58
+ }
59
+
60
+ /**
61
+ * True only when the incoming (intent-less) selection authoritatively refers to
62
+ * a different element than the committed one (sourceId match wins, else CSS
63
+ * selector). Returns false when identity can't be compared, so a real selection
64
+ * is never dropped.
65
+ */
66
+ export function isSupersededSelectionEcho(
67
+ incoming: ElementInfo,
68
+ current: ElementInfo | null,
69
+ ): boolean {
70
+ if (!current) return false;
71
+ const incomingId = incoming.sourceId?.trim();
72
+ const currentId = current.sourceId?.trim();
73
+ if (incomingId && currentId) return incomingId !== currentId;
74
+ const incomingSelector = incoming.selector?.trim();
75
+ const currentSelector = current.selector?.trim();
76
+ if (incomingSelector && currentSelector)
77
+ return incomingSelector !== currentSelector;
78
+ return false;
79
+ }
80
+
81
+ export function describeSelectionForHost(element: ElementInfo): {
82
+ label: string;
83
+ detail: string;
84
+ } {
85
+ const text = element.textContent?.trim();
86
+ const label =
87
+ element.provenance?.component ||
88
+ element.componentName ||
89
+ (text ? text.slice(0, 60) : "") ||
90
+ element.tagName.toLowerCase();
91
+ const provenance = element.provenance;
92
+ const sourceFile = provenance?.sourceFile
93
+ ? `${provenance.sourceFile}${provenance.line ? `:${provenance.line}` : ""}`
94
+ : null;
95
+ const classSelector = element.classes?.length
96
+ ? `${element.tagName.toLowerCase()}.${element.classes.slice(0, 2).join(".")}`
97
+ : null;
98
+ const detail =
99
+ sourceFile ??
100
+ classSelector ??
101
+ element.selector ??
102
+ element.tagName.toLowerCase();
103
+ return { label, detail };
104
+ }
105
+
106
+ export function reloadRunningAppPreviewFrames(): void {
107
+ if (typeof document === "undefined") return;
108
+ const frames = document.querySelectorAll<HTMLIFrameElement>(
109
+ "iframe[data-design-preview-iframe]",
110
+ );
111
+ for (const frame of frames) {
112
+ const src = frame.getAttribute("src");
113
+ if (!src) continue;
114
+ let origin: string;
115
+ try {
116
+ origin = new URL(src, window.location.href).origin;
117
+ // coercion-ok: an unparsable src names no container to reload.
118
+ } catch {
119
+ continue;
120
+ }
121
+ if (origin === window.location.origin) continue;
122
+ frame.contentWindow?.postMessage({ type: "agentNative.reload" }, origin);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * A code-layer-derived ElementInfo carries authored inline styles and a zero
128
+ * rect, so the inspector shows 0 for anything the source does not state
129
+ * (hug sizing, in-flow position). Measure the live preview node instead.
130
+ */
131
+ export function withMeasuredGeometry(
132
+ info: ElementInfo,
133
+ screenId?: string,
134
+ ): ElementInfo {
135
+ const rect = info.boundingRect;
136
+ if (rect && (rect.width > 0 || rect.height > 0)) return info;
137
+ if (typeof document === "undefined") return info;
138
+ const selector = info.runtimeSelector ?? info.selector;
139
+ if (!selector) return info;
140
+ // Selectors and stamped ids are per-screen, so an unscoped scan can measure
141
+ // identical markup on a different screen.
142
+ const owning = screenId
143
+ ? document.querySelector<HTMLIFrameElement>(
144
+ `iframe[data-design-preview-iframe][data-screen-iframe-id="${CSS.escape(screenId)}"]`,
145
+ )
146
+ : null;
147
+ const frames = owning
148
+ ? [owning]
149
+ : Array.from(
150
+ document.querySelectorAll<HTMLIFrameElement>(
151
+ "iframe[data-design-preview-iframe]",
152
+ ),
153
+ );
154
+ for (const frame of frames) {
155
+ let node: Element | null = null;
156
+ try {
157
+ node = frame.contentDocument?.querySelector(selector) ?? null;
158
+ } catch {
159
+ node = null;
160
+ }
161
+ if (!node) continue;
162
+ const box = node.getBoundingClientRect();
163
+ if (box.width <= 0 && box.height <= 0) continue;
164
+ const computed = frame.contentWindow?.getComputedStyle(node);
165
+ return {
166
+ ...info,
167
+ boundingRect: {
168
+ x: box.x,
169
+ y: box.y,
170
+ width: box.width,
171
+ height: box.height,
172
+ },
173
+ computedStyles: computed
174
+ ? {
175
+ width: computed.width,
176
+ height: computed.height,
177
+ ...info.computedStyles,
178
+ }
179
+ : info.computedStyles,
180
+ };
181
+ }
182
+ return info;
183
+ }
@@ -0,0 +1,270 @@
1
+ import { shouldUseLiveFileContent } from "@shared/html-content";
2
+ import { sourceContentHash } from "@shared/source-workspace";
3
+ import type { Dispatch, RefObject, SetStateAction } from "react";
4
+ import * as Y from "yjs";
5
+
6
+ import {
7
+ TAB_ID,
8
+ shouldAdoptExternalReconcileContent,
9
+ } from "@/pages/design-editor/editor-session";
10
+ import type { PreviewContentReplaceResult } from "@/pages/design-editor/editor-state";
11
+ import { previewContentReplaceNeedsRenderFallback } from "@/pages/design-editor/editor-state";
12
+ import type { ContentHistoryChange } from "@/pages/design-editor/history";
13
+ import type { DesignFile } from "@/pages/design-editor/types";
14
+
15
+ export interface AdoptDbFileContentArgs {
16
+ activeFile: DesignFile;
17
+ agentActive: boolean;
18
+ clearStaleAgentCollabRecovery: () => void;
19
+ collabContent: string | null;
20
+ collabContentFileId: string | null;
21
+ collabContentFileIdRef: RefObject<string | null>;
22
+ collabContentRef: RefObject<string | null>;
23
+ documentFileContentRef: RefObject<string | null>;
24
+ documentFileUpdatedAtRef: RefObject<string | null>;
25
+ isLeadClient: boolean;
26
+ isSynced: boolean;
27
+ lastAckedFileContentHashRef: RefObject<Record<string, string>>;
28
+ lastAppliedFileUpdatedAtRef: RefObject<string | null>;
29
+ lastLocalContentRef: RefObject<string | null>;
30
+ latestActiveContentRef: RefObject<string | null>;
31
+ recordExternalContentHistoryCheckpoint: (
32
+ change: ContentHistoryChange,
33
+ ) => void;
34
+ replacePreviewContent: (
35
+ nextContent: string,
36
+ selector?: string | null,
37
+ options?: { forceFullDocument?: boolean },
38
+ ) => PreviewContentReplaceResult;
39
+ setCollabContent: Dispatch<SetStateAction<string | null>>;
40
+ setCollabContentFileId: Dispatch<SetStateAction<string | null>>;
41
+ setContentRenderRevision: Dispatch<SetStateAction<number>>;
42
+ staleAgentCollabRecoveryTimerRef: RefObject<number | null>;
43
+ undoManagerRef: RefObject<Y.UndoManager | null>;
44
+ ydoc: Y.Doc | null;
45
+ }
46
+
47
+ export function runAdoptDbFileContent({
48
+ activeFile,
49
+ agentActive,
50
+ clearStaleAgentCollabRecovery,
51
+ collabContent,
52
+ collabContentFileId,
53
+ collabContentFileIdRef,
54
+ collabContentRef,
55
+ documentFileContentRef,
56
+ documentFileUpdatedAtRef,
57
+ isLeadClient,
58
+ isSynced,
59
+ lastAckedFileContentHashRef,
60
+ lastAppliedFileUpdatedAtRef,
61
+ lastLocalContentRef,
62
+ latestActiveContentRef,
63
+ recordExternalContentHistoryCheckpoint,
64
+ replacePreviewContent,
65
+ setCollabContent,
66
+ setCollabContentFileId,
67
+ setContentRenderRevision,
68
+ staleAgentCollabRecoveryTimerRef,
69
+ undoManagerRef,
70
+ ydoc,
71
+ }: AdoptDbFileContentArgs) {
72
+ if (!activeFile || !isSynced) return;
73
+ const dbContent = activeFile.content ?? "";
74
+ const dbUpdatedAt = activeFile.updatedAt ?? null;
75
+ const activeScopedCollabContent =
76
+ collabContentFileId === activeFile.id ? collabContent : null;
77
+ if (
78
+ typeof activeScopedCollabContent === "string" &&
79
+ !shouldUseLiveFileContent({
80
+ liveContent: activeScopedCollabContent,
81
+ storedContent: dbContent,
82
+ fileType: activeFile.fileType,
83
+ })
84
+ ) {
85
+ clearStaleAgentCollabRecovery();
86
+ setCollabContent(dbContent);
87
+ setCollabContentFileId(activeFile.id);
88
+ lastLocalContentRef.current = dbContent;
89
+ latestActiveContentRef.current = dbContent;
90
+ lastAckedFileContentHashRef.current[activeFile.id] =
91
+ sourceContentHash(dbContent);
92
+ if (dbUpdatedAt) lastAppliedFileUpdatedAtRef.current = dbUpdatedAt;
93
+ if (
94
+ previewContentReplaceNeedsRenderFallback(
95
+ replacePreviewContent(dbContent, null, {
96
+ forceFullDocument: true,
97
+ }),
98
+ )
99
+ ) {
100
+ setContentRenderRevision((revision) => revision + 1);
101
+ }
102
+
103
+ if (isLeadClient && ydoc) {
104
+ const ytext = ydoc.getText("content");
105
+ if (ytext.toString() !== dbContent) {
106
+ // Untracked full rewrite (agent edit / external DB content
107
+ // replacing a live doc that diverged) — clear the undo stack so a
108
+ // stale tracked delta can't be replayed against content it no
109
+ // longer matches (see U1: this is the primary corruption path —
110
+ // agent edits, motion autosave, and id-stamping all land here).
111
+ undoManagerRef.current?.clear(true, false);
112
+ ydoc.transact(() => {
113
+ ytext.delete(0, ytext.length);
114
+ ytext.insert(0, dbContent);
115
+ }, TAB_ID);
116
+ }
117
+ }
118
+ return;
119
+ }
120
+
121
+ // Already reflecting this exact content (our own echo or Yjs already
122
+ // delivered it) — just advance the watermark and stop.
123
+ if (
124
+ activeScopedCollabContent === dbContent ||
125
+ lastLocalContentRef.current === dbContent
126
+ ) {
127
+ if (dbUpdatedAt) lastAppliedFileUpdatedAtRef.current = dbUpdatedAt;
128
+ return;
129
+ }
130
+
131
+ // Only adopt genuinely newer content. No baseline yet (fresh file load)
132
+ // always adopts so a stale persisted Y.Doc can't shadow newer SQL. See
133
+ // shouldAdoptExternalReconcileContent's doc comment
134
+ // (design-editor/editor-session.ts) for the same-millisecond tie-break
135
+ // fix this closes: a strict `>` used to silently drop a real external
136
+ // write that landed in the same millisecond as the one already applied,
137
+ // whenever agentActive was false.
138
+ const applied = lastAppliedFileUpdatedAtRef.current;
139
+ const externalNewer = shouldAdoptExternalReconcileContent({
140
+ appliedUpdatedAt: applied,
141
+ dbUpdatedAt,
142
+ agentActive,
143
+ });
144
+ const staleAgentEchoPossible =
145
+ agentActive &&
146
+ !!applied &&
147
+ !!dbUpdatedAt &&
148
+ dbUpdatedAt === applied &&
149
+ lastLocalContentRef.current !== activeScopedCollabContent;
150
+ if (!externalNewer) {
151
+ if (staleAgentEchoPossible) {
152
+ if (staleAgentCollabRecoveryTimerRef.current === null) {
153
+ const expectedContent = dbContent;
154
+ const expectedUpdatedAt = dbUpdatedAt;
155
+ const expectedFileId = activeFile.id;
156
+ staleAgentCollabRecoveryTimerRef.current = window.setTimeout(() => {
157
+ staleAgentCollabRecoveryTimerRef.current = null;
158
+ const currentCollab = collabContentRef.current;
159
+ if (collabContentFileIdRef.current !== expectedFileId) return;
160
+ if (documentFileUpdatedAtRef.current !== expectedUpdatedAt) return;
161
+ if (documentFileContentRef.current !== expectedContent) return;
162
+ if (currentCollab === expectedContent) return;
163
+ if (lastLocalContentRef.current === currentCollab) return;
164
+
165
+ setCollabContent(expectedContent);
166
+ setCollabContentFileId(expectedFileId);
167
+ lastLocalContentRef.current = expectedContent;
168
+ latestActiveContentRef.current = expectedContent;
169
+ lastAckedFileContentHashRef.current[expectedFileId] =
170
+ sourceContentHash(expectedContent);
171
+ lastAppliedFileUpdatedAtRef.current = expectedUpdatedAt;
172
+ if (
173
+ previewContentReplaceNeedsRenderFallback(
174
+ replacePreviewContent(expectedContent, null, {
175
+ forceFullDocument: true,
176
+ }),
177
+ )
178
+ ) {
179
+ setContentRenderRevision((revision) => revision + 1);
180
+ }
181
+
182
+ if (isLeadClient && ydoc) {
183
+ const ytext = ydoc.getText("content");
184
+ if (ytext.toString() !== expectedContent) {
185
+ // Untracked full rewrite — see U1 note above.
186
+ undoManagerRef.current?.clear(true, false);
187
+ ydoc.transact(() => {
188
+ ytext.delete(0, ytext.length);
189
+ ytext.insert(0, expectedContent);
190
+ }, TAB_ID);
191
+ }
192
+ }
193
+ }, 1200);
194
+ }
195
+ } else {
196
+ clearStaleAgentCollabRecovery();
197
+ }
198
+ return;
199
+ }
200
+ clearStaleAgentCollabRecovery();
201
+
202
+ // U21: this whole effect exists BECAUSE the Yjs observe path (which
203
+ // already checkpoints agent edits into the local undo fallback, see U3
204
+ // above at the ytext.observe handler) can miss the update — same-tab
205
+ // background, a paused collab poll, or no collab session ever
206
+ // established for this viewer. Every branch below used to respond to
207
+ // that exact case by clearing the undo manager with nothing to fall back
208
+ // to, so Cmd+Z after an agent-driven full-content replacement that
209
+ // landed via THIS path had nothing left to restore — the reported
210
+ // "pressed cmd+z but unable to change it back", reproduced end-to-end:
211
+ // an `edit-design`/`update-file` write (mode=replace-file) lands purely
212
+ // through `writeInlineSourceFile`'s SQL write, never through the Yjs
213
+ // collab broadcast that the U3 checkpoint above depends on to learn
214
+ // `agentActive` — so gating this path on the SAME `agentActive` flag
215
+ // (sourced from `useCollaborativeDoc`'s Yjs `requestSource === "agent"`
216
+ // signal) would almost never fire here, since the very reason a change
217
+ // lands on THIS path instead of the Yjs-observe path is that no matching
218
+ // Yjs broadcast arrived to flip that flag. Unlike the Yjs-observe site
219
+ // (which can distinguish an agent transaction from a human peer's by
220
+ // origin), this path cannot reliably tell "agent" from "another human
221
+ // editing the same design without a live collab session" apart — but
222
+ // recording a local undo checkpoint either way is still correct and
223
+ // safe: it only affects what THIS viewer's Cmd+Z reverts to, never what
224
+ // gets written back to any other collaborator.
225
+ const previousActiveContentForCheckpoint = latestActiveContentRef.current;
226
+ if (
227
+ typeof previousActiveContentForCheckpoint === "string" &&
228
+ previousActiveContentForCheckpoint !== dbContent
229
+ ) {
230
+ recordExternalContentHistoryCheckpoint({
231
+ fileId: activeFile.id,
232
+ before: previousActiveContentForCheckpoint,
233
+ after: dbContent,
234
+ });
235
+ }
236
+
237
+ // Render the newer content immediately so the preview is never stale.
238
+ setCollabContent(dbContent);
239
+ setCollabContentFileId(activeFile.id);
240
+ lastLocalContentRef.current = dbContent;
241
+ latestActiveContentRef.current = dbContent;
242
+ lastAckedFileContentHashRef.current[activeFile.id] =
243
+ sourceContentHash(dbContent);
244
+ if (dbUpdatedAt) lastAppliedFileUpdatedAtRef.current = dbUpdatedAt;
245
+ if (
246
+ previewContentReplaceNeedsRenderFallback(
247
+ replacePreviewContent(dbContent, null, { forceFullDocument: true }),
248
+ )
249
+ ) {
250
+ setContentRenderRevision((revision) => revision + 1);
251
+ }
252
+
253
+ // Lead client mirrors it into the shared Y.Doc so other open clients
254
+ // receive it through Yjs and the durable collab state stays in step. The
255
+ // agent's update-file/generate-design already wrote the Y.Doc in-process,
256
+ // so in the common case this is a no-op diff; it only does real work when
257
+ // the Yjs update was missed (the failure this fallback exists to cover).
258
+ if (isLeadClient && ydoc) {
259
+ const ytext = ydoc.getText("content");
260
+ if (ytext.toString() !== dbContent) {
261
+ // Untracked full rewrite — see U1 note above. The view-appropriate
262
+ // checkpoint recorded above (U21) is what Cmd+Z now falls back to.
263
+ undoManagerRef.current?.clear(true, false);
264
+ ydoc.transact(() => {
265
+ ytext.delete(0, ytext.length);
266
+ ytext.insert(0, dbContent);
267
+ }, TAB_ID);
268
+ }
269
+ }
270
+ }
@@ -0,0 +1,134 @@
1
+ import {
2
+ removeAgentChatContextItem,
3
+ setAgentChatContextItem,
4
+ } from "@agent-native/core/client/agent-chat";
5
+ import type { CodeLayerNode } from "@shared/code-layer";
6
+ import type { RefObject } from "react";
7
+
8
+ import type { ElementInfo } from "@/components/design/types";
9
+ import { nodeRepromptSubtreeExcerpt } from "@/lib/node-reprompt";
10
+ import { shouldMirrorSelectedElementToAgentChat } from "@/pages/design-editor/selection-state";
11
+ import type { DesignData, DesignFile } from "@/pages/design-editor/types";
12
+
13
+ export interface MirrorSelectionToAgentChatArgs {
14
+ activeFile: DesignFile;
15
+ activeProjectionContent: string;
16
+ composerContextHasOurKeyRef: RefObject<boolean>;
17
+ design: DesignData | null;
18
+ id: string | undefined;
19
+ isSignedIn: boolean;
20
+ mirroredSelectionIdRef: RefObject<string | null>;
21
+ selectedCodeLayerNode: CodeLayerNode | null;
22
+ selectedElement: ElementInfo | null;
23
+ sentSelectionIdRef: RefObject<string | null>;
24
+ }
25
+
26
+ export function runMirrorSelectionToAgentChat({
27
+ activeFile,
28
+ activeProjectionContent,
29
+ composerContextHasOurKeyRef,
30
+ design,
31
+ id,
32
+ isSignedIn,
33
+ mirroredSelectionIdRef,
34
+ selectedCodeLayerNode,
35
+ selectedElement,
36
+ sentSelectionIdRef,
37
+ }: MirrorSelectionToAgentChatArgs) {
38
+ const key = "design:selected-element";
39
+ if (!isSignedIn) return;
40
+ if (!id || !shouldMirrorSelectedElementToAgentChat(selectedElement)) {
41
+ mirroredSelectionIdRef.current = null;
42
+ sentSelectionIdRef.current = null;
43
+ removeAgentChatContextItem(key);
44
+ return;
45
+ }
46
+
47
+ const selectionId = `${activeFile?.id ?? ""}::${selectedElement.sourceId ?? selectedElement.selector}`;
48
+ if (selectionId !== mirroredSelectionIdRef.current) {
49
+ // A genuinely new/changed selection always (re)attaches, regardless of
50
+ // whether the previous one was marked sent.
51
+ sentSelectionIdRef.current = null;
52
+ } else if (
53
+ sentSelectionIdRef.current === selectionId ||
54
+ !composerContextHasOurKeyRef.current
55
+ ) {
56
+ // Same selection as before, and either it was already marked sent, or
57
+ // the shared store no longer carries our key (a send just cleared it,
58
+ // observed by the bookkeeping effect below) — stay cleared. Critically:
59
+ // do nothing else here, so this branch never calls
60
+ // setAgentChatContextItem for a selection that hasn't changed.
61
+ if (!composerContextHasOurKeyRef.current) {
62
+ sentSelectionIdRef.current = selectionId;
63
+ }
64
+ return;
65
+ } else {
66
+ // Same selection, still present in the shared store, nothing to do —
67
+ // avoid republishing (and thus avoid the feedback loop above) when
68
+ // nothing about the selection actually changed.
69
+ return;
70
+ }
71
+ mirroredSelectionIdRef.current = selectionId;
72
+
73
+ const labelSource =
74
+ selectedElement.textContent?.trim() ||
75
+ selectedCodeLayerNode?.layerName ||
76
+ selectedElement.id ||
77
+ selectedElement.tagName.toLowerCase();
78
+ const shortLabel =
79
+ labelSource.length > 28 ? `${labelSource.slice(0, 25)}...` : labelSource;
80
+ const targetNodeId =
81
+ selectedCodeLayerNode?.dataAttributes[
82
+ "data-agent-native-node-id"
83
+ ]?.trim() ??
84
+ selectedElement.sourceId ??
85
+ null;
86
+ const targetSelector =
87
+ selectedCodeLayerNode?.selector ?? selectedElement.selector ?? null;
88
+ // Excerpt the outerHTML out of the SOURCE projection rather than the
89
+ // rendered DOM: this is the exact text edit-design's search/replace has
90
+ // to match, so an edit anchored to it cannot drift onto a child or
91
+ // sibling that merely measures the same on canvas.
92
+ const selectedNodeSpan = selectedCodeLayerNode?.source;
93
+ const outerHtmlExcerpt = selectedNodeSpan
94
+ ? nodeRepromptSubtreeExcerpt(
95
+ activeProjectionContent.slice(
96
+ selectedNodeSpan.start,
97
+ selectedNodeSpan.end,
98
+ ),
99
+ )
100
+ : "";
101
+ const contextLines = [
102
+ `Selected design element in design "${design?.title ?? id}".`,
103
+ `designId: ${id}`,
104
+ activeFile ? `fileId: ${activeFile.id}` : "",
105
+ activeFile ? `Active screen: ${activeFile.filename}` : "",
106
+ `target: ${targetNodeId ?? targetSelector ?? "unknown"}`,
107
+ targetNodeId ? `targetNodeId: ${targetNodeId}` : "",
108
+ targetSelector ? `targetSelector: ${targetSelector}` : "",
109
+ `Element: <${selectedElement.tagName.toLowerCase()}> ${shortLabel}`,
110
+ selectedCodeLayerNode ? `Code layer id: ${selectedCodeLayerNode.id}` : "",
111
+ selectedElement.classes.length
112
+ ? `Classes: ${selectedElement.classes.join(" ")}`
113
+ : "",
114
+ selectedElement.textContent?.trim()
115
+ ? `Text: ${selectedElement.textContent.trim()}`
116
+ : "",
117
+ outerHtmlExcerpt
118
+ ? `--- selected element (outerHTML excerpt, truncated) ---\n${outerHtmlExcerpt}`
119
+ : "",
120
+ ].filter(Boolean);
121
+
122
+ setAgentChatContextItem({
123
+ key,
124
+ title: shortLabel,
125
+ context: contextLines.join("\n"),
126
+ openSidebar: false,
127
+ // Mirror the selection into chat context without stealing focus: this
128
+ // effect re-fires on every selection change and on each get-design poll
129
+ // during an agent run, and focusing the composer here would blur (and
130
+ // tear down) an in-progress inline text edit on the canvas.
131
+ focus: false,
132
+ });
133
+ composerContextHasOurKeyRef.current = true;
134
+ }