@agent-native/core 0.69.0 → 0.70.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 (336) hide show
  1. package/README.md +15 -15
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +109 -0
  4. package/corpus/core/docs/design/durable-agent-runs.md +217 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/action.ts +89 -0
  7. package/corpus/core/src/agent/model-config.ts +0 -2
  8. package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
  9. package/corpus/core/src/changelog/parse.ts +245 -0
  10. package/corpus/core/src/cli/changelog.ts +199 -0
  11. package/corpus/core/src/cli/index.ts +19 -0
  12. package/corpus/core/src/cli/recap.ts +24 -1
  13. package/corpus/core/src/cli/skills.ts +74 -47
  14. package/corpus/core/src/client/AgentPanel.tsx +5 -3
  15. package/corpus/core/src/client/AssistantChat.tsx +8 -0
  16. package/corpus/core/src/client/CommandMenu.tsx +183 -75
  17. package/corpus/core/src/client/agent-chat-adapter.ts +44 -7
  18. package/corpus/core/src/client/analytics.ts +181 -0
  19. package/corpus/core/src/client/changelog/Changelog.tsx +321 -0
  20. package/corpus/core/src/client/client-surface.ts +41 -0
  21. package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
  22. package/corpus/core/src/client/feedback-context.ts +4 -0
  23. package/corpus/core/src/client/index.ts +12 -0
  24. package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +54 -0
  25. package/corpus/core/src/client/settings/useBuilderStatus.ts +82 -20
  26. package/corpus/core/src/client/sse-event-processor.ts +7 -1
  27. package/corpus/core/src/mcp/build-server.ts +57 -1
  28. package/corpus/core/src/mcp/embed-app.ts +197 -7
  29. package/corpus/core/src/mcp/oauth-route.ts +109 -13
  30. package/corpus/core/src/server/attribution.ts +211 -0
  31. package/corpus/core/src/server/auth.ts +6 -0
  32. package/corpus/core/src/server/better-auth-instance.ts +49 -5
  33. package/corpus/core/src/server/builder-browser.ts +27 -1
  34. package/corpus/core/src/server/core-routes-plugin.ts +136 -0
  35. package/corpus/core/src/server/google-oauth.ts +19 -6
  36. package/corpus/core/src/server/onboarding-html.ts +35 -0
  37. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  38. package/corpus/core/src/styles/agent-conversation.css +21 -0
  39. package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  40. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  41. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  42. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
  43. package/corpus/templates/analytics/AGENTS.md +8 -1
  44. package/corpus/templates/analytics/CHANGELOG.md +10 -0
  45. package/corpus/templates/analytics/actions/install-dashboard-template.ts +99 -4
  46. package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
  47. package/corpus/templates/analytics/actions/update-dashboard.ts +39 -8
  48. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
  49. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
  50. package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -3
  51. package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
  52. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -215
  53. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -6
  54. package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
  55. package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
  56. package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
  57. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
  58. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
  59. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
  60. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
  61. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
  62. package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
  63. package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
  64. package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
  65. package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
  66. package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
  69. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
  70. package/corpus/templates/analytics/server/db/index.ts +1 -1
  71. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
  72. package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
  73. package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
  74. package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
  75. package/corpus/templates/assets/CHANGELOG.md +10 -0
  76. package/corpus/templates/assets/actions/generate-image.ts +24 -0
  77. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
  78. package/corpus/templates/assets/app/global.css +17 -1
  79. package/corpus/templates/assets/app/root.tsx +7 -1
  80. package/corpus/templates/assets/app/routes/_index.tsx +94 -14
  81. package/corpus/templates/assets/app/routes/settings.tsx +6 -0
  82. package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
  83. package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
  84. package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
  85. package/corpus/templates/brain/CHANGELOG.md +10 -0
  86. package/corpus/templates/brain/app/root.tsx +7 -1
  87. package/corpus/templates/brain/app/routes/settings.tsx +8 -1
  88. package/corpus/templates/calendar/CHANGELOG.md +10 -0
  89. package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
  90. package/corpus/templates/calendar/app/root.tsx +7 -1
  91. package/corpus/templates/chat/CHANGELOG.md +10 -0
  92. package/corpus/templates/chat/app/root.tsx +7 -1
  93. package/corpus/templates/clips/CHANGELOG.md +10 -0
  94. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
  95. package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
  96. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
  97. package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
  98. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
  99. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +7 -11
  100. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +31 -0
  101. package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
  102. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
  103. package/corpus/templates/clips/app/root.tsx +7 -1
  104. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
  105. package/corpus/templates/clips/app/routes/record.tsx +78 -5
  106. package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
  107. package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
  112. package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
  113. package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
  114. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
  115. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
  116. package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
  117. package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
  118. package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
  119. package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
  120. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
  121. package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
  122. package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
  123. package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
  124. package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
  125. package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
  126. package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
  127. package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
  128. package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
  129. package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
  130. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
  131. package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
  132. package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
  133. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  134. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
  135. package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
  136. package/corpus/templates/clips/desktop/src/styles.css +119 -2
  137. package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
  138. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +140 -1
  139. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
  140. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
  141. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +85 -6
  142. package/corpus/templates/clips/shared/share-attribution.ts +79 -0
  143. package/corpus/templates/content/AGENTS.md +20 -3
  144. package/corpus/templates/content/CHANGELOG.md +10 -0
  145. package/corpus/templates/content/actions/_database-utils.ts +15 -0
  146. package/corpus/templates/content/actions/_property-utils.ts +371 -4
  147. package/corpus/templates/content/actions/configure-document-property.ts +35 -1
  148. package/corpus/templates/content/actions/create-content-database.ts +5 -1
  149. package/corpus/templates/content/actions/delete-document-property.ts +52 -2
  150. package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
  151. package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
  152. package/corpus/templates/content/actions/set-document-property.ts +39 -0
  153. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
  154. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
  155. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
  156. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
  157. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
  158. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
  159. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
  160. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
  161. package/corpus/templates/content/app/global.css +9 -0
  162. package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
  163. package/corpus/templates/content/app/root.tsx +7 -1
  164. package/corpus/templates/content/server/db/schema.ts +30 -0
  165. package/corpus/templates/content/server/plugins/db.ts +87 -0
  166. package/corpus/templates/content/shared/api.ts +7 -0
  167. package/corpus/templates/content/shared/properties.ts +109 -0
  168. package/corpus/templates/design/CHANGELOG.md +10 -0
  169. package/corpus/templates/design/actions/generate-design.ts +4 -0
  170. package/corpus/templates/design/app/root.tsx +7 -1
  171. package/corpus/templates/design/app/routes/settings.tsx +10 -2
  172. package/corpus/templates/dispatch/CHANGELOG.md +10 -0
  173. package/corpus/templates/dispatch/app/root.tsx +7 -1
  174. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
  175. package/corpus/templates/forms/CHANGELOG.md +10 -0
  176. package/corpus/templates/forms/actions/export-responses.ts +6 -0
  177. package/corpus/templates/forms/actions/list-responses.ts +2 -0
  178. package/corpus/templates/forms/actions/response-insights.ts +2 -0
  179. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
  180. package/corpus/templates/forms/app/root.tsx +7 -1
  181. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
  182. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
  183. package/corpus/templates/forms/server/db/schema.ts +7 -0
  184. package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
  185. package/corpus/templates/forms/server/lib/integrations.ts +76 -2
  186. package/corpus/templates/forms/server/plugins/db.ts +20 -0
  187. package/corpus/templates/forms/shared/types.ts +12 -0
  188. package/corpus/templates/macros/CHANGELOG.md +10 -0
  189. package/corpus/templates/macros/app/root.tsx +7 -1
  190. package/corpus/templates/mail/CHANGELOG.md +10 -0
  191. package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
  192. package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
  193. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
  194. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
  195. package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
  196. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
  197. package/corpus/templates/plan/AGENTS.md +4 -0
  198. package/corpus/templates/plan/CHANGELOG.md +10 -0
  199. package/corpus/templates/plan/actions/view-screen.ts +5 -0
  200. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
  201. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
  202. package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
  203. package/corpus/templates/plan/app/root.tsx +7 -1
  204. package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
  205. package/corpus/templates/plan/shared/share-attribution.ts +72 -0
  206. package/corpus/templates/slides/CHANGELOG.md +10 -0
  207. package/corpus/templates/slides/app/root.tsx +7 -1
  208. package/corpus/templates/videos/CHANGELOG.md +4 -151
  209. package/corpus/templates/videos/app/root.tsx +7 -1
  210. package/dist/action.js +87 -0
  211. package/dist/action.js.map +1 -1
  212. package/dist/agent/engine/builder-engine.d.ts +1 -1
  213. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  214. package/dist/agent/model-config.d.ts +2 -2
  215. package/dist/agent/model-config.d.ts.map +1 -1
  216. package/dist/agent/model-config.js +0 -2
  217. package/dist/agent/model-config.js.map +1 -1
  218. package/dist/agent/run-loop-with-resume.d.ts +13 -0
  219. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  220. package/dist/agent/run-loop-with-resume.js +44 -0
  221. package/dist/agent/run-loop-with-resume.js.map +1 -1
  222. package/dist/changelog/parse.d.ts +70 -0
  223. package/dist/changelog/parse.d.ts.map +1 -0
  224. package/dist/changelog/parse.js +200 -0
  225. package/dist/changelog/parse.js.map +1 -0
  226. package/dist/cli/changelog.d.ts +4 -0
  227. package/dist/cli/changelog.d.ts.map +1 -0
  228. package/dist/cli/changelog.js +166 -0
  229. package/dist/cli/changelog.js.map +1 -0
  230. package/dist/cli/index.js +18 -0
  231. package/dist/cli/index.js.map +1 -1
  232. package/dist/cli/recap.d.ts.map +1 -1
  233. package/dist/cli/recap.js +22 -1
  234. package/dist/cli/recap.js.map +1 -1
  235. package/dist/cli/skills.d.ts +3 -3
  236. package/dist/cli/skills.d.ts.map +1 -1
  237. package/dist/cli/skills.js +74 -47
  238. package/dist/cli/skills.js.map +1 -1
  239. package/dist/client/AgentPanel.d.ts.map +1 -1
  240. package/dist/client/AgentPanel.js +5 -3
  241. package/dist/client/AgentPanel.js.map +1 -1
  242. package/dist/client/AssistantChat.d.ts +6 -1
  243. package/dist/client/AssistantChat.d.ts.map +1 -1
  244. package/dist/client/AssistantChat.js +2 -2
  245. package/dist/client/AssistantChat.js.map +1 -1
  246. package/dist/client/CommandMenu.d.ts +17 -1
  247. package/dist/client/CommandMenu.d.ts.map +1 -1
  248. package/dist/client/CommandMenu.js +49 -13
  249. package/dist/client/CommandMenu.js.map +1 -1
  250. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  251. package/dist/client/agent-chat-adapter.js +43 -6
  252. package/dist/client/agent-chat-adapter.js.map +1 -1
  253. package/dist/client/analytics.d.ts +18 -0
  254. package/dist/client/analytics.d.ts.map +1 -1
  255. package/dist/client/analytics.js +177 -0
  256. package/dist/client/analytics.js.map +1 -1
  257. package/dist/client/changelog/Changelog.d.ts +47 -0
  258. package/dist/client/changelog/Changelog.d.ts.map +1 -0
  259. package/dist/client/changelog/Changelog.js +138 -0
  260. package/dist/client/changelog/Changelog.js.map +1 -0
  261. package/dist/client/client-surface.d.ts +17 -0
  262. package/dist/client/client-surface.d.ts.map +1 -0
  263. package/dist/client/client-surface.js +24 -0
  264. package/dist/client/client-surface.js.map +1 -0
  265. package/dist/client/composer/TiptapComposer.d.ts +26 -1
  266. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  267. package/dist/client/composer/TiptapComposer.js +27 -5
  268. package/dist/client/composer/TiptapComposer.js.map +1 -1
  269. package/dist/client/feedback-context.d.ts +3 -0
  270. package/dist/client/feedback-context.d.ts.map +1 -1
  271. package/dist/client/feedback-context.js +2 -0
  272. package/dist/client/feedback-context.js.map +1 -1
  273. package/dist/client/index.d.ts +3 -1
  274. package/dist/client/index.d.ts.map +1 -1
  275. package/dist/client/index.js +3 -1
  276. package/dist/client/index.js.map +1 -1
  277. package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
  278. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  279. package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
  280. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
  281. package/dist/client/settings/useBuilderStatus.d.ts +2 -0
  282. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  283. package/dist/client/settings/useBuilderStatus.js +58 -10
  284. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  285. package/dist/client/sse-event-processor.d.ts.map +1 -1
  286. package/dist/client/sse-event-processor.js +7 -1
  287. package/dist/client/sse-event-processor.js.map +1 -1
  288. package/dist/mcp/build-server.d.ts +21 -0
  289. package/dist/mcp/build-server.d.ts.map +1 -1
  290. package/dist/mcp/build-server.js +43 -1
  291. package/dist/mcp/build-server.js.map +1 -1
  292. package/dist/mcp/embed-app.d.ts.map +1 -1
  293. package/dist/mcp/embed-app.js +197 -7
  294. package/dist/mcp/embed-app.js.map +1 -1
  295. package/dist/mcp/oauth-route.d.ts.map +1 -1
  296. package/dist/mcp/oauth-route.js +95 -12
  297. package/dist/mcp/oauth-route.js.map +1 -1
  298. package/dist/server/attribution.d.ts +83 -0
  299. package/dist/server/attribution.d.ts.map +1 -0
  300. package/dist/server/attribution.js +178 -0
  301. package/dist/server/attribution.js.map +1 -0
  302. package/dist/server/auth.d.ts.map +1 -1
  303. package/dist/server/auth.js +7 -0
  304. package/dist/server/auth.js.map +1 -1
  305. package/dist/server/better-auth-instance.d.ts +9 -1
  306. package/dist/server/better-auth-instance.d.ts.map +1 -1
  307. package/dist/server/better-auth-instance.js +32 -2
  308. package/dist/server/better-auth-instance.js.map +1 -1
  309. package/dist/server/builder-browser.d.ts +4 -0
  310. package/dist/server/builder-browser.d.ts.map +1 -1
  311. package/dist/server/builder-browser.js +20 -1
  312. package/dist/server/builder-browser.js.map +1 -1
  313. package/dist/server/core-routes-plugin.d.ts +25 -0
  314. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  315. package/dist/server/core-routes-plugin.js +101 -0
  316. package/dist/server/core-routes-plugin.js.map +1 -1
  317. package/dist/server/google-oauth.d.ts.map +1 -1
  318. package/dist/server/google-oauth.js +16 -2
  319. package/dist/server/google-oauth.js.map +1 -1
  320. package/dist/server/onboarding-html.d.ts.map +1 -1
  321. package/dist/server/onboarding-html.js +35 -0
  322. package/dist/server/onboarding-html.js.map +1 -1
  323. package/dist/shared/mcp-embed-headers.js +1 -1
  324. package/dist/shared/mcp-embed-headers.js.map +1 -1
  325. package/dist/styles/agent-conversation.css +21 -0
  326. package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  327. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  328. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  329. package/docs/design/durable-agent-runs.md +217 -0
  330. package/package.json +1 -1
  331. package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  332. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  333. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  334. package/corpus/templates/analytics/app/pages/About.tsx +0 -108
  335. package/corpus/templates/analytics/app/routes/about.tsx +0 -9
  336. /package/corpus/templates/analytics/app/routes/{dashboards.tsx → dashboards._index.tsx} +0 -0
