@agent-native/core 0.90.4 → 0.90.7

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 (189) hide show
  1. package/README.md +34 -7
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +21 -0
  4. package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
  5. package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  6. package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  7. package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  8. package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  9. package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  10. package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  11. package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  12. package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  13. package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  14. package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  15. package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
  16. package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
  17. package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
  18. package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
  19. package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
  20. package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
  21. package/corpus/core/docs/content/toolkit-history.mdx +1 -1
  22. package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
  23. package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
  24. package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
  25. package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
  26. package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
  27. package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
  28. package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
  29. package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
  30. package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
  31. package/corpus/core/package.json +1 -1
  32. package/corpus/core/src/agent/durable-background.ts +18 -18
  33. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
  34. package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
  35. package/corpus/core/src/agent/production-agent.ts +22 -11
  36. package/corpus/core/src/agent/run-manager.ts +17 -6
  37. package/corpus/core/src/agent/run-store.ts +16 -11
  38. package/corpus/core/src/cli/design-connect.ts +16 -2
  39. package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
  40. package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
  41. package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
  42. package/corpus/core/src/client/use-db-sync.ts +99 -1
  43. package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
  44. package/corpus/core/src/collab/client.ts +49 -4
  45. package/corpus/core/src/deploy/build.ts +103 -0
  46. package/corpus/core/src/server/core-routes-plugin.ts +137 -8
  47. package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
  48. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
  49. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
  50. package/corpus/templates/content/AGENTS.md +1 -0
  51. package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
  52. package/corpus/templates/content/actions/_database-utils.ts +2 -1
  53. package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
  54. package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
  55. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
  56. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
  57. package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
  58. package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
  59. package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
  60. package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
  61. package/corpus/templates/content/app/i18n-data.ts +31 -0
  62. package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
  63. package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
  64. package/corpus/templates/content/parity/matrix.md +24 -24
  65. package/corpus/templates/content/parity/matrix.ts +2 -0
  66. package/corpus/templates/content/shared/api.ts +59 -4
  67. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
  68. package/corpus/templates/design/actions/connect-localhost.ts +26 -3
  69. package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
  70. package/corpus/templates/design/actions/read-source-file.ts +28 -0
  71. package/corpus/templates/design/actions/update-file.ts +76 -7
  72. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +207 -11
  73. package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
  74. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
  75. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
  76. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
  77. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
  78. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
  79. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
  80. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
  81. package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
  82. package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
  83. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  84. package/corpus/templates/design/app/i18n-data.ts +19 -0
  85. package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
  86. package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
  87. package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
  88. package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
  89. package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
  90. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
  91. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
  92. package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
  93. package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
  94. package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
  95. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
  96. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
  97. package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
  98. package/corpus/templates/design/server/source-workspace.ts +27 -3
  99. package/corpus/templates/design/shared/board-file.ts +359 -0
  100. package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
  101. package/corpus/templates/design/shared/code-layer.ts +145 -10
  102. package/dist/agent/durable-background.d.ts +12 -12
  103. package/dist/agent/durable-background.js +18 -18
  104. package/dist/agent/durable-background.js.map +1 -1
  105. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  106. package/dist/agent/engine/ai-sdk-engine.js +8 -2
  107. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  108. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  109. package/dist/agent/engine/anthropic-engine.js +11 -1
  110. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  111. package/dist/agent/production-agent.d.ts +2 -2
  112. package/dist/agent/production-agent.d.ts.map +1 -1
  113. package/dist/agent/production-agent.js +18 -11
  114. package/dist/agent/production-agent.js.map +1 -1
  115. package/dist/agent/run-manager.d.ts +7 -1
  116. package/dist/agent/run-manager.d.ts.map +1 -1
  117. package/dist/agent/run-manager.js +6 -1
  118. package/dist/agent/run-manager.js.map +1 -1
  119. package/dist/agent/run-store.d.ts +1 -1
  120. package/dist/agent/run-store.d.ts.map +1 -1
  121. package/dist/agent/run-store.js +15 -10
  122. package/dist/agent/run-store.js.map +1 -1
  123. package/dist/cli/design-connect.d.ts.map +1 -1
  124. package/dist/cli/design-connect.js +12 -2
  125. package/dist/cli/design-connect.js.map +1 -1
  126. package/dist/client/KeepTabOpenNotice.d.ts +9 -10
  127. package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
  128. package/dist/client/KeepTabOpenNotice.js +8 -4
  129. package/dist/client/KeepTabOpenNotice.js.map +1 -1
  130. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  131. package/dist/client/RunStuckBanner.js +14 -13
  132. package/dist/client/RunStuckBanner.js.map +1 -1
  133. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  134. package/dist/client/agent-chat-adapter.js +25 -17
  135. package/dist/client/agent-chat-adapter.js.map +1 -1
  136. package/dist/client/use-db-sync.d.ts +18 -0
  137. package/dist/client/use-db-sync.d.ts.map +1 -1
  138. package/dist/client/use-db-sync.js +88 -1
  139. package/dist/client/use-db-sync.js.map +1 -1
  140. package/dist/client/use-run-stuck-detection.d.ts +1 -1
  141. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  142. package/dist/client/use-run-stuck-detection.js +6 -4
  143. package/dist/client/use-run-stuck-detection.js.map +1 -1
  144. package/dist/collab/client.d.ts.map +1 -1
  145. package/dist/collab/client.js +43 -4
  146. package/dist/collab/client.js.map +1 -1
  147. package/dist/collab/routes.d.ts +1 -1
  148. package/dist/collab/struct-routes.d.ts +1 -1
  149. package/dist/deploy/build.d.ts +1 -0
  150. package/dist/deploy/build.d.ts.map +1 -1
  151. package/dist/deploy/build.js +57 -0
  152. package/dist/deploy/build.js.map +1 -1
  153. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  154. package/dist/notifications/routes.d.ts +2 -2
  155. package/dist/resources/handlers.d.ts +3 -3
  156. package/dist/secrets/routes.d.ts +9 -9
  157. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  158. package/dist/server/core-routes-plugin.d.ts +10 -0
  159. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  160. package/dist/server/core-routes-plugin.js +103 -9
  161. package/dist/server/core-routes-plugin.js.map +1 -1
  162. package/docs/content/agent-native-toolkit.mdx +1 -1
  163. package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  164. package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  165. package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  166. package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  167. package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  168. package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  169. package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  170. package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  171. package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  172. package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  173. package/docs/content/toolkit-agent-ux.mdx +1 -1
  174. package/docs/content/toolkit-app-adapters.mdx +1 -1
  175. package/docs/content/toolkit-collaboration-ui.mdx +1 -1
  176. package/docs/content/toolkit-collaboration.mdx +1 -1
  177. package/docs/content/toolkit-command-navigation.mdx +2 -2
  178. package/docs/content/toolkit-comments-review.mdx +1 -1
  179. package/docs/content/toolkit-history.mdx +1 -1
  180. package/docs/content/toolkit-observability.mdx +1 -1
  181. package/docs/content/toolkit-org-team.mdx +1 -1
  182. package/docs/content/toolkit-resources.mdx +1 -1
  183. package/docs/content/toolkit-settings.mdx +1 -1
  184. package/docs/content/toolkit-setup-connections.mdx +1 -1
  185. package/docs/content/toolkit-sharing-ui.mdx +1 -1
  186. package/docs/content/toolkit-sharing.mdx +1 -1
  187. package/docs/content/toolkit-shell-hooks.mdx +1 -1
  188. package/docs/content/toolkit-ui.mdx +1 -1
  189. package/package.json +1 -1
