@agent-native/core 0.92.7 → 0.92.8

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 (289) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/a2a-protocol.mdx +12 -0
  4. package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
  5. package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
  6. package/corpus/core/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
  7. package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
  8. package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
  9. package/corpus/core/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
  10. package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
  11. package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
  12. package/corpus/core/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
  13. package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
  14. package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
  15. package/corpus/core/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
  16. package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
  17. package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
  18. package/corpus/core/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
  19. package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
  20. package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
  21. package/corpus/core/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
  22. package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
  23. package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
  24. package/corpus/core/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
  25. package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
  26. package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
  27. package/corpus/core/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
  28. package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
  29. package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
  30. package/corpus/core/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
  31. package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
  32. package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
  33. package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
  34. package/corpus/core/docs/content/pr-visual-recap.mdx +3 -3
  35. package/corpus/core/docs/content/pure-agent-apps.mdx +22 -0
  36. package/corpus/core/package.json +1 -1
  37. package/corpus/core/src/agent/engine/builtin.ts +1 -1
  38. package/corpus/core/src/agent/model-config.ts +30 -30
  39. package/corpus/core/src/agent/production-agent.ts +129 -7
  40. package/corpus/core/src/cli/design-connect.ts +438 -83
  41. package/corpus/core/src/cli/recap.ts +2 -2
  42. package/corpus/core/src/client/AgentPanel.tsx +8 -1
  43. package/corpus/core/src/client/agent-chat-adapter.ts +12 -7
  44. package/corpus/core/src/client/composer/TiptapComposer.tsx +1 -1
  45. package/corpus/core/src/client/index.ts +4 -0
  46. package/corpus/core/src/client/org/TeamPage.tsx +41 -26
  47. package/corpus/core/src/client/use-action.ts +113 -1
  48. package/corpus/core/src/db/client.ts +12 -2
  49. package/corpus/core/src/db/create-get-db.ts +4 -1
  50. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +1 -1
  51. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +1 -1
  52. package/corpus/core/src/server/transcribe-voice.ts +1 -1
  53. package/corpus/core/src/usage/store.ts +13 -4
  54. package/corpus/core/src/vite/client.ts +38 -1
  55. package/corpus/templates/analytics/AGENTS.md +3 -0
  56. package/corpus/templates/analytics/actions/get-data-program.ts +78 -0
  57. package/corpus/templates/analytics/actions/list-data-programs.ts +49 -0
  58. package/corpus/templates/analytics/app/global.css +11 -0
  59. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +163 -17
  60. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  61. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  62. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +96 -3
  63. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -1
  64. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +61 -4
  65. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +40 -2
  66. package/corpus/templates/analytics/changelog/2026-07-09-analytics-no-longer-forgets-successful-data-queries-when-a-l.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-keep-consistent-spacing-when-they-stack-ver.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-tables-and-extensions-can-be-selected-and-d.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-07-09-long-running-analytics-requests-now-recover-automatically-wh.md +6 -0
  70. package/corpus/templates/analytics/server/db/index.ts +6 -0
  71. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +1 -7
  72. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +102 -15
  73. package/corpus/templates/content/app/hooks/use-content-database.ts +52 -0
  74. package/corpus/templates/content/app/hooks/use-document-properties.ts +50 -6
  75. package/corpus/templates/content/app/i18n/zh-TW.ts +2 -0
  76. package/corpus/templates/content/app/i18n-data.ts +28 -0
  77. package/corpus/templates/content/changelog/2026-07-09-notion-conflict-warnings-no-longer-flash-during-normal-synce.md +6 -0
  78. package/corpus/templates/content/changelog/2026-07-09-property-menus-and-delete-confirmations-now-open-visibly-and.md +6 -0
  79. package/corpus/templates/content/server/lib/notion-sync.ts +130 -28
  80. package/corpus/templates/design/actions/add-breakpoint.ts +48 -68
  81. package/corpus/templates/design/actions/add-localhost-screens.ts +496 -61
  82. package/corpus/templates/design/actions/apply-component-prop-edit.ts +2 -18
  83. package/corpus/templates/design/actions/apply-design-token-edit.ts +51 -82
  84. package/corpus/templates/design/actions/apply-tweaks.ts +63 -41
  85. package/corpus/templates/design/actions/capture-design-state.ts +2 -2
  86. package/corpus/templates/design/actions/connect-localhost.ts +58 -12
  87. package/corpus/templates/design/actions/create-component.ts +2 -18
  88. package/corpus/templates/design/actions/create-design-branch.ts +38 -18
  89. package/corpus/templates/design/actions/create-fusion-app.ts +15 -11
  90. package/corpus/templates/design/actions/delete-file.ts +19 -34
  91. package/corpus/templates/design/actions/deploy-design-preview.ts +59 -27
  92. package/corpus/templates/design/actions/deploy-fusion-app.ts +22 -15
  93. package/corpus/templates/design/actions/export-pdf.ts +2 -1
  94. package/corpus/templates/design/actions/export-zip.ts +9 -3
  95. package/corpus/templates/design/actions/generate-design.ts +107 -70
  96. package/corpus/templates/design/actions/get-component-details.ts +2 -18
  97. package/corpus/templates/design/actions/get-design-branch-diff.ts +2 -3
  98. package/corpus/templates/design/actions/get-design-surface-index.ts +2 -18
  99. package/corpus/templates/design/actions/get-design.ts +2 -1
  100. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +7 -1
  101. package/corpus/templates/design/actions/import-design-tokens.ts +114 -76
  102. package/corpus/templates/design/actions/index-components.ts +2 -18
  103. package/corpus/templates/design/actions/list-design-source-capabilities.ts +23 -28
  104. package/corpus/templates/design/actions/list-local-files.ts +9 -2
  105. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +82 -98
  106. package/corpus/templates/design/actions/open-component-source.ts +2 -18
  107. package/corpus/templates/design/actions/open-visual-edit.ts +19 -1
  108. package/corpus/templates/design/actions/present-design-variants.ts +70 -70
  109. package/corpus/templates/design/actions/preview-component-prop-edit.ts +4 -14
  110. package/corpus/templates/design/actions/read-local-file.ts +9 -2
  111. package/corpus/templates/design/actions/remove-breakpoint.ts +40 -61
  112. package/corpus/templates/design/actions/rename-screen.ts +425 -0
  113. package/corpus/templates/design/actions/request-localhost-write-consent.ts +12 -2
  114. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +9 -2
  115. package/corpus/templates/design/actions/sync-fusion-app.ts +56 -39
  116. package/corpus/templates/design/actions/update-design.ts +376 -61
  117. package/corpus/templates/design/actions/update-file.ts +455 -188
  118. package/corpus/templates/design/actions/write-local-file.ts +10 -2
  119. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +685 -608
  120. package/corpus/templates/design/app/components/design/LayersPanel.tsx +43 -15
  121. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +576 -2015
  122. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +62 -14
  123. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +263 -25
  124. package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +34 -0
  125. package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +40 -0
  126. package/corpus/templates/design/app/components/design/design-canvas/creation.ts +47 -0
  127. package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +27 -0
  128. package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +62 -0
  129. package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +137 -0
  130. package/corpus/templates/design/app/components/design/design-canvas/file-drop.ts +10 -0
  131. package/corpus/templates/design/app/components/design/design-canvas/hit-test.ts +23 -0
  132. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +23 -0
  133. package/corpus/templates/design/app/components/design/design-canvas/iframe-pan.ts +177 -0
  134. package/corpus/templates/design/app/components/design/design-canvas/motion-types.ts +7 -0
  135. package/corpus/templates/design/app/components/design/design-canvas/pending-text-edit.ts +62 -0
  136. package/corpus/templates/design/app/components/design/index.ts +2 -2
  137. package/corpus/templates/design/app/components/design/multi-screen/canvas-tools.ts +100 -0
  138. package/corpus/templates/design/app/components/design/multi-screen/coordinate-transforms.ts +81 -0
  139. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +23 -13
  140. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +238 -19
  141. package/corpus/templates/design/app/components/design/multi-screen/draft-primitives.ts +424 -0
  142. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +341 -0
  143. package/corpus/templates/design/app/components/design/multi-screen/iframe-targeting.ts +52 -0
  144. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +62 -0
  145. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +283 -0
  146. package/corpus/templates/design/app/components/design/multi-screen/screen-content-cache.ts +272 -0
  147. package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -1
  148. package/corpus/templates/design/app/components/design/multi-screen/wheel-gesture-state.ts +12 -0
  149. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +325 -84
  150. package/corpus/templates/design/app/hooks/use-navigation-state.ts +15 -1
  151. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +119 -47
  152. package/corpus/templates/design/app/i18n/zh-TW.ts +9 -0
  153. package/corpus/templates/design/app/i18n-data.ts +223 -0
  154. package/corpus/templates/design/app/lib/design-save-outbox.ts +379 -0
  155. package/corpus/templates/design/app/lib/png-clipboard.ts +113 -0
  156. package/corpus/templates/design/app/pages/design-editor/canvas-primitives.ts +229 -0
  157. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +918 -0
  158. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +43 -0
  159. package/corpus/templates/design/app/pages/design-editor/cross-screen-text-color.ts +336 -0
  160. package/corpus/templates/design/app/pages/design-editor/data-operations.ts +270 -0
  161. package/corpus/templates/design/app/pages/design-editor/dom-guards.ts +24 -0
  162. package/corpus/templates/design/app/pages/design-editor/dom-utils.ts +11 -0
  163. package/corpus/templates/design/app/pages/design-editor/editor-session.ts +20 -0
  164. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +408 -0
  165. package/corpus/templates/design/app/pages/design-editor/export-capture.ts +115 -0
  166. package/corpus/templates/design/app/pages/design-editor/geometry-persistence.ts +93 -0
  167. package/corpus/templates/design/app/pages/design-editor/history.ts +169 -0
  168. package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +185 -0
  169. package/corpus/templates/design/app/pages/design-editor/motion-state.ts +309 -0
  170. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +438 -0
  171. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +510 -0
  172. package/corpus/templates/design/app/pages/design-editor/portable-style.ts +115 -0
  173. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +337 -0
  174. package/corpus/templates/design/app/pages/design-editor/style-utils.ts +5 -0
  175. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +92 -0
  176. package/corpus/templates/design/app/pages/design-editor/tweak-save.ts +87 -0
  177. package/corpus/templates/design/app/pages/design-editor/types.ts +36 -0
  178. package/corpus/templates/design/app/routes/design.$id.tsx +11 -1
  179. package/corpus/templates/design/changelog/2026-07-09-design-edits-canvas-positions-tools-and-tab-context-now-pers.md +6 -0
  180. package/corpus/templates/design/changelog/2026-07-09-figma-style-shortcuts-drawing-annotations-and-cross-screen-d.md +6 -0
  181. package/corpus/templates/design/changelog/2026-07-09-hand-and-scale-tools-now-keep-their-active-toolbar-identity-.md +6 -0
  182. package/corpus/templates/design/changelog/2026-07-09-large-multi-screen-designs-now-stay-responsive-during-long-c.md +6 -0
  183. package/corpus/templates/design/changelog/2026-07-09-layer-drags-now-land-exactly-where-the-insertion-indicator-s.md +6 -0
  184. package/corpus/templates/design/server/db/index.ts +2 -52
  185. package/corpus/templates/design/server/db/schema.ts +16 -0
  186. package/corpus/templates/design/server/lib/design-data-access.ts +70 -0
  187. package/corpus/templates/design/server/lib/design-data-mutation.ts +249 -0
  188. package/corpus/templates/design/server/lib/fusion-screens.ts +89 -76
  189. package/corpus/templates/design/server/lib/import-design-files.ts +76 -35
  190. package/corpus/templates/design/server/lib/verify-write-grant.ts +7 -2
  191. package/corpus/templates/design/server/plugins/db.ts +19 -0
  192. package/corpus/templates/design/server/source-workspace.ts +12 -13
  193. package/corpus/templates/design/shared/resolve-tweaks.ts +15 -0
  194. package/corpus/templates/design/shared/screen-rename.ts +42 -0
  195. package/corpus/templates/design/shared/source-mode.ts +84 -4
  196. package/dist/agent/engine/builder-engine.d.ts +1 -1
  197. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  198. package/dist/agent/engine/builtin.js +1 -1
  199. package/dist/agent/engine/builtin.js.map +1 -1
  200. package/dist/agent/model-config.d.ts +9 -9
  201. package/dist/agent/model-config.d.ts.map +1 -1
  202. package/dist/agent/model-config.js +30 -30
  203. package/dist/agent/model-config.js.map +1 -1
  204. package/dist/agent/production-agent.d.ts +60 -1
  205. package/dist/agent/production-agent.d.ts.map +1 -1
  206. package/dist/agent/production-agent.js +106 -7
  207. package/dist/agent/production-agent.js.map +1 -1
  208. package/dist/cli/design-connect.d.ts +30 -1
  209. package/dist/cli/design-connect.d.ts.map +1 -1
  210. package/dist/cli/design-connect.js +359 -75
  211. package/dist/cli/design-connect.js.map +1 -1
  212. package/dist/cli/recap.js +2 -2
  213. package/dist/cli/recap.js.map +1 -1
  214. package/dist/client/AgentPanel.d.ts.map +1 -1
  215. package/dist/client/AgentPanel.js +6 -1
  216. package/dist/client/AgentPanel.js.map +1 -1
  217. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  218. package/dist/client/agent-chat-adapter.js +10 -5
  219. package/dist/client/agent-chat-adapter.js.map +1 -1
  220. package/dist/client/composer/TiptapComposer.js +1 -1
  221. package/dist/client/composer/TiptapComposer.js.map +1 -1
  222. package/dist/client/index.d.ts +1 -1
  223. package/dist/client/index.d.ts.map +1 -1
  224. package/dist/client/index.js +1 -1
  225. package/dist/client/index.js.map +1 -1
  226. package/dist/client/org/TeamPage.d.ts.map +1 -1
  227. package/dist/client/org/TeamPage.js +6 -5
  228. package/dist/client/org/TeamPage.js.map +1 -1
  229. package/dist/client/use-action.d.ts +32 -0
  230. package/dist/client/use-action.d.ts.map +1 -1
  231. package/dist/client/use-action.js +68 -1
  232. package/dist/client/use-action.js.map +1 -1
  233. package/dist/collab/struct-routes.d.ts +1 -1
  234. package/dist/db/client.d.ts +3 -0
  235. package/dist/db/client.d.ts.map +1 -1
  236. package/dist/db/client.js +7 -2
  237. package/dist/db/client.js.map +1 -1
  238. package/dist/db/create-get-db.d.ts.map +1 -1
  239. package/dist/db/create-get-db.js +2 -2
  240. package/dist/db/create-get-db.js.map +1 -1
  241. package/dist/notifications/routes.d.ts +3 -3
  242. package/dist/resources/handlers.d.ts +2 -2
  243. package/dist/scripts/agent-engines/manage-agent-engine.js +1 -1
  244. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  245. package/dist/scripts/agent-engines/set-agent-engine.js +1 -1
  246. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  247. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  248. package/dist/server/transcribe-voice.js +1 -1
  249. package/dist/server/transcribe-voice.js.map +1 -1
  250. package/dist/usage/store.d.ts.map +1 -1
  251. package/dist/usage/store.js +13 -4
  252. package/dist/usage/store.js.map +1 -1
  253. package/dist/vite/client.d.ts.map +1 -1
  254. package/dist/vite/client.js +36 -1
  255. package/dist/vite/client.js.map +1 -1
  256. package/docs/content/a2a-protocol.mdx +12 -0
  257. package/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
  258. package/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
  259. package/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
  260. package/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
  261. package/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
  262. package/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
  263. package/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
  264. package/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
  265. package/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
  266. package/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
  267. package/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
  268. package/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
  269. package/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
  270. package/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
  271. package/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
  272. package/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
  273. package/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
  274. package/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
  275. package/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
  276. package/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
  277. package/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
  278. package/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
  279. package/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
  280. package/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
  281. package/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
  282. package/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
  283. package/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
  284. package/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
  285. package/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
  286. package/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
  287. package/docs/content/pr-visual-recap.mdx +3 -3
  288. package/docs/content/pure-agent-apps.mdx +22 -0
  289. package/package.json +2 -2