@@ -1,12 +1,17 @@
1
- import { and, asc, eq, inArray, type InferSelectModel } from "drizzle-orm";
1
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
2
+ import { and, asc, eq, inArray, sql, type InferSelectModel } from "drizzle-orm";
2
3
  import { getDb, schema } from "../server/db/index.js";
3
4
  import {
5
+ DEFAULT_BLOCKS_FIELD_NAME,
4
6
  defaultPropertyOptions,
5
7
  evaluatePropertyFormula,
6
8
  formulaValueText,
9
+ isBlocksPropertyType,
7
10
  isComputedPropertyType,
8
11
  isEmptyPropertyValue,
12
+ isPrimaryBlocksField,
9
13
  normalizePropertyValue,
14
+ resolveBlocksFieldValue,
10
15
  normalizePropertyVisibility,
11
16
  parsePropertyOptions,
12
17
  parsePropertyValue,
@@ -386,6 +391,10 @@ function normalizeStringList(value: unknown) {
386
391
  export async function listPropertiesForDocument(document: DocumentRow) {
387
392
  const database = await resolvePropertyDatabaseForDocument(document);
388
393
  if (!database) return [];
394
+ // Read path: PURE read. Seeding the primary Blocks field happens at create
395
+ // time and via the one-time startup repair (repairUnseededBlocksFields) —
396
+ // never here. A viewer opening a shared/legacy row must not trigger writes on
397
+ // another owner's database.
389
398
  return listPropertiesForDatabase(database.id, document);
390
399
  }
391
400
 
@@ -416,6 +425,13 @@ export async function listPropertiesForDatabase(
416
425
  ? await databaseRowNumbersByDocumentId(databaseId)
417
426
  : new Map<string, number>();
418
427
 
428
+ // Additional (non-primary) Blocks fields keep their content in their own
429
+ // store, keyed by (documentId, propertyId). Load this row's contents up front
430
+ // so each Blocks field resolves to its OWN independent content.
431
+ const blockContentByPropertyId = valueDocument
432
+ ? await blockFieldContentsForDocument(valueDocument.id)
433
+ : new Map<string, string>();
434
+
419
435
  const properties = definitions.map((definition) => {
420
436
  const type = definition.type as DocumentPropertyType;
421
437
  const storedValue = valueByPropertyId.get(definition.id);
@@ -437,7 +453,15 @@ export async function listPropertiesForDatabase(
437
453
  ? computedPropertyValue(type, valueDocument, {
438
454
  databaseRowNumber: rowNumberByDocumentId.get(valueDocument.id),
439
455
  })
440
- : parsePropertyValue(storedValue?.valueJson),
456
+ : valueDocument && isBlocksPropertyType(type)
457
+ ? // Each Blocks field reads from exactly one place: the primary from
458
+ // the document body, additional fields from their own store.
459
+ resolveBlocksFieldValue({
460
+ options,
461
+ documentBody: valueDocument.content,
462
+ blockFieldContent: blockContentByPropertyId.get(definition.id),
463
+ })
464
+ : parsePropertyValue(storedValue?.valueJson),
441
465
  editable: !isComputedPropertyType(type),
442
466
  };
443
467
  });
@@ -544,8 +568,14 @@ async function propertyValuesForLinkedDocuments(
544
568
  const docs = await db
545
569
  .select()
546
570
  .from(schema.documents)
547
- .where(inArray(schema.documents.id, documentIds));
571
+ .where(
572
+ and(
573
+ inArray(schema.documents.id, documentIds),
574
+ accessFilter(schema.documents, schema.documentShares),
575
+ ),
576
+ );
548
577
  const docById = new Map(docs.map((doc) => [doc.id, doc]));
578
+ const accessibleDocumentIds = docs.map((doc) => doc.id);
549
579
 
550
580
  if (isComputedPropertyType(property.definition.type)) {
551
581
  const rowNumberByDocumentId = property.definition.databaseId
@@ -561,12 +591,55 @@ async function propertyValuesForLinkedDocuments(
561
591
  });
562
592
  }
563
593
 
594
+ if (accessibleDocumentIds.length === 0) {
595
+ return documentIds.map(() => null);
596
+ }
597
+
598
+ if (isBlocksPropertyType(property.definition.type)) {
599
+ const blockFieldRows = isPrimaryBlocksField(property.definition.options)
600
+ ? []
601
+ : await db
602
+ .select({
603
+ documentId: schema.documentBlockFieldContents.documentId,
604
+ content: schema.documentBlockFieldContents.content,
605
+ })
606
+ .from(schema.documentBlockFieldContents)
607
+ .where(
608
+ and(
609
+ inArray(
610
+ schema.documentBlockFieldContents.documentId,
611
+ accessibleDocumentIds,
612
+ ),
613
+ eq(
614
+ schema.documentBlockFieldContents.propertyId,
615
+ property.definition.id,
616
+ ),
617
+ ),
618
+ );
619
+ const blockFieldContentByDocumentId = new Map(
620
+ blockFieldRows.map((row) => [row.documentId, row.content]),
621
+ );
622
+ return documentIds.map((documentId) => {
623
+ const doc = docById.get(documentId);
624
+ return doc
625
+ ? resolveBlocksFieldValue({
626
+ options: property.definition.options,
627
+ documentBody: doc.content,
628
+ blockFieldContent: blockFieldContentByDocumentId.get(documentId),
629
+ })
630
+ : null;
631
+ });
632
+ }
633
+
564
634
  const storedValues = await db
565
635
  .select()
566
636
  .from(schema.documentPropertyValues)
567
637
  .where(
568
638
  and(
569
- inArray(schema.documentPropertyValues.documentId, documentIds),
639
+ inArray(
640
+ schema.documentPropertyValues.documentId,
641
+ accessibleDocumentIds,
642
+ ),
570
643
  eq(schema.documentPropertyValues.propertyId, property.definition.id),
571
644
  ),
572
645
  );
@@ -612,3 +685,297 @@ export function normalizedValueJson(
612
685
  ) {
613
686
  return serializePropertyValue(normalizePropertyValue(type, value));
614
687
  }
688
+
689
+ // --- Blocks fields ---------------------------------------------------------
690
+ //
691
+ // Storage model: the default/primary "Content" Blocks field is backed by
692
+ // `documents.content`. Every ADDITIONAL Blocks field stores its content in its
693
+ // own row in `document_block_field_contents`, keyed by (documentId,
694
+ // propertyId). This guarantees independence — no two Blocks fields ever share
695
+ // content.
696
+
697
+ // Load all additional-Blocks-field contents for a single document, keyed by
698
+ // propertyId. The primary field is intentionally absent here (its content lives
699
+ // on the document itself).
700
+ export async function blockFieldContentsForDocument(
701
+ documentId: string,
702
+ ): Promise<Map<string, string>> {
703
+ const db = getDb();
704
+ const rows = await db
705
+ .select({
706
+ propertyId: schema.documentBlockFieldContents.propertyId,
707
+ content: schema.documentBlockFieldContents.content,
708
+ })
709
+ .from(schema.documentBlockFieldContents)
710
+ .where(eq(schema.documentBlockFieldContents.documentId, documentId));
711
+ return new Map(rows.map((row) => [row.propertyId, row.content ?? ""]));
712
+ }
713
+
714
+ export async function readBlockFieldContent(
715
+ documentId: string,
716
+ propertyId: string,
717
+ ): Promise<string> {
718
+ const db = getDb();
719
+ const [row] = await db
720
+ .select({ content: schema.documentBlockFieldContents.content })
721
+ .from(schema.documentBlockFieldContents)
722
+ .where(
723
+ and(
724
+ eq(schema.documentBlockFieldContents.documentId, documentId),
725
+ eq(schema.documentBlockFieldContents.propertyId, propertyId),
726
+ ),
727
+ );
728
+ return row?.content ?? "";
729
+ }
730
+
731
+ // Upsert the content for an additional (non-primary) Blocks field.
732
+ //
733
+ // Atomic insert-or-update on the UNIQUE (document_id, property_id) index — no
734
+ // read-then-write window. Two concurrent first-saves can no longer race into a
735
+ // duplicate-key throw: the loser falls through to the conflict UPDATE branch.
736
+ export async function writeBlockFieldContent(args: {
737
+ documentId: string;
738
+ propertyId: string;
739
+ ownerEmail: string;
740
+ content: string;
741
+ now: string;
742
+ }): Promise<void> {
743
+ const db = getDb();
744
+ await db
745
+ .insert(schema.documentBlockFieldContents)
746
+ .values({
747
+ id: nanoid(),
748
+ ownerEmail: args.ownerEmail,
749
+ documentId: args.documentId,
750
+ propertyId: args.propertyId,
751
+ content: args.content,
752
+ createdAt: args.now,
753
+ updatedAt: args.now,
754
+ })
755
+ .onConflictDoUpdate({
756
+ target: [
757
+ schema.documentBlockFieldContents.documentId,
758
+ schema.documentBlockFieldContents.propertyId,
759
+ ],
760
+ set: { content: args.content, updatedAt: args.now },
761
+ });
762
+ }
763
+
764
+ // Write the primary Blocks field's content — i.e. the document body.
765
+ export async function writePrimaryBlocksContent(args: {
766
+ documentId: string;
767
+ content: string;
768
+ now: string;
769
+ }): Promise<void> {
770
+ await assertAccess("document", args.documentId, "editor");
771
+ const db = getDb();
772
+ await db
773
+ .update(schema.documents)
774
+ .set({ content: args.content, updatedAt: args.now })
775
+ .where(eq(schema.documents.id, args.documentId));
776
+ }
777
+
778
+ // Fetch a single property definition scoped to a database (and owner).
779
+ export async function getPropertyDefinitionForDatabase(args: {
780
+ propertyId: string;
781
+ databaseId: string;
782
+ ownerEmail: string;
783
+ }) {
784
+ const db = getDb();
785
+ const [definition] = await db
786
+ .select()
787
+ .from(schema.documentPropertyDefinitions)
788
+ .where(
789
+ and(
790
+ eq(schema.documentPropertyDefinitions.id, args.propertyId),
791
+ eq(schema.documentPropertyDefinitions.ownerEmail, args.ownerEmail),
792
+ eq(schema.documentPropertyDefinitions.databaseId, args.databaseId),
793
+ ),
794
+ );
795
+ return definition ?? null;
796
+ }
797
+
798
+ // How many Blocks-type property definitions a database has. Used to drive the
799
+ // solo (chromeless) vs. multi (headers + collapsible) rendering decision and
800
+ // the "only Blocks field" delete warning.
801
+ export async function countBlocksFieldsForDatabase(
802
+ databaseId: string,
803
+ ): Promise<number> {
804
+ const db = getDb();
805
+ const definitions = await db
806
+ .select({ type: schema.documentPropertyDefinitions.type })
807
+ .from(schema.documentPropertyDefinitions)
808
+ .where(eq(schema.documentPropertyDefinitions.databaseId, databaseId));
809
+ return definitions.filter((definition) =>
810
+ isBlocksPropertyType(definition.type as DocumentPropertyType),
811
+ ).length;
812
+ }
813
+
814
+ // The id of a database's existing primary Blocks definition, if any. Used to
815
+ // adopt a legacy primary created by the old read-path seeder rather than
816
+ // creating a duplicate.
817
+ async function findExistingPrimaryBlocksDefinition(
818
+ databaseId: string,
819
+ ): Promise<string | null> {
820
+ const db = getDb();
821
+ const definitions = await db
822
+ .select({
823
+ id: schema.documentPropertyDefinitions.id,
824
+ type: schema.documentPropertyDefinitions.type,
825
+ optionsJson: schema.documentPropertyDefinitions.optionsJson,
826
+ })
827
+ .from(schema.documentPropertyDefinitions)
828
+ .where(eq(schema.documentPropertyDefinitions.databaseId, databaseId));
829
+ const primary = definitions.find(
830
+ (definition) =>
831
+ isBlocksPropertyType(definition.type as DocumentPropertyType) &&
832
+ isPrimaryBlocksField(parsePropertyOptions(definition.optionsJson)),
833
+ );
834
+ return primary?.id ?? null;
835
+ }
836
+
837
+ // Seed the primary "Content" Blocks field for a database exactly ONCE.
838
+ //
839
+ // `content_databases.primary_blocks_property_id` is the single source of truth
840
+ // and the concurrency guard. The deterministic primary definition is inserted
841
+ // before the database row is marked seeded, so we never publish blocks_seeded=1
842
+ // before the definition row exists. Two concurrent calls converge on the same
843
+ // property id and the loser returns the already-claimed id.
844
+ //
845
+ // Returns the primary property id (existing or newly created). Never reseeds a
846
+ // database whose primary was intentionally deleted (blocks_seeded=1, id NULL).
847
+ export async function seedDefaultBlocksField(args: {
848
+ databaseId: string;
849
+ ownerEmail: string;
850
+ orgId: string | null;
851
+ now: string;
852
+ }): Promise<string | null> {
853
+ const db = getDb();
854
+
855
+ // Deterministic id keyed to the database so concurrent claimants converge on
856
+ // the same value; the UNIQUE primary-key on definitions also rejects a
857
+ // duplicate insert if two callers somehow both attempt it.
858
+ const id = `blocks_primary_${args.databaseId}`;
859
+
860
+ // Legacy adoption: a database seeded by the OLD read-path safety net already
861
+ // has a primary "Content" definition but a NULL column (if the v52 backfill
862
+ // somehow didn't run for it). Adopt that existing definition instead of
863
+ // creating a second primary — guarantees the invariant even off the migration
864
+ // path. The atomic UPDATE (column still NULL) makes this race-safe.
865
+ const existingPrimary = await findExistingPrimaryBlocksDefinition(
866
+ args.databaseId,
867
+ );
868
+ if (existingPrimary) {
869
+ await db
870
+ .update(schema.contentDatabases)
871
+ .set({
872
+ primaryBlocksPropertyId: existingPrimary,
873
+ blocksSeeded: 1,
874
+ updatedAt: args.now,
875
+ })
876
+ .where(
877
+ and(
878
+ eq(schema.contentDatabases.id, args.databaseId),
879
+ sql`primary_blocks_property_id IS NULL`,
880
+ ),
881
+ );
882
+ return existingPrimary;
883
+ }
884
+
885
+ const [database] = await db
886
+ .select({
887
+ primaryId: schema.contentDatabases.primaryBlocksPropertyId,
888
+ blocksSeeded: schema.contentDatabases.blocksSeeded,
889
+ })
890
+ .from(schema.contentDatabases)
891
+ .where(eq(schema.contentDatabases.id, args.databaseId));
892
+ if (!database) return null;
893
+ if (database.primaryId) return database.primaryId;
894
+ if (database.blocksSeeded === 1) return null;
895
+
896
+ const [maxPos] = await db
897
+ .select({ max: sql<number>`COALESCE(MAX(position), -1)` })
898
+ .from(schema.documentPropertyDefinitions)
899
+ .where(eq(schema.documentPropertyDefinitions.databaseId, args.databaseId));
900
+
901
+ await db
902
+ .insert(schema.documentPropertyDefinitions)
903
+ .values({
904
+ id,
905
+ ownerEmail: args.ownerEmail,
906
+ orgId: args.orgId,
907
+ databaseId: args.databaseId,
908
+ name: DEFAULT_BLOCKS_FIELD_NAME,
909
+ type: "blocks",
910
+ visibility: "always_show",
911
+ optionsJson: serializePropertyOptions({ blocks: { primary: true } }),
912
+ position: (maxPos?.max ?? -1) + 1,
913
+ createdAt: args.now,
914
+ updatedAt: args.now,
915
+ })
916
+ .onConflictDoNothing();
917
+
918
+ const claim = await db
919
+ .update(schema.contentDatabases)
920
+ .set({
921
+ primaryBlocksPropertyId: id,
922
+ blocksSeeded: 1,
923
+ updatedAt: args.now,
924
+ })
925
+ .where(
926
+ and(
927
+ eq(schema.contentDatabases.id, args.databaseId),
928
+ sql`primary_blocks_property_id IS NULL`,
929
+ sql`blocks_seeded = 0`,
930
+ ),
931
+ )
932
+ .returning({ id: schema.contentDatabases.primaryBlocksPropertyId });
933
+
934
+ if (claim[0]?.id === id) return id;
935
+
936
+ const [afterClaim] = await db
937
+ .select({ primaryId: schema.contentDatabases.primaryBlocksPropertyId })
938
+ .from(schema.contentDatabases)
939
+ .where(eq(schema.contentDatabases.id, args.databaseId));
940
+ if (afterClaim?.primaryId) return afterClaim.primaryId;
941
+
942
+ await db
943
+ .delete(schema.documentPropertyDefinitions)
944
+ .where(eq(schema.documentPropertyDefinitions.id, id));
945
+ return null;
946
+ }
947
+
948
+ // One-time startup repair for LEGACY databases that have never been seeded —
949
+ // i.e. databases created before the Blocks type existed and which have NO
950
+ // primary Blocks field yet (blocks_seeded = 0). Their `documents.content` body
951
+ // still works; seeding the primary field exposes it as a first-class property.
952
+ //
953
+ // Runs at boot from the migration plugin, NOT from any read path, so opening a
954
+ // shared/legacy row never triggers a write. Uses each database's own owner/org
955
+ // (no request context). Idempotent: the atomic claim in seedDefaultBlocksField
956
+ // makes re-runs no-ops, and databases whose primary was intentionally deleted
957
+ // (blocks_seeded = 1) are skipped.
958
+ export async function repairUnseededBlocksFields(): Promise<number> {
959
+ const db = getDb();
960
+ const databases = await db
961
+ .select({
962
+ id: schema.contentDatabases.id,
963
+ ownerEmail: schema.contentDatabases.ownerEmail,
964
+ orgId: schema.contentDatabases.orgId,
965
+ })
966
+ .from(schema.contentDatabases)
967
+ .where(eq(schema.contentDatabases.blocksSeeded, 0));
968
+
969
+ const now = new Date().toISOString();
970
+ let seeded = 0;
971
+ for (const database of databases) {
972
+ await seedDefaultBlocksField({
973
+ databaseId: database.id,
974
+ ownerEmail: database.ownerEmail,
975
+ orgId: database.orgId ?? null,
976
+ now,
977
+ });
978
+ seeded += 1;
979
+ }
980
+ return seeded;
981
+ }
@@ -7,7 +7,11 @@ import { getDb, schema } from "../server/db/index.js";
7
7
  import {
8
8
  CREATABLE_DOCUMENT_PROPERTY_TYPES,
9
9
  DOCUMENT_PROPERTY_VISIBILITIES,
10
+ isBlocksPropertyType,
10
11
  isComputedPropertyType,
12
+ isPrimaryBlocksField,
13
+ parsePropertyOptions,
14
+ serializePropertyOptions,
11
15
  normalizePropertyVisibility,
12
16
  type DocumentPropertyType,
13
17
  } from "../shared/properties.js";
@@ -79,7 +83,7 @@ export default defineAction({
79
83
  const now = new Date().toISOString();
80
84
  const name = args.name.trim();
81
85
  const type = args.type as DocumentPropertyType;
82
- const optionsJson = optionsForNewProperty(type, args.options as any);
86
+ let optionsJson = optionsForNewProperty(type, args.options as any);
83
87
  const database = await resolvePropertyDatabaseForDocument(document);
84
88
  if (!database) {
85
89
  throw new Error(
@@ -108,6 +112,27 @@ export default defineAction({
108
112
  ) {
109
113
  throw new Error("Computed property types cannot be changed.");
110
114
  }
115
+
116
+ const existingOptions = parsePropertyOptions(existing.optionsJson);
117
+ const existingIsPrimaryBlocks =
118
+ isBlocksPropertyType(existing.type as DocumentPropertyType) &&
119
+ isPrimaryBlocksField(existingOptions);
120
+
121
+ // The primary "Content" Blocks field backs the document body — it can be
122
+ // renamed/hidden but not retyped (delete it from the database view to
123
+ // remove the body). Block the type switch defensively.
124
+ if (existingIsPrimaryBlocks && existing.type !== type) {
125
+ throw new Error(
126
+ "The primary Content (Blocks) field cannot change type. Delete it from the database view to remove the body.",
127
+ );
128
+ }
129
+
130
+ // Preserve the primary flag when re-saving the primary Blocks field (a
131
+ // rename or visibility change must NOT demote it to a normal Blocks field).
132
+ if (existingIsPrimaryBlocks && isBlocksPropertyType(type)) {
133
+ optionsJson = serializePropertyOptions({ blocks: { primary: true } });
134
+ }
135
+
111
136
  if (existing.type !== type) {
112
137
  await db
113
138
  .delete(schema.documentPropertyValues)
@@ -117,6 +142,15 @@ export default defineAction({
117
142
  eq(schema.documentPropertyValues.ownerEmail, document.ownerEmail),
118
143
  ),
119
144
  );
145
+ // Switching a Blocks field to another type drops its independent content.
146
+ if (
147
+ isBlocksPropertyType(existing.type as DocumentPropertyType) &&
148
+ !isBlocksPropertyType(type)
149
+ ) {
150
+ await db
151
+ .delete(schema.documentBlockFieldContents)
152
+ .where(eq(schema.documentBlockFieldContents.propertyId, args.id));
153
+ }
120
154
  }
121
155
 
122
156
  await db
@@ -10,7 +10,7 @@ import { and, eq, sql } from "drizzle-orm";
10
10
  import { z } from "zod";
11
11
  import { getDb, schema } from "../server/db/index.js";
12
12
  import { getContentDatabaseResponse } from "./_database-utils.js";
13
- import { nanoid } from "./_property-utils.js";
13
+ import { nanoid, seedDefaultBlocksField } from "./_property-utils.js";
14
14
 
15
15
  export default defineAction({
16
16
  description:
@@ -151,6 +151,10 @@ export default defineAction({
151
151
  updatedAt: now,
152
152
  });
153
153
 
154
+ // Every database is seeded with one primary "Content" Blocks field, backed
155
+ // by `documents.content`, so each row's body is a first-class property.
156
+ await seedDefaultBlocksField({ databaseId, ownerEmail, orgId, now });
157
+
154
158
  await writeAppState("refresh-signal", { ts: Date.now() });
155
159
 
156
160
  return getContentDatabaseResponse(databaseId);
@@ -1,9 +1,15 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { assertAccess } from "@agent-native/core/sharing";
4
- import { and, eq } from "drizzle-orm";
4
+ import { and, eq, inArray } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import {
8
+ isBlocksPropertyType,
9
+ isPrimaryBlocksField,
10
+ parsePropertyOptions,
11
+ type DocumentPropertyType,
12
+ } from "../shared/properties.js";
7
13
  import {
8
14
  listPropertiesForDocument,
9
15
  resolvePropertyDatabaseForDocument,
@@ -24,7 +30,7 @@ export default defineAction({
24
30
  if (!database) throw new Error("Document is not part of a database.");
25
31
 
26
32
  const [definition] = await db
27
- .select({ id: schema.documentPropertyDefinitions.id })
33
+ .select()
28
34
  .from(schema.documentPropertyDefinitions)
29
35
  .where(
30
36
  and(
@@ -38,6 +44,13 @@ export default defineAction({
38
44
  );
39
45
  if (!definition) throw new Error(`Property "${propertyId}" not found`);
40
46
 
47
+ const isBlocks = isBlocksPropertyType(
48
+ definition.type as DocumentPropertyType,
49
+ );
50
+ const isPrimaryBlocks =
51
+ isBlocks &&
52
+ isPrimaryBlocksField(parsePropertyOptions(definition.optionsJson));
53
+
41
54
  await db
42
55
  .delete(schema.documentPropertyValues)
43
56
  .where(eq(schema.documentPropertyValues.propertyId, propertyId));
@@ -45,6 +58,43 @@ export default defineAction({
45
58
  .delete(schema.documentPropertyDefinitions)
46
59
  .where(eq(schema.documentPropertyDefinitions.id, propertyId));
47
60
 
61
+ if (isBlocks) {
62
+ // Drop the independent content for this Blocks field across every row.
63
+ await db
64
+ .delete(schema.documentBlockFieldContents)
65
+ .where(eq(schema.documentBlockFieldContents.propertyId, propertyId));
66
+
67
+ // Deleting the primary "Content" field removes the body (documents.content)
68
+ // for every object of this type, per the delete warning shown in the UI.
69
+ if (isPrimaryBlocks) {
70
+ // Record that the primary was intentionally removed: clear the single
71
+ // source of truth but LEAVE blocks_seeded = 1, so neither the read path
72
+ // nor the startup repair ever recreates it. Deleting the only Blocks
73
+ // field is an allowed product action that leaves the row metadata-only
74
+ // with ZERO Blocks fields.
75
+ await db
76
+ .update(schema.contentDatabases)
77
+ .set({
78
+ primaryBlocksPropertyId: null,
79
+ updatedAt: new Date().toISOString(),
80
+ })
81
+ .where(eq(schema.contentDatabases.id, database.id));
82
+
83
+ const items = await db
84
+ .select({ documentId: schema.contentDatabaseItems.documentId })
85
+ .from(schema.contentDatabaseItems)
86
+ .where(eq(schema.contentDatabaseItems.databaseId, database.id));
87
+ const documentIds = items.map((item) => item.documentId);
88
+ if (documentIds.length > 0) {
89
+ const now = new Date().toISOString();
90
+ await db
91
+ .update(schema.documents)
92
+ .set({ content: "", updatedAt: now })
93
+ .where(inArray(schema.documents.id, documentIds));
94
+ }
95
+ }
96
+ }
97
+
48
98
  // Free any source field that was mapped to this property so it returns to
49
99
  // the "From source" picker immediately, instead of staying orphaned until
50
100
  // the next source refresh reconciles it.
@@ -4,6 +4,11 @@ import { assertAccess } from "@agent-native/core/sharing";
4
4
  import { and, eq, sql } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import {
8
+ isBlocksPropertyType,
9
+ serializePropertyOptions,
10
+ type DocumentPropertyType,
11
+ } from "../shared/properties.js";
7
12
  import {
8
13
  listPropertiesForDocument,
9
14
  nanoid,
@@ -56,6 +61,15 @@ export default defineAction({
56
61
 
57
62
  const now = new Date().toISOString();
58
63
  const newPropertyId = nanoid();
64
+ const isBlocks = isBlocksPropertyType(
65
+ definition.type as DocumentPropertyType,
66
+ );
67
+ // A duplicated Blocks field is a brand-new, independent, EMPTY field — never
68
+ // primary (only one field backs the body) and with no copied content.
69
+ const optionsJson = isBlocks
70
+ ? serializePropertyOptions({ blocks: { primary: false } })
71
+ : definition.optionsJson;
72
+
59
73
  await db.insert(schema.documentPropertyDefinitions).values({
60
74
  id: newPropertyId,
61
75
  ownerEmail: definition.ownerEmail,
@@ -64,28 +78,31 @@ export default defineAction({
64
78
  name: `${definition.name} copy`,
65
79
  type: definition.type,
66
80
  visibility: definition.visibility,
67
- optionsJson: definition.optionsJson,
81
+ optionsJson,
68
82
  position: (maxPos?.max ?? -1) + 1,
69
83
  createdAt: now,
70
84
  updatedAt: now,
71
85
  });
72
86
 
73
- const values = await db
74
- .select()
75
- .from(schema.documentPropertyValues)
76
- .where(eq(schema.documentPropertyValues.propertyId, propertyId));
77
- if (values.length > 0) {
78
- await db.insert(schema.documentPropertyValues).values(
79
- values.map((value) => ({
80
- id: nanoid(),
81
- ownerEmail: value.ownerEmail,
82
- documentId: value.documentId,
83
- propertyId: newPropertyId,
84
- valueJson: value.valueJson,
85
- createdAt: now,
86
- updatedAt: now,
87
- })),
88
- );
87
+ // Blocks fields don't use document_property_values; a duplicate starts empty.
88
+ if (!isBlocks) {
89
+ const values = await db
90
+ .select()
91
+ .from(schema.documentPropertyValues)
92
+ .where(eq(schema.documentPropertyValues.propertyId, propertyId));
93
+ if (values.length > 0) {
94
+ await db.insert(schema.documentPropertyValues).values(
95
+ values.map((value) => ({
96
+ id: nanoid(),
97
+ ownerEmail: value.ownerEmail,
98
+ documentId: value.documentId,
99
+ propertyId: newPropertyId,
100
+ valueJson: value.valueJson,
101
+ createdAt: now,
102
+ updatedAt: now,
103
+ })),
104
+ );
105
+ }
89
106
  }
90
107
 
91
108
  await writeAppState("refresh-signal", { ts: Date.now() });