@@ -30,6 +30,34 @@ export default defineAction({
30
30
  const workspace = await resolveSourceWorkspace(designId, {
31
31
  includeContent: true,
32
32
  });
33
+ // The board overlay file is a reserved canvas-model document, not a
34
+ // source file the code workbench edits — resolveSourceWorkspace already
35
+ // excludes it from `files`. Asking for "the board's source" (e.g. a UI
36
+ // surface that follows setActiveFileId to whatever screen a cross-screen
37
+ // drop just landed on, including the board) is a legitimate no-op query,
38
+ // not an error: return an empty/readonly placeholder instead of letting
39
+ // findSourceWorkspaceFile throw a 404-as-500 for every such request.
40
+ if (fileId && workspace.boardFileId && fileId === workspace.boardFileId) {
41
+ return {
42
+ designId,
43
+ path: "__board__.html",
44
+ displayName: "__board__.html",
45
+ fileId,
46
+ sourceType: workspace.sourceType,
47
+ backendKind: "virtual-inline",
48
+ readonly: true,
49
+ language: "html",
50
+ content: "",
51
+ versionHash: "",
52
+ updatedAt: null,
53
+ provenance: {
54
+ kind: "design-file" as const,
55
+ designId,
56
+ fileId,
57
+ filename: "__board__.html",
58
+ },
59
+ };
60
+ }
33
61
  const file = findSourceWorkspaceFile(workspace.files, { fileId, path });
34
62
  const live = await readLiveSourceFile(file);
35
63
  return {
@@ -33,8 +33,14 @@ function rowsAffected(result: unknown): number | undefined {
33
33
  * `skippedStaleMirror?: true` is added to the result ONLY in the narrow
34
34
  * SQL-mirror-only staleness case: `content` was provided, `syncCollab` was
35
35
  * explicitly `false`, a live collaboration document exists for this file,
36
- * and the caller's `expectedVersionHash` no longer matches that live text.
37
- * In that case the content column is intentionally left untouched (the live
36
+ * and the caller's `expectedVersionHash` matches NEITHER the live collab
37
+ * text, NOR the `content` being written (an own edit that raced ahead via
38
+ * Yjs), NOR the current SQL mirror content. A caller whose hash matches the
39
+ * current mirror is the mirror column's own lineage (mirror-lineage rescue):
40
+ * it proceeds instead of skipping, advancing the mirror AND diff-merging its
41
+ * content into the live doc.
42
+ * In the genuinely-stale case the content column is intentionally left
43
+ * untouched (the live
38
44
  * collab document remains the source of truth) while any `filename`/
39
45
  * `fileType` updates in the same call still apply, and the action returns
40
46
  * success instead of throwing. The field is omitted (not `false`) in every
@@ -163,7 +169,24 @@ export default defineAction({
163
169
  // trying to sync into collab in the first place. Every other
164
170
  // expectedVersionHash combination (syncCollab true/default, or no live
165
171
  // collab state, or a matching hash, or no hash at all) is UNCHANGED.
172
+ //
173
+ // Own-edit false-positive fix: a single client's own edit reaches the
174
+ // live collab doc via TWO independent, unordered paths — the Yjs
175
+ // update (~80ms client debounce, applied to the server's in-memory doc
176
+ // as soon as its POST lands) and this guarded update-file call (~400ms
177
+ // client debounce). The Yjs path usually wins the race, so by the time
178
+ // this call's hash check runs, `liveContent` often already equals the
179
+ // very `content` this call is trying to write — that is NOT a
180
+ // divergent concurrent edit, it's the same edit having arrived early
181
+ // by a different transport. Comparing hashes first would reject that
182
+ // as "stale" and permanently skip the SQL mirror write (there is no
183
+ // background job that later reconciles design_files.content from the
184
+ // live collab doc — see hasCollabState below), silently losing writes
185
+ // on every edit after the first in a session. Check content equality
186
+ // BEFORE the hash comparison so this exact-match case always proceeds
187
+ // as a normal write instead of hitting either the skip or throw path.
166
188
  let skipContentWrite = false;
189
+ let mirrorLineageCollabSync = false;
167
190
  if (expectedVersionHash !== undefined && content !== undefined) {
168
191
  const collabExists = await hasCollabState(id);
169
192
  let liveContent: string;
@@ -177,10 +200,52 @@ export default defineAction({
177
200
  .limit(1);
178
201
  liveContent = current?.content ?? "";
179
202
  }
180
- if (sourceContentHash(liveContent) !== expectedVersionHash) {
203
+ if (
204
+ liveContent !== content &&
205
+ sourceContentHash(liveContent) !== expectedVersionHash
206
+ ) {
181
207
  if (syncCollab === false && collabExists) {
182
- skipContentWrite = true;
183
- skippedStaleMirror = true;
208
+ // Mirror-lineage rescue (sequential-edit data-loss fix, verified
209
+ // live): the client's Yjs transact and its guarded update-file
210
+ // call ride two independent transports, and the Yjs pipe can lag
211
+ // or silently die — reproduced with a live collab doc that never
212
+ // received EITHER of two sequential scrub edits while the HTTP
213
+ // saves advanced the SQL mirror normally. Comparing the caller
214
+ // only against that stale live text mis-classified the SECOND
215
+ // save as a divergent writer and silently dropped it. When the
216
+ // caller's expectedVersionHash matches the CURRENT SQL mirror,
217
+ // the caller is the mirror's own uninterrupted lineage (a plain
218
+ // CAS success against the column this write updates) while the
219
+ // live doc is the diverging party — a dead/lagging client Yjs
220
+ // pipe or a concurrent live-only editor. Do NOT skip, and do NOT
221
+ // silently drop either side: proceed with the mirror write AND
222
+ // push `content` through the collab layer exactly like
223
+ // syncCollab:true does (mirrorLineageCollabSync below). The
224
+ // applyText char-diff merge folds the caller's change into the
225
+ // live doc as a CRDT diff, so no one's edits are dropped: the
226
+ // mirror advances with the caller, and the live doc receives the
227
+ // caller's change as a diff-merge that preserves any other
228
+ // editor's live edits. Only callers matching NEITHER the live
229
+ // text NOR the mirror are genuinely stale and still hit the skip
230
+ // below.
231
+ const [mirrorRow] = await db
232
+ .select({ content: schema.designFiles.content })
233
+ .from(schema.designFiles)
234
+ .where(eq(schema.designFiles.id, id))
235
+ .limit(1);
236
+ if (
237
+ sourceContentHash(mirrorRow?.content ?? "") ===
238
+ expectedVersionHash
239
+ ) {
240
+ // Caller is exactly at the persisted mirror's tip — the live
241
+ // collab doc is the lagging party, not the caller. Write the
242
+ // mirror normally and also sync the caller's content into the
243
+ // live doc via the collab diff-merge below.
244
+ mirrorLineageCollabSync = true;
245
+ } else {
246
+ skipContentWrite = true;
247
+ skippedStaleMirror = true;
248
+ }
184
249
  } else {
185
250
  throw new Error(
186
251
  "File changed since it was read. Re-read the file and retry.",
@@ -263,8 +328,12 @@ export default defineAction({
263
328
  }
264
329
  }
265
330
 
266
- // Push content through the collab layer so live editors see the change
267
- if (content !== undefined && syncCollab) {
331
+ // Push content through the collab layer so live editors see the change.
332
+ // mirrorLineageCollabSync: a syncCollab:false caller whose hash matched
333
+ // the current SQL mirror (mirror-lineage rescue) also syncs here, so a
334
+ // dead/lagging live doc receives the caller's change as a CRDT
335
+ // diff-merge instead of silently diverging from the mirror.
336
+ if (content !== undefined && (syncCollab || mirrorLineageCollabSync)) {
268
337
  const collabExists = await hasCollabState(id);
269
338
  if (collabExists) {
270
339
  await applyText(id, content, "content", "agent");
@@ -865,9 +865,16 @@ function snapshotEndpointUrl(bridgeUrl: string, previewUrl: string): string {
865
865
  return endpoint.toString();
866
866
  }
867
867
 
868
- function liveEditEndpointUrl(bridgeUrl: string, previewUrl: string): string {
868
+ export function liveEditEndpointUrl(
869
+ bridgeUrl: string,
870
+ previewUrl: string,
871
+ options?: { includeEditorBridge?: boolean },
872
+ ): string {
869
873
  const endpoint = new URL("/live-edit", bridgeUrl);
870
874
  endpoint.searchParams.set("url", previewUrl);
875
+ if (options?.includeEditorBridge === false) {
876
+ endpoint.searchParams.set("bridge", "0");
877
+ }
871
878
  return endpoint.toString();
872
879
  }
873
880
 
@@ -981,9 +988,38 @@ export function getEmbeddedIframeBackgroundColor(args: {
981
988
  : (args.embeddedFrameBackground ?? "transparent");
982
989
  }
983
990
 
991
+ /**
992
+ * CSS rule that shifts embedded-frame (board) content so board-coordinate
993
+ * (0,0) lands at the center of the board iframe's oversized surface.
994
+ *
995
+ * The selector is deliberately scoped to DIRECT `<body>` children only.
996
+ * `translate` composes per element, so a blanket
997
+ * `[data-agent-native-node-id]{translate:…}` rule shifted every NESTED
998
+ * node-id-bearing descendant by the surface offset AGAIN (+65536px per
999
+ * nesting level) — any board child nested inside another board primitive
1000
+ * rendered tens of thousands of px outside its parent (i.e. visually
1001
+ * vanished) even when its persisted parent-relative left/top were perfectly
1002
+ * correct, and every rect-based coordinate computation on nested board
1003
+ * content was poisoned by the extra offset.
1004
+ *
1005
+ * Scoping (rather than wrapping content in a single offset container) is the
1006
+ * robust fix here because board elements are DOCUMENTED direct `<body>`
1007
+ * children (see shared/board-file.ts module doc + emptyBoardHtml): every
1008
+ * creation path appends at body level (appendCanvasPrimitiveToHtml,
1009
+ * boardObjectEntryToHtmlFragment, moveNodeBetweenDocuments' body-append),
1010
+ * and several consumers key on that structure with `body > …` selectors
1011
+ * (BOARD_SURFACE_RENDER_STYLE) and depth-1 walks
1012
+ * (backfillBoardPrimitiveMarkers, getBoardContentLayerSignature). A wrapper
1013
+ * element would silently break all of those structural contracts.
1014
+ *
1015
+ * With this rule, the offset applies exactly once — to top-level board
1016
+ * elements — and nested children position purely via parent-relative
1017
+ * left/top inside their (absolutely positioned) parent, like any normal
1018
+ * CSS containing-block hierarchy.
1019
+ */
984
1020
  function embeddedContentOffsetStyle(x: number, y: number): string {
985
1021
  if (x === 0 && y === 0) return "";
986
- return `<style data-agent-native-content-offset>[data-agent-native-node-id]{translate:${Math.round(x)}px ${Math.round(y)}px;}</style>`;
1022
+ return `<style data-agent-native-content-offset>body > [data-agent-native-node-id]{translate:${Math.round(x)}px ${Math.round(y)}px;}</style>`;
987
1023
  }
988
1024
 
989
1025
  function injectEmbeddedFrameStyle(content: string, style: string): string {
@@ -1039,6 +1075,60 @@ export interface IframeContextMenuPayload {
1039
1075
  info?: ElementInfo | null;
1040
1076
  }
1041
1077
 
1078
+ /**
1079
+ * Creation-race keystroke routing: after the host calls `beginTextEdit(nodeId)`
1080
+ * for a JUST-created text element, there is a window (persist round-trip +
1081
+ * bridge activation) where the browser focus still sits on the HOST document.
1082
+ * Keystrokes typed in that window used to fall into host keyboard shortcuts —
1083
+ * letters switched tools, digits zoomed, and Delete/Backspace DELETED the
1084
+ * selected layer/screen — while the characters themselves were silently lost.
1085
+ *
1086
+ * While a text edit is pending, every host keydown is routed through this
1087
+ * pure helper:
1088
+ * - printable characters are BUFFERED (replayed into the editable once the
1089
+ * bridge reports the session active) and swallowed,
1090
+ * - Backspace edits the buffer, Delete/Enter/Tab/arrows are swallowed so they
1091
+ * can never reach host layer-deletion/navigation,
1092
+ * - Escape clears the pending buffer (user aborted) and is swallowed,
1093
+ * - IME composition and Cmd/Ctrl chords (undo!) pass through untouched.
1094
+ *
1095
+ * Exported for unit tests (DesignCanvas.pending-text-edit.test.ts).
1096
+ */
1097
+ export type PendingTextEditKeyAction =
1098
+ | { action: "buffer"; char: string }
1099
+ | { action: "drop-last" }
1100
+ | { action: "swallow" }
1101
+ | { action: "clear-and-swallow" }
1102
+ | { action: "pass" };
1103
+
1104
+ export function routePendingTextEditKey(e: {
1105
+ key: string;
1106
+ metaKey?: boolean;
1107
+ ctrlKey?: boolean;
1108
+ altKey?: boolean;
1109
+ isComposing?: boolean;
1110
+ }): PendingTextEditKeyAction {
1111
+ if (e.isComposing) return { action: "pass" };
1112
+ if (e.metaKey || e.ctrlKey) return { action: "pass" };
1113
+ if (e.key === "Escape") return { action: "clear-and-swallow" };
1114
+ if (e.key === "Backspace") return { action: "drop-last" };
1115
+ if (
1116
+ e.key === "Delete" ||
1117
+ e.key === "Enter" ||
1118
+ e.key === "Tab" ||
1119
+ e.key.startsWith("Arrow")
1120
+ ) {
1121
+ return { action: "swallow" };
1122
+ }
1123
+ if (e.key.length === 1) return { action: "buffer", char: e.key };
1124
+ return { action: "pass" };
1125
+ }
1126
+
1127
+ /** How long a pending text edit may wait for bridge activation before the
1128
+ * keystroke routing above stands down (matches the bridge's own ~2s
1129
+ * begin-text-edit retry window, plus round-trip slack). */
1130
+ export const PENDING_TEXT_EDIT_TIMEOUT_MS = 3000;
1131
+
1042
1132
  export function DesignCanvas({
1043
1133
  content,
1044
1134
  contentKey,
@@ -1268,19 +1358,28 @@ export function DesignCanvas({
1268
1358
  () => contentHash(liveEditBridgeScript),
1269
1359
  [liveEditBridgeScript],
1270
1360
  );
1271
- const usesLiveEditExternalFrame =
1361
+ const hasLiveEditExternalFrame =
1362
+ sourceType === "localhost" && Boolean(bridgeUrl && rawExternalPreviewUrl);
1363
+ const usesLiveEditEditorBridge =
1272
1364
  sourceType === "localhost" &&
1273
1365
  Boolean(bridgeUrl && rawExternalPreviewUrl) &&
1274
1366
  !interactMode &&
1275
1367
  !readOnly;
1276
1368
  const liveEditBridgeRegistered =
1277
- usesLiveEditExternalFrame &&
1369
+ usesLiveEditEditorBridge &&
1278
1370
  registeredLiveEditBridgeKey === liveEditBridgeKey;
1279
1371
  const requiresEditableExternalSnapshot = false;
1280
1372
  const liveEditExternalPreviewUrl =
1281
- liveEditBridgeRegistered && bridgeUrl && rawExternalPreviewUrl
1282
- ? liveEditEndpointUrl(bridgeUrl, rawExternalPreviewUrl)
1283
- : null;
1373
+ hasLiveEditExternalFrame &&
1374
+ bridgeUrl &&
1375
+ rawExternalPreviewUrl &&
1376
+ interactMode
1377
+ ? liveEditEndpointUrl(bridgeUrl, rawExternalPreviewUrl, {
1378
+ includeEditorBridge: false,
1379
+ })
1380
+ : liveEditBridgeRegistered && bridgeUrl && rawExternalPreviewUrl
1381
+ ? liveEditEndpointUrl(bridgeUrl, rawExternalPreviewUrl)
1382
+ : null;
1284
1383
  const iframeRenderContent =
1285
1384
  activeExternalSnapshotHtml ??
1286
1385
  (requiresEditableExternalSnapshot ? "" : renderedContent);
@@ -1292,7 +1391,7 @@ export function DesignCanvas({
1292
1391
  const waitingForEditableExternalSnapshot =
1293
1392
  requiresEditableExternalSnapshot && !activeExternalSnapshotHtml;
1294
1393
  const waitingForLiveEditBridge =
1295
- usesLiveEditExternalFrame && !liveEditBridgeRegistered;
1394
+ usesLiveEditEditorBridge && !liveEditBridgeRegistered;
1296
1395
  zoomRef.current = zoom;
1297
1396
  runtimeReplacementContentRef.current = runtimeReplacementContent;
1298
1397
  runtimeReplacementKeyRef.current = runtimeReplacementKey;
@@ -1302,7 +1401,7 @@ export function DesignCanvas({
1302
1401
  }, [onExternalContentSnapshot]);
1303
1402
 
1304
1403
  useEffect(() => {
1305
- if (!usesLiveEditExternalFrame || !bridgeUrl) {
1404
+ if (!usesLiveEditEditorBridge || !bridgeUrl) {
1306
1405
  setRegisteredLiveEditBridgeKey(null);
1307
1406
  return;
1308
1407
  }
@@ -1345,7 +1444,7 @@ export function DesignCanvas({
1345
1444
  bridgeToken,
1346
1445
  liveEditBridgeKey,
1347
1446
  liveEditBridgeScript,
1348
- usesLiveEditExternalFrame,
1447
+ usesLiveEditEditorBridge,
1349
1448
  ]);
1350
1449
 
1351
1450
  useEffect(() => {
@@ -1884,7 +1983,44 @@ export function DesignCanvas({
1884
1983
  }
1885
1984
  return;
1886
1985
  }
1986
+ if (e.data.type === "text-edit-pending") {
1987
+ // The bridge is waiting for a just-created node before it can enter
1988
+ // text-edit mode (begin-text-edit arrived first). Arm the host-side
1989
+ // keystroke buffer for the window — repeated pending:true re-posts
1990
+ // for the same node (DesignEditor's T6 retry loop) only extend the
1991
+ // wait, never reset an in-progress buffer.
1992
+ const pendingNodeId =
1993
+ typeof e.data.nodeId === "string" ? e.data.nodeId : "";
1994
+ const currentPending = pendingTextEditRef.current;
1995
+ if (e.data.pending && pendingNodeId) {
1996
+ if (currentPending && currentPending.nodeId === pendingNodeId) {
1997
+ currentPending.startedAt = Date.now();
1998
+ } else {
1999
+ pendingTextEditRef.current = {
2000
+ nodeId: pendingNodeId,
2001
+ buffer: "",
2002
+ startedAt: Date.now(),
2003
+ };
2004
+ }
2005
+ } else if (currentPending?.nodeId === pendingNodeId) {
2006
+ pendingTextEditRef.current = null;
2007
+ }
2008
+ return;
2009
+ }
1887
2010
  if (e.data.type === "text-editing-state") {
2011
+ // Creation-race replay: the bridge just armed the session for the
2012
+ // node we were waiting on — flush any keystrokes the host buffered
2013
+ // during the round-trip window into the now-live editable.
2014
+ if (e.data.active && pendingTextEditRef.current) {
2015
+ const pendingBuffer = pendingTextEditRef.current.buffer;
2016
+ pendingTextEditRef.current = null;
2017
+ if (pendingBuffer) {
2018
+ postOneShotBridgeMessage({
2019
+ type: "text-edit-insert-text",
2020
+ text: pendingBuffer,
2021
+ });
2022
+ }
2023
+ }
1888
2024
  onTextEditingStateChange?.({
1889
2025
  active: Boolean(e.data.active),
1890
2026
  selector:
@@ -2075,6 +2211,7 @@ export function DesignCanvas({
2075
2211
  bridgeUrl,
2076
2212
  fusionUrl,
2077
2213
  flushPendingOneShotMessages,
2214
+ postOneShotBridgeMessage,
2078
2215
  ]);
2079
2216
 
2080
2217
  const replayIframeEditorState = useCallback(() => {
@@ -2374,6 +2511,18 @@ export function DesignCanvas({
2374
2511
  (nodeId: string) => {
2375
2512
  const iframe = iframeRef.current;
2376
2513
  if (!iframe?.contentWindow || !nodeId) return;
2514
+ // Arm the creation-race keystroke buffer (see routePendingTextEditKey):
2515
+ // until the bridge reports text-editing-state active for this command,
2516
+ // host keystrokes are buffered/swallowed instead of hitting host
2517
+ // shortcuts, then replayed into the editable. Re-arming for the same
2518
+ // node preserves an in-progress buffer.
2519
+ if (pendingTextEditRef.current?.nodeId !== nodeId) {
2520
+ pendingTextEditRef.current = {
2521
+ nodeId,
2522
+ buffer: "",
2523
+ startedAt: Date.now(),
2524
+ };
2525
+ }
2377
2526
  postOneShotBridgeMessage({
2378
2527
  type: "begin-text-edit",
2379
2528
  nodeId,
@@ -2383,6 +2532,53 @@ export function DesignCanvas({
2383
2532
  [postOneShotBridgeMessage],
2384
2533
  );
2385
2534
 
2535
+ // Creation-race keystroke routing (host side). Active only while a
2536
+ // beginTextEdit() command is pending; see routePendingTextEditKey's doc
2537
+ // comment for the exact policy. Capture-phase on window so it runs before
2538
+ // the editor's own shortcut handlers.
2539
+ const pendingTextEditRef = useRef<{
2540
+ nodeId: string;
2541
+ buffer: string;
2542
+ startedAt: number;
2543
+ } | null>(null);
2544
+ useEffect(() => {
2545
+ function onPendingTextEditKeyDown(e: KeyboardEvent) {
2546
+ const pending = pendingTextEditRef.current;
2547
+ if (!pending) return;
2548
+ if (Date.now() - pending.startedAt > PENDING_TEXT_EDIT_TIMEOUT_MS) {
2549
+ pendingTextEditRef.current = null;
2550
+ return;
2551
+ }
2552
+ const routed = routePendingTextEditKey(e);
2553
+ if (routed.action === "pass") return;
2554
+ e.preventDefault();
2555
+ e.stopPropagation();
2556
+ e.stopImmediatePropagation();
2557
+ if (routed.action === "buffer") {
2558
+ pending.buffer += routed.char;
2559
+ } else if (routed.action === "drop-last") {
2560
+ pending.buffer = pending.buffer.slice(0, -1);
2561
+ } else if (routed.action === "clear-and-swallow") {
2562
+ pendingTextEditRef.current = null;
2563
+ }
2564
+ }
2565
+ function onPendingTextEditPointerDown() {
2566
+ // The user clicked somewhere else in the host — stand down so buffered
2567
+ // keys are never replayed into an edit they've abandoned.
2568
+ pendingTextEditRef.current = null;
2569
+ }
2570
+ window.addEventListener("keydown", onPendingTextEditKeyDown, true);
2571
+ window.addEventListener("pointerdown", onPendingTextEditPointerDown, true);
2572
+ return () => {
2573
+ window.removeEventListener("keydown", onPendingTextEditKeyDown, true);
2574
+ window.removeEventListener(
2575
+ "pointerdown",
2576
+ onPendingTextEditPointerDown,
2577
+ true,
2578
+ );
2579
+ };
2580
+ }, []);
2581
+
2386
2582
  const sendStyleChange = useCallback(
2387
2583
  (
2388
2584
  selector: string,
@@ -3004,7 +3200,7 @@ export function DesignCanvas({
3004
3200
  </div>
3005
3201
  ) : null}
3006
3202
  {waitingForEditableExternalSnapshot || waitingForLiveEditBridge ? (
3007
- <div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center bg-background/85 px-4 text-center text-sm text-muted-foreground">
3203
+ <div className="pointer-events-auto absolute inset-0 z-10 flex items-center justify-center bg-background/85 px-4 text-center text-sm text-muted-foreground">
3008
3204
  {waitingForLiveEditBridge ? (
3009
3205
  <div className="max-w-[28rem] rounded-md border bg-card px-4 py-3 shadow-sm">
3010
3206
  {
@@ -213,6 +213,7 @@ import {
213
213
  parseGradientLayer,
214
214
  removeFillLayerAtIndex,
215
215
  SOLID_FILL_ID,
216
+ solidToGradientPatch,
216
217
  splitCssLayers,
217
218
  withLayerSizeMarker,
218
219
  } from "./edit-panel/fill-gradient-helpers";
@@ -356,11 +357,14 @@ export {
356
357
  truncateOpeningTag,
357
358
  };
358
359
  export {
360
+ averageGradientOpacity,
359
361
  buildGradientLayer,
362
+ defaultGradientStops,
360
363
  isLayerHiddenBySize,
361
364
  joinCssLayers,
362
365
  parseGradientLayer,
363
366
  removeFillLayerAtIndex,
367
+ solidToGradientPatch,
364
368
  splitCssLayers,
365
369
  withLayerSizeMarker,
366
370
  type FillLayerArrays,