@@ -0,0 +1,43 @@
1
+ import { shouldUseLiveFileContent } from "@shared/html-content";
2
+
3
+ export function shouldRebaseCollabDocFromStoredContent({
4
+ liveContent,
5
+ storedContent,
6
+ storedUpdatedAt,
7
+ lastAppliedUpdatedAt,
8
+ fileType,
9
+ }: {
10
+ liveContent: string;
11
+ storedContent: string;
12
+ storedUpdatedAt: string | null | undefined;
13
+ lastAppliedUpdatedAt: string | null;
14
+ fileType: string;
15
+ }): boolean {
16
+ if (liveContent === storedContent) return false;
17
+ if (
18
+ !shouldUseLiveFileContent({
19
+ liveContent,
20
+ storedContent,
21
+ fileType,
22
+ })
23
+ ) {
24
+ return true;
25
+ }
26
+ if (fileType.toLowerCase() !== "html") return false;
27
+ if (!lastAppliedUpdatedAt) return !!storedUpdatedAt;
28
+ return false;
29
+ }
30
+
31
+ export function resolveScreenCollabSyncTarget({
32
+ fileId,
33
+ overviewPresenceFileId,
34
+ overviewDocConnected,
35
+ }: {
36
+ fileId: string;
37
+ overviewPresenceFileId: string | null;
38
+ overviewDocConnected: boolean;
39
+ }): { writeLiveDoc: boolean; syncCollab: boolean } {
40
+ const writeLiveDoc =
41
+ overviewDocConnected && overviewPresenceFileId === fileId;
42
+ return { writeLiveDoc, syncCollab: !writeLiveDoc };
43
+ }
@@ -0,0 +1,336 @@
1
+ import { parseCssColorExtended } from "@shared/color-utils";
2
+
3
+ export const BOARD_TEXT_AUTO_COLOR_MARKER = "data-an-auto-text-color";
4
+
5
+ // Relative luminance threshold mirroring containerBackgroundIsLight in
6
+ // editor-chrome.bridge.ts (keep both in sync) — used by the pure decision
7
+ // helper below so its threshold can't drift from the in-screen bridge path.
8
+ const AUTO_TEXT_COLOR_LIGHT_LUMINANCE_THRESHOLD = 150;
9
+
10
+ function relativeLuminance(rgb: { r: number; g: number; b: number }): number {
11
+ return 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
12
+ }
13
+
14
+ // Tolerant "is this the board's auto-applied default white" check, shared
15
+ // by shouldAdaptAutoTextColorForCrossScreenMove and
16
+ // isStaleAutoTextColorMarker (finding 2) — both need the exact same
17
+ // definition of "still the auto-default", just applied to different
18
+ // questions (safe to adapt vs. safe to keep trusting the marker).
19
+ function isAutoDefaultWhiteColor(
20
+ inlineColor: string | null | undefined,
21
+ ): boolean {
22
+ const normalized = (inlineColor || "")
23
+ .trim()
24
+ .toLowerCase()
25
+ .replace(/\s+/g, " ");
26
+ return (
27
+ normalized === "#ffffff" ||
28
+ normalized === "#fff" ||
29
+ normalized === "rgb(255,255,255)" ||
30
+ normalized === "rgb(255, 255, 255)" ||
31
+ normalized === "white"
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Finding 2(a): BOARD_TEXT_AUTO_COLOR_MARKER is stamped once at board-text
37
+ * creation time and never removed by the bridge, so it goes stale the
38
+ * moment a user explicitly recolors previously-auto-white text (e.g. picks
39
+ * a brand color) without the marker being cleared in the same edit. Trusting
40
+ * a stale marker unconditionally (the previous behavior) forced that
41
+ * deliberately-chosen color back to `inherit` on the next reparent/
42
+ * cross-screen move. A marker is only trustworthy evidence of "this color is
43
+ * still auto-applied" when the node's CURRENT inline color is still exactly
44
+ * the auto-default white — any other color means the user (or some other
45
+ * edit) changed it after the marker was stamped, so the marker is stale and
46
+ * must not be honored (callers should also strip it — see
47
+ * stripStaleAutoTextColorMarkerFromHtml below).
48
+ */
49
+ export function isStaleAutoTextColorMarker(params: {
50
+ inlineColor: string | null | undefined;
51
+ hasAutoMarker: boolean;
52
+ }): boolean {
53
+ return params.hasAutoMarker && !isAutoDefaultWhiteColor(params.inlineColor);
54
+ }
55
+
56
+ /**
57
+ * Pure decision: should a moved text node's auto-applied board color be
58
+ * rewritten to `inherit` once it lands in its cross-screen destination?
59
+ *
60
+ * Mirrors adaptAutoTextColorForNest's decision in editor-chrome.bridge.ts
61
+ * (keep both in sync), adapted to the host's HTML-string world instead of a
62
+ * live DOM re-parent check — the cross-screen drop path
63
+ * (handleCrossScreenElementDrop) always represents an actual re-parent (the
64
+ * node moves from one document's body into another), so there is no
65
+ * same-parent short-circuit to mirror here.
66
+ *
67
+ * - `hasAutoMarker` AND the color is still the auto-default white: always
68
+ * safe to adapt regardless of the destination background — the color is
69
+ * definitely still auto-applied, not user-set (finding 2: a marker whose
70
+ * color has since diverged from white is STALE and must not short-circuit
71
+ * here — falls through to the same conservative heuristic as no marker).
72
+ * - No marker (pre-marker content, a node the stamp missed, or a stale
73
+ * marker whose color moved off white): fall back to the conservative
74
+ * default-white + light-destination heuristic so a deliberately-chosen
75
+ * color is never touched.
76
+ */
77
+ export function shouldAdaptAutoTextColorForCrossScreenMove(params: {
78
+ inlineColor: string | null | undefined;
79
+ hasAutoMarker: boolean;
80
+ destinationBackgroundIsLight: boolean;
81
+ }): boolean {
82
+ const { inlineColor, hasAutoMarker, destinationBackgroundIsLight } = params;
83
+ const normalized = (inlineColor || "").trim().toLowerCase();
84
+ if (
85
+ !normalized ||
86
+ normalized === "inherit" ||
87
+ normalized === "currentcolor"
88
+ ) {
89
+ return false;
90
+ }
91
+ const isDefaultWhite = isAutoDefaultWhiteColor(inlineColor);
92
+ if (hasAutoMarker && isDefaultWhite) return true;
93
+ if (!isDefaultWhite) return false;
94
+ return destinationBackgroundIsLight;
95
+ }
96
+
97
+ /**
98
+ * Pure decision for finding 1: given an ordered chain of background signals
99
+ * read from an element's ancestor chain (innermost first — same walk order
100
+ * `collectDestinationBackgroundSignals` below produces), decide whether the
101
+ * destination background is light.
102
+ *
103
+ * Each entry is either a CSS color string (from an inline `background`/
104
+ * `background-color` declaration, or a live `getComputedStyle().
105
+ * backgroundColor` read when a live document is available — see call site)
106
+ * or a `{ darkClassHint: true }` marker for a cheap utility-class signal
107
+ * (e.g. `bg-black`, `bg-gray-900`, `dark:bg-*`) when no inline/computed
108
+ * color was found on that same element. The first entry that resolves to a
109
+ * non-transparent (alpha >= 0.4) color wins; alpha below that threshold is
110
+ * treated as transparent so the walk keeps climbing instead of trusting a
111
+ * near-invisible tint. A dark-class hint only counts when no color signal
112
+ * was present on that element (a real color always wins over a guessed
113
+ * class). No signal anywhere in the chain conservatively reports "light" so
114
+ * the default-white heuristic can still fire and prevent invisible text.
115
+ */
116
+ export function resolveDestinationBackgroundLightness(
117
+ chain: ReadonlyArray<{ color: string | null } | { darkClassHint: boolean }>,
118
+ ): boolean {
119
+ for (const entry of chain) {
120
+ if ("color" in entry && entry.color) {
121
+ const rgba = parseCssColorExtended(entry.color);
122
+ if (rgba && rgba.a >= 0.4) {
123
+ return (
124
+ relativeLuminance(rgba) > AUTO_TEXT_COLOR_LIGHT_LUMINANCE_THRESHOLD
125
+ );
126
+ }
127
+ continue;
128
+ }
129
+ if ("darkClassHint" in entry && entry.darkClassHint) {
130
+ return false;
131
+ }
132
+ }
133
+ return true;
134
+ }
135
+
136
+ // Cheap, best-effort utility-class signal for "this element's classes look
137
+ // like a dark background" — used only when an element carries no inline or
138
+ // computed background color at all (a real color signal always wins). Not
139
+ // an attempt to parse Tailwind's full config: just the small set of
140
+ // class-name shapes generated designs actually use for dark surfaces
141
+ // (`bg-black`, `bg-gray-900`, `bg-neutral-950`, `dark:bg-slate-900`, etc.).
142
+ const DARK_BACKGROUND_CLASS_RE =
143
+ /(?:^|:)bg-(?:black|(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:800|900|950))\b/;
144
+
145
+ function elementHasDarkBackgroundClassHint(element: Element): boolean {
146
+ const className =
147
+ typeof element.className === "string"
148
+ ? element.className
149
+ : (element.getAttribute("class") ?? "");
150
+ return DARK_BACKGROUND_CLASS_RE.test(className);
151
+ }
152
+
153
+ /**
154
+ * Builds the ancestor background-signal chain `resolveDestinationBackgroundLightness`
155
+ * consumes, walking up from `element`. Prefers a LIVE document's computed
156
+ * style (real cascade, resolves stylesheet/utility classes correctly —
157
+ * mirrors the bridge's `containerBackgroundIsLight`) when `liveElement` is
158
+ * supplied (see `destinationBackgroundIsLightForNode`'s doc for how the
159
+ * live node is resolved); otherwise falls back to reading INLINE
160
+ * `background`/`background-color` declarations plus the dark-class-name
161
+ * heuristic on the detached parsed-doc element, since a DOMParser document
162
+ * has no `defaultView` in real browsers and can't run `getComputedStyle`.
163
+ */
164
+ function collectDestinationBackgroundSignals(
165
+ element: Element,
166
+ liveElement: Element | null,
167
+ ): Array<{ color: string | null } | { darkClassHint: boolean }> {
168
+ const chain: Array<{ color: string | null } | { darkClassHint: boolean }> =
169
+ [];
170
+ if (liveElement) {
171
+ const liveView = liveElement.ownerDocument?.defaultView;
172
+ let cursor: Element | null = liveElement;
173
+ while (
174
+ liveView &&
175
+ cursor &&
176
+ cursor !== liveElement.ownerDocument.documentElement
177
+ ) {
178
+ chain.push({ color: liveView.getComputedStyle(cursor).backgroundColor });
179
+ cursor = cursor.parentElement;
180
+ }
181
+ return chain;
182
+ }
183
+ let cursor: Element | null = element;
184
+ while (cursor && cursor !== element.ownerDocument.documentElement) {
185
+ const inline = (cursor as HTMLElement).style;
186
+ const inlineColor = inline?.backgroundColor || inline?.background || null;
187
+ if (inlineColor) {
188
+ chain.push({ color: inlineColor });
189
+ } else {
190
+ chain.push({ darkClassHint: elementHasDarkBackgroundClassHint(cursor) });
191
+ }
192
+ cursor = cursor.parentElement;
193
+ }
194
+ return chain;
195
+ }
196
+
197
+ /**
198
+ * Resolves whether the destination background around `element` (a node in a
199
+ * DOMParser-parsed detached document) is light. Prefers reading the LIVE
200
+ * destination screen iframe's computed style for the corresponding node
201
+ * when `liveDoc` is supplied — MultiScreenCanvas mounts every visible
202
+ * screen as a same-origin iframe reachable via
203
+ * `[data-screen-iframe-id="<screenId>"]` (see `getPrimaryIframeId` in
204
+ * MultiScreenCanvas.tsx), so the real cascade (including Tailwind utility
205
+ * classes, `<style>` rules, `dark:` variants, etc.) is available exactly
206
+ * like the in-iframe bridge's `containerBackgroundIsLight` — that live path
207
+ * is what makes class-based dark destinations (not just inline-styled ones)
208
+ * resolve correctly. Falls back to the detached doc's inline-style +
209
+ * dark-class-hint chain when no live document is available (e.g. the
210
+ * destination screen isn't currently mounted).
211
+ */
212
+ function destinationBackgroundIsLightForNode(
213
+ element: Element,
214
+ liveDoc?: Document | null,
215
+ ): boolean {
216
+ try {
217
+ const liveElement =
218
+ liveDoc && element.hasAttribute("data-agent-native-node-id")
219
+ ? liveDoc.querySelector(
220
+ `[data-agent-native-node-id="${CSS.escape(
221
+ element.getAttribute("data-agent-native-node-id") ?? "",
222
+ )}"]`,
223
+ )
224
+ : null;
225
+ const chain = collectDestinationBackgroundSignals(element, liveElement);
226
+ return resolveDestinationBackgroundLightness(chain);
227
+ } catch {
228
+ return true;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Cross-screen counterpart to adaptAutoTextColorForNest — runs HOST-SIDE
234
+ * after moveNodeBetweenDocuments has already re-parented the text node into
235
+ * `destContent`'s DOM (identified by `destNodeAttrId`). Board-drawn text
236
+ * carries an explicit inline `color:#ffffff` default (see
237
+ * defaultCanvasTextColor / appendCanvasPrimitiveToHtml) because
238
+ * `currentColor` would inherit black on the always-dark board; dropped into
239
+ * a light destination screen/container, that stale inline white is
240
+ * invisible white-on-white. The in-screen drag path already handles this
241
+ * via the bridge's adaptAutoTextColorForNest; this is the missing
242
+ * cross-screen mirror (finding 8).
243
+ *
244
+ * No-op (returns `content` unchanged) when the moved node isn't a text
245
+ * primitive, carries no color needing adaptation, or the DOM can't be
246
+ * parsed — always best-effort, never a hard requirement for the move to
247
+ * succeed.
248
+ *
249
+ * `liveDestDoc` (optional) is the destination screen's LIVE iframe document
250
+ * when it's currently mounted — see `destinationBackgroundIsLightForNode`'s
251
+ * doc comment for why callers should prefer passing it when available
252
+ * (correct resolution for class-based/cascaded dark backgrounds, not just
253
+ * inline-styled ones).
254
+ */
255
+ export function adaptAutoTextColorForCrossScreenNode(
256
+ content: string,
257
+ destNodeAttrId: string,
258
+ liveDestDoc?: Document | null,
259
+ ): string {
260
+ if (typeof window === "undefined" || !destNodeAttrId) return content;
261
+ try {
262
+ const doc = new DOMParser().parseFromString(content, "text/html");
263
+ const moved = doc.querySelector(
264
+ `[data-agent-native-node-id="${CSS.escape(destNodeAttrId)}"]`,
265
+ );
266
+ if (!moved) return content;
267
+ const kind = (
268
+ moved.getAttribute("data-an-primitive") ||
269
+ moved.getAttribute("data-agent-native-primitive") ||
270
+ ""
271
+ ).toLowerCase();
272
+ if (kind !== "text") return content;
273
+ const el = moved as HTMLElement;
274
+ const hasAutoMarker = moved.hasAttribute(BOARD_TEXT_AUTO_COLOR_MARKER);
275
+ // Finding 2(a): a marker whose color has since diverged from the
276
+ // auto-default white is stale — strip it here (in addition to falling
277
+ // through to the conservative heuristic below) so it can't mislead a
278
+ // LATER move/commit into re-honoring it once the color happens to be
279
+ // reset back to white for unrelated reasons.
280
+ let markerStripped = false;
281
+ if (
282
+ isStaleAutoTextColorMarker({
283
+ inlineColor: el.style.color,
284
+ hasAutoMarker,
285
+ })
286
+ ) {
287
+ moved.removeAttribute(BOARD_TEXT_AUTO_COLOR_MARKER);
288
+ markerStripped = true;
289
+ }
290
+ const shouldAdapt = shouldAdaptAutoTextColorForCrossScreenMove({
291
+ inlineColor: el.style.color,
292
+ hasAutoMarker: hasAutoMarker && !markerStripped,
293
+ destinationBackgroundIsLight: destinationBackgroundIsLightForNode(
294
+ moved,
295
+ liveDestDoc,
296
+ ),
297
+ });
298
+ if (!shouldAdapt && !markerStripped) return content;
299
+ if (shouldAdapt) el.style.color = "inherit";
300
+ return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
301
+ } catch {
302
+ return content;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Finding 2(b): the other half of the stale-marker fix. Stripping only
308
+ * happens lazily (finding 2(a), inside adaptAutoTextColorForCrossScreenNode)
309
+ * when a move/adapt pass actually re-reads the node — a user who sets an
310
+ * explicit color and never triggers another move/reparent would otherwise
311
+ * keep carrying a now-stale marker indefinitely. Called from
312
+ * commitVisualStyles whenever a "color" property is part of the committed
313
+ * style patch, so the marker is cleared at the moment the user's explicit
314
+ * choice is actually persisted, not just the next time something happens to
315
+ * re-check it. No-op (returns `content` unchanged) when the node has no
316
+ * marker, isn't found, or the DOM can't be parsed.
317
+ */
318
+ export function clearAutoTextColorMarkerOnExplicitColorCommit(
319
+ content: string,
320
+ nodeId: string | null | undefined,
321
+ ): string {
322
+ if (typeof window === "undefined" || !nodeId) return content;
323
+ try {
324
+ const doc = new DOMParser().parseFromString(content, "text/html");
325
+ const node = doc.querySelector(
326
+ `[data-agent-native-node-id="${CSS.escape(nodeId)}"]`,
327
+ );
328
+ if (!node || !node.hasAttribute(BOARD_TEXT_AUTO_COLOR_MARKER)) {
329
+ return content;
330
+ }
331
+ node.removeAttribute(BOARD_TEXT_AUTO_COLOR_MARKER);
332
+ return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
333
+ } catch {
334
+ return content;
335
+ }
336
+ }
@@ -0,0 +1,270 @@
1
+ import type {
2
+ CanvasFrameGeometry,
3
+ CanvasFrameGeometryById,
4
+ } from "@shared/canvas-frames";
5
+
6
+ export const KEEPALIVE_ACTION_MAX_BYTES = 60_000;
7
+
8
+ export type DesignDataOperation =
9
+ | {
10
+ op: "set";
11
+ path: [string, ...string[]];
12
+ value: unknown;
13
+ }
14
+ | {
15
+ op: "delete";
16
+ path: [string, ...string[]];
17
+ };
18
+
19
+ export interface PendingDesignDataOperation {
20
+ operation: DesignDataOperation;
21
+ revision: number;
22
+ order: number;
23
+ }
24
+
25
+ export type PendingDesignDataOperations = Record<
26
+ string,
27
+ PendingDesignDataOperation
28
+ >;
29
+
30
+ function isRecord(value: unknown): value is Record<string, unknown> {
31
+ return value !== null && typeof value === "object" && !Array.isArray(value);
32
+ }
33
+
34
+ function recordValue(
35
+ data: Record<string, unknown>,
36
+ key: string,
37
+ ): Record<string, unknown> {
38
+ const value = data[key];
39
+ return isRecord(value) ? value : {};
40
+ }
41
+
42
+ function operationPathKey(operation: DesignDataOperation): string {
43
+ return JSON.stringify(operation.path);
44
+ }
45
+
46
+ function valuesEqual(left: unknown, right: unknown): boolean {
47
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
48
+ }
49
+
50
+ export function viewportSizeFromFrameGeometry(
51
+ geometry: CanvasFrameGeometry | undefined,
52
+ ): { width: number; height: number } | null {
53
+ if (
54
+ typeof geometry?.width !== "number" ||
55
+ !Number.isFinite(geometry.width) ||
56
+ typeof geometry.height !== "number" ||
57
+ !Number.isFinite(geometry.height)
58
+ ) {
59
+ return null;
60
+ }
61
+ return {
62
+ width: Math.max(1, Math.round(geometry.width)),
63
+ height: Math.max(1, Math.round(geometry.height)),
64
+ };
65
+ }
66
+
67
+ export function buildFrameGeometryDataOperations(args: {
68
+ previousGeometry: CanvasFrameGeometryById;
69
+ nextGeometry: CanvasFrameGeometryById;
70
+ designData: Record<string, unknown>;
71
+ syncViewportFrameIds?: readonly string[];
72
+ }): DesignDataOperation[] {
73
+ const operations: DesignDataOperation[] = [];
74
+ const frameIds = new Set([
75
+ ...Object.keys(args.previousGeometry),
76
+ ...Object.keys(args.nextGeometry),
77
+ ]);
78
+
79
+ for (const frameId of frameIds) {
80
+ const previous = args.previousGeometry[frameId];
81
+ const next = args.nextGeometry[frameId];
82
+ if (valuesEqual(previous, next)) continue;
83
+ if (next === undefined) {
84
+ operations.push({ op: "delete", path: ["canvasFrames", frameId] });
85
+ } else {
86
+ operations.push({
87
+ op: "set",
88
+ path: ["canvasFrames", frameId],
89
+ value: { ...next },
90
+ });
91
+ }
92
+ }
93
+
94
+ const screenMetadata = recordValue(args.designData, "screenMetadata");
95
+ const localhostScreens = recordValue(args.designData, "localhostScreens");
96
+ for (const frameId of new Set(args.syncViewportFrameIds ?? [])) {
97
+ const viewport = viewportSizeFromFrameGeometry(args.nextGeometry[frameId]);
98
+ if (!viewport) continue;
99
+ const metadataEntry = recordValue(screenMetadata, frameId);
100
+ if (metadataEntry.width !== viewport.width) {
101
+ operations.push({
102
+ op: "set",
103
+ path: ["screenMetadata", frameId, "width"],
104
+ value: viewport.width,
105
+ });
106
+ }
107
+ if (metadataEntry.height !== viewport.height) {
108
+ operations.push({
109
+ op: "set",
110
+ path: ["screenMetadata", frameId, "height"],
111
+ value: viewport.height,
112
+ });
113
+ }
114
+
115
+ const localhostEntry = recordValue(localhostScreens, frameId);
116
+ if (Object.keys(localhostEntry).length === 0) continue;
117
+ if (localhostEntry.width !== viewport.width) {
118
+ operations.push({
119
+ op: "set",
120
+ path: ["localhostScreens", frameId, "width"],
121
+ value: viewport.width,
122
+ });
123
+ }
124
+ if (localhostEntry.height !== viewport.height) {
125
+ operations.push({
126
+ op: "set",
127
+ path: ["localhostScreens", frameId, "height"],
128
+ value: viewport.height,
129
+ });
130
+ }
131
+ }
132
+
133
+ return operations;
134
+ }
135
+
136
+ export function applyDesignDataOperations(
137
+ data: Record<string, unknown>,
138
+ operations: readonly DesignDataOperation[],
139
+ ): Record<string, unknown> {
140
+ const root = { ...data };
141
+ for (const operation of operations) {
142
+ let target = root;
143
+ let missingDeleteParent = false;
144
+ for (const segment of operation.path.slice(0, -1)) {
145
+ const current = target[segment];
146
+ if (current === undefined) {
147
+ if (operation.op === "delete") {
148
+ missingDeleteParent = true;
149
+ break;
150
+ }
151
+ const next: Record<string, unknown> = {};
152
+ target[segment] = next;
153
+ target = next;
154
+ continue;
155
+ }
156
+ if (!isRecord(current)) {
157
+ throw new Error(
158
+ `Cannot apply design data operation through non-object path "${operation.path.join(".")}".`,
159
+ );
160
+ }
161
+ const cloned = { ...current };
162
+ target[segment] = cloned;
163
+ target = cloned;
164
+ }
165
+ if (missingDeleteParent) continue;
166
+ const leaf = operation.path[operation.path.length - 1]!;
167
+ if (operation.op === "delete") delete target[leaf];
168
+ else target[leaf] = operation.value;
169
+ }
170
+ return root;
171
+ }
172
+
173
+ export function compactDesignDataOperations(
174
+ operations: readonly DesignDataOperation[],
175
+ ): DesignDataOperation[] {
176
+ const byPath = new Map<string, DesignDataOperation>();
177
+ for (const operation of operations) {
178
+ const key = operationPathKey(operation);
179
+ byPath.delete(key);
180
+ byPath.set(key, operation);
181
+ }
182
+ return [...byPath.values()];
183
+ }
184
+
185
+ export function stagePendingDesignDataOperations(
186
+ pending: PendingDesignDataOperations,
187
+ operations: readonly DesignDataOperation[],
188
+ revision: number,
189
+ ): PendingDesignDataOperations {
190
+ const next = { ...pending };
191
+ operations.forEach((operation, order) => {
192
+ next[operationPathKey(operation)] = { operation, revision, order };
193
+ });
194
+ return next;
195
+ }
196
+
197
+ export function clearAcknowledgedDesignDataOperations(
198
+ pending: PendingDesignDataOperations,
199
+ operations: readonly DesignDataOperation[],
200
+ revision: number,
201
+ ): PendingDesignDataOperations {
202
+ const next = { ...pending };
203
+ for (const operation of operations) {
204
+ const key = operationPathKey(operation);
205
+ if (next[key]?.revision === revision) delete next[key];
206
+ }
207
+ return next;
208
+ }
209
+
210
+ /**
211
+ * Clears every operation included in a compacted save through `revision`
212
+ * while preserving edits that entered the queue after that request began.
213
+ */
214
+ export function clearAcknowledgedDesignDataOperationsThroughRevision(
215
+ pending: PendingDesignDataOperations,
216
+ revision: number,
217
+ ): PendingDesignDataOperations {
218
+ return Object.fromEntries(
219
+ Object.entries(pending).filter(
220
+ ([, operation]) => operation.revision > revision,
221
+ ),
222
+ );
223
+ }
224
+
225
+ export function pendingDesignDataOperations(
226
+ pending: PendingDesignDataOperations,
227
+ ): DesignDataOperation[] {
228
+ return Object.values(pending)
229
+ .sort(
230
+ (left, right) =>
231
+ left.revision - right.revision || left.order - right.order,
232
+ )
233
+ .map(({ operation }) => operation);
234
+ }
235
+
236
+ function byteLength(value: string): number {
237
+ if (typeof TextEncoder === "undefined") return value.length;
238
+ return new TextEncoder().encode(value).length;
239
+ }
240
+
241
+ export function buildDataOperationsKeepalivePayload(
242
+ designId: string | undefined,
243
+ operations: readonly DesignDataOperation[],
244
+ operationSource: string,
245
+ operationRevision: number,
246
+ maxBytes = KEEPALIVE_ACTION_MAX_BYTES,
247
+ ): {
248
+ id: string;
249
+ dataOperations: DesignDataOperation[];
250
+ operationSource: string;
251
+ operationRevision: number;
252
+ } | null {
253
+ if (!designId) return null;
254
+ const dataOperations = compactDesignDataOperations(operations);
255
+ if (dataOperations.length === 0) return null;
256
+ if (
257
+ !operationSource ||
258
+ !Number.isSafeInteger(operationRevision) ||
259
+ operationRevision < 0
260
+ ) {
261
+ return null;
262
+ }
263
+ const payload = {
264
+ id: designId,
265
+ dataOperations,
266
+ operationSource,
267
+ operationRevision,
268
+ };
269
+ return byteLength(JSON.stringify(payload)) <= maxBytes ? payload : null;
270
+ }
@@ -0,0 +1,24 @@
1
+ export function isRadixOverlayOpen(wrapperEl: Element): boolean {
2
+ if (wrapperEl.hasAttribute("data-agent-native-tooltip")) return false;
3
+ const isOpenState = (el: Element) =>
4
+ el.getAttribute("data-state") !== "closed";
5
+ const stateful = wrapperEl.querySelectorAll("[data-state]");
6
+ if (stateful.length > 0) {
7
+ const allTooltips = Array.from(stateful).every((content) =>
8
+ content.hasAttribute("data-agent-native-tooltip"),
9
+ );
10
+ if (allTooltips) return false;
11
+ if (wrapperEl.hasAttribute("data-state")) {
12
+ return isOpenState(wrapperEl);
13
+ }
14
+ return Array.from(stateful).some(
15
+ (content) =>
16
+ !content.hasAttribute("data-agent-native-tooltip") &&
17
+ isOpenState(content),
18
+ );
19
+ }
20
+ if (wrapperEl.hasAttribute("data-state")) {
21
+ return isOpenState(wrapperEl);
22
+ }
23
+ return true;
24
+ }
@@ -0,0 +1,11 @@
1
+ export function queryUniqueSelector(
2
+ root: ParentNode,
3
+ selector: string,
4
+ ): Element | null {
5
+ try {
6
+ const matches = root.querySelectorAll(selector);
7
+ return matches.length === 1 ? (matches[0] ?? null) : null;
8
+ } catch {
9
+ return null;
10
+ }
11
+ }