@agent-native/core 0.120.2 → 0.120.4

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 (278) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/context-xray-local.ts +1 -1
  5. package/corpus/core/src/cli/index.ts +63 -18
  6. package/corpus/core/src/cli/skills.ts +5 -0
  7. package/corpus/core/src/cli/telemetry.ts +118 -1
  8. package/corpus/core/src/client/AgentPanel.tsx +10 -0
  9. package/corpus/core/src/client/CommandMenu.tsx +13 -1
  10. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +39 -4
  11. package/corpus/core/src/client/analytics.ts +13 -0
  12. package/corpus/core/src/client/error-capture.ts +6 -1
  13. package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +12 -5
  14. package/corpus/core/src/client/extensions/agent-native-extension-runtime.ts +1 -1
  15. package/corpus/core/src/client/navigation/index.ts +2 -0
  16. package/corpus/core/src/client/org/OrgSwitcher.tsx +14 -32
  17. package/corpus/core/src/client/settings/ExtensionsSettingsContent.tsx +1 -1
  18. package/corpus/core/src/extensions/theme.ts +1 -1
  19. package/corpus/core/src/localization/default-messages.ts +2 -0
  20. package/corpus/core/src/server/core-routes-plugin.ts +10 -1
  21. package/corpus/core/src/server/framework-request-handler.ts +17 -26
  22. package/corpus/core/src/styles/agent-native.css +13 -0
  23. package/corpus/core/src/templates/chat/app/components/layout/Layout.tsx +3 -0
  24. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +78 -45
  25. package/corpus/core/src/templates/chat/app/global.css +1 -1
  26. package/corpus/core/src/templates/default/app/global.css +1 -1
  27. package/corpus/core/src/tracking/error-capture.ts +151 -0
  28. package/corpus/core/src/tracking/index.ts +5 -0
  29. package/corpus/templates/analytics/app/components/dashboard/CumulativeNetChart.tsx +1 -1
  30. package/corpus/templates/analytics/app/components/dashboard/DashboardHistoryPanel.tsx +1 -4
  31. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +1 -1
  32. package/corpus/templates/analytics/app/components/dashboard/DataTable.tsx +1 -1
  33. package/corpus/templates/analytics/app/components/dashboard/DateRangePicker.tsx +2 -2
  34. package/corpus/templates/analytics/app/components/dashboard/MetricCard.tsx +1 -1
  35. package/corpus/templates/analytics/app/components/dashboard/RecentActivity.tsx +1 -1
  36. package/corpus/templates/analytics/app/components/dashboard/RevenueChart.tsx +1 -1
  37. package/corpus/templates/analytics/app/components/dashboard/RevenueComparisonChart.tsx +1 -1
  38. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +41 -9
  39. package/corpus/templates/analytics/app/components/dashboard/StatsCard.tsx +1 -1
  40. package/corpus/templates/analytics/app/components/dashboard/TimeSeriesChart.tsx +1 -1
  41. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +2 -0
  42. package/corpus/templates/analytics/app/components/layout/Layout.tsx +28 -14
  43. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +152 -124
  44. package/corpus/templates/analytics/app/global.css +5 -5
  45. package/corpus/templates/analytics/app/lib/dashboard-report-capture.ts +42 -0
  46. package/corpus/templates/analytics/app/pages/adhoc/_shared/KpiChart.tsx +1 -1
  47. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/ChartTypePicker.tsx +1 -1
  48. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/DateRangePicker.tsx +1 -1
  49. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/EventRow.tsx +1 -1
  50. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/SqlPreview.tsx +2 -2
  51. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +3 -3
  52. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +2 -2
  53. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  54. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +1 -1
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +162 -51
  56. package/corpus/templates/analytics/app/root.tsx +28 -0
  57. package/corpus/templates/analytics/changelog/2026-07-24-analytics-charts-now-explain-slow-loads-and-offer-a-direct-r.md +6 -0
  58. package/corpus/templates/analytics/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  59. package/corpus/templates/analytics/changelog/2026-07-24-daily-dashboard-emails-now-load-signed-reports-more-reliably.md +6 -0
  60. package/corpus/templates/analytics/changelog/2026-07-24-manage-agent-is-clearly-marked-as-a-link-to-its-dedicated-pa.md +6 -0
  61. package/corpus/templates/analytics/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  62. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -1
  63. package/corpus/templates/analytics/server/db/schema.ts +5 -0
  64. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +21 -0
  65. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +11 -0
  66. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +54 -0
  67. package/corpus/templates/analytics/server/lib/dashboard-report.ts +235 -26
  68. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +4 -1
  69. package/corpus/templates/analytics/server/plugins/db.ts +9 -0
  70. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +1 -1
  71. package/corpus/templates/assets/app/components/asset/AssetPreviewDialog.tsx +376 -0
  72. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +14 -4
  73. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  74. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +118 -74
  75. package/corpus/templates/assets/app/global.css +1 -1
  76. package/corpus/templates/assets/app/i18n-data.ts +10 -0
  77. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +99 -10
  78. package/corpus/templates/assets/app/routes/library.tsx +21 -198
  79. package/corpus/templates/assets/changelog/2026-07-21-unified-asset-preview-click-any-asset-to-open-one-preview-wi.md +6 -0
  80. package/corpus/templates/assets/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  81. package/corpus/templates/assets/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  82. package/corpus/templates/brain/app/components/layout/Layout.tsx +3 -0
  83. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +76 -43
  84. package/corpus/templates/brain/app/global.css +1 -1
  85. package/corpus/templates/brain/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  86. package/corpus/templates/brain/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  87. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +67 -52
  88. package/corpus/templates/calendar/app/global.css +1 -1
  89. package/corpus/templates/calendar/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  90. package/corpus/templates/calendar/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  91. package/corpus/templates/chat/app/components/layout/Layout.tsx +3 -0
  92. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +78 -45
  93. package/corpus/templates/chat/app/global.css +1 -1
  94. package/corpus/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  95. package/corpus/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  96. package/corpus/templates/clips/app/components/library/library-layout.tsx +65 -31
  97. package/corpus/templates/clips/app/components/library/organization-switcher.tsx +2 -2
  98. package/corpus/templates/clips/app/components/player/video-player.tsx +16 -1
  99. package/corpus/templates/clips/app/global.css +1 -1
  100. package/corpus/templates/clips/app/hooks/use-view-tracking.ts +55 -12
  101. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +6 -5
  102. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -7
  103. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -5
  104. package/corpus/templates/clips/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  105. package/corpus/templates/clips/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  106. package/corpus/templates/clips/server/plugins/db.ts +113 -68
  107. package/corpus/templates/content/actions/update-document.ts +145 -91
  108. package/corpus/templates/content/app/components/editor/CommentComposer.tsx +4 -1
  109. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +366 -164
  110. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +51 -28
  111. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +19 -8
  112. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +66 -42
  113. package/corpus/templates/content/app/global.css +1 -1
  114. package/corpus/templates/content/app/i18n-data.ts +15 -0
  115. package/corpus/templates/content/changelog/2026-07-23-shared-pages-now-keep-durable-content-visible-and-comment-th.md +6 -0
  116. package/corpus/templates/content/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  117. package/corpus/templates/content/changelog/2026-07-24-comment-drafts-now-stay-open-when-saving-fails-and-long-live.md +6 -0
  118. package/corpus/templates/content/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  119. package/corpus/templates/design/actions/update-file.ts +85 -18
  120. package/corpus/templates/design/app/components/layout/Sidebar.tsx +65 -30
  121. package/corpus/templates/design/app/entry.client.tsx +14 -1
  122. package/corpus/templates/design/app/global.css +1 -1
  123. package/corpus/templates/design/app/lib/design-save-outbox.ts +39 -0
  124. package/corpus/templates/design/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  125. package/corpus/templates/design/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  126. package/corpus/templates/dispatch/app/global.css +1 -1
  127. package/corpus/templates/dispatch/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  128. package/corpus/templates/dispatch/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  129. package/corpus/templates/forms/app/components/layout/Layout.tsx +3 -0
  130. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +72 -37
  131. package/corpus/templates/forms/app/global.css +1 -1
  132. package/corpus/templates/forms/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  133. package/corpus/templates/forms/changelog/2026-07-24-forms-sidebar-actions-now-keep-search-language-feedback-and-.md +6 -0
  134. package/corpus/templates/forms/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  135. package/corpus/templates/macros/app/components/DailyProgress.tsx +5 -5
  136. package/corpus/templates/macros/app/components/ExerciseCard.tsx +2 -2
  137. package/corpus/templates/macros/app/components/MealCard.tsx +2 -2
  138. package/corpus/templates/macros/app/components/WeightCard.tsx +2 -2
  139. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +56 -29
  140. package/corpus/templates/macros/app/components/ui/button.tsx +1 -2
  141. package/corpus/templates/macros/app/components/ui/card.tsx +1 -1
  142. package/corpus/templates/macros/app/global.css +1 -1
  143. package/corpus/templates/macros/app/routes/_index.tsx +1 -1
  144. package/corpus/templates/macros/app/routes/analytics.tsx +4 -7
  145. package/corpus/templates/macros/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  146. package/corpus/templates/macros/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  147. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +125 -37
  148. package/corpus/templates/mail/app/global.css +1 -1
  149. package/corpus/templates/mail/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  150. package/corpus/templates/mail/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  151. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -0
  152. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +90 -60
  153. package/corpus/templates/plan/app/global.css +1 -1
  154. package/corpus/templates/plan/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  155. package/corpus/templates/plan/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  156. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +69 -32
  157. package/corpus/templates/slides/app/global.css +1 -1
  158. package/corpus/templates/slides/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  159. package/corpus/templates/slides/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  160. package/corpus/templates/tasks/app/global.css +1 -1
  161. package/corpus/templates/tasks/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  162. package/corpus/toolkit/CHANGELOG.md +14 -0
  163. package/corpus/toolkit/README.md +8 -1
  164. package/corpus/toolkit/agent-native.eject.json +1 -1
  165. package/corpus/toolkit/package.json +1 -1
  166. package/corpus/toolkit/src/app-shell/index.ts +1 -0
  167. package/corpus/toolkit/src/app-shell/sidebar-footer-actions.tsx +48 -0
  168. package/corpus/toolkit/src/chat-history.css +52 -1
  169. package/corpus/toolkit/src/design-system/default-adapter.tsx +1 -1
  170. package/corpus/toolkit/src/editor/useCollabReconcile.ts +83 -15
  171. package/corpus/toolkit/src/ui/button.tsx +1 -2
  172. package/corpus/toolkit/src/ui/card.tsx +1 -1
  173. package/corpus/toolkit/src/ui/table.tsx +4 -11
  174. package/dist/cli/context-xray-local.js +1 -1
  175. package/dist/cli/context-xray-local.js.map +1 -1
  176. package/dist/cli/index.js +56 -17
  177. package/dist/cli/index.js.map +1 -1
  178. package/dist/cli/skills.d.ts.map +1 -1
  179. package/dist/cli/skills.js +5 -0
  180. package/dist/cli/skills.js.map +1 -1
  181. package/dist/cli/telemetry.d.ts +7 -0
  182. package/dist/cli/telemetry.d.ts.map +1 -1
  183. package/dist/cli/telemetry.js +91 -1
  184. package/dist/cli/telemetry.js.map +1 -1
  185. package/dist/client/AgentPanel.d.ts +7 -1
  186. package/dist/client/AgentPanel.d.ts.map +1 -1
  187. package/dist/client/AgentPanel.js +2 -2
  188. package/dist/client/AgentPanel.js.map +1 -1
  189. package/dist/client/CommandMenu.d.ts +2 -0
  190. package/dist/client/CommandMenu.d.ts.map +1 -1
  191. package/dist/client/CommandMenu.js +12 -1
  192. package/dist/client/CommandMenu.js.map +1 -1
  193. package/dist/client/agent-page/AgentTabsPage.d.ts +4 -0
  194. package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
  195. package/dist/client/agent-page/AgentTabsPage.js +8 -4
  196. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  197. package/dist/client/analytics.d.ts.map +1 -1
  198. package/dist/client/analytics.js +7 -0
  199. package/dist/client/analytics.js.map +1 -1
  200. package/dist/client/error-capture.d.ts.map +1 -1
  201. package/dist/client/error-capture.js +8 -1
  202. package/dist/client/error-capture.js.map +1 -1
  203. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  204. package/dist/client/extensions/ExtensionsListPage.js +2 -2
  205. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  206. package/dist/client/extensions/agent-native-extension-runtime.js +1 -1
  207. package/dist/client/extensions/agent-native-extension-runtime.js.map +1 -1
  208. package/dist/client/navigation/index.d.ts +1 -1
  209. package/dist/client/navigation/index.d.ts.map +1 -1
  210. package/dist/client/navigation/index.js +1 -1
  211. package/dist/client/navigation/index.js.map +1 -1
  212. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  213. package/dist/client/org/OrgSwitcher.js +9 -13
  214. package/dist/client/org/OrgSwitcher.js.map +1 -1
  215. package/dist/client/settings/ExtensionsSettingsContent.js +1 -1
  216. package/dist/client/settings/ExtensionsSettingsContent.js.map +1 -1
  217. package/dist/collab/awareness.d.ts +2 -2
  218. package/dist/collab/awareness.d.ts.map +1 -1
  219. package/dist/collab/struct-routes.d.ts +1 -1
  220. package/dist/extensions/theme.js +1 -1
  221. package/dist/extensions/theme.js.map +1 -1
  222. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  223. package/dist/localization/default-messages.d.ts +2 -0
  224. package/dist/localization/default-messages.d.ts.map +1 -1
  225. package/dist/localization/default-messages.js +2 -0
  226. package/dist/localization/default-messages.js.map +1 -1
  227. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  228. package/dist/secrets/routes.d.ts +9 -9
  229. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  230. package/dist/server/core-routes-plugin.js +8 -1
  231. package/dist/server/core-routes-plugin.js.map +1 -1
  232. package/dist/server/framework-request-handler.d.ts.map +1 -1
  233. package/dist/server/framework-request-handler.js +17 -27
  234. package/dist/server/framework-request-handler.js.map +1 -1
  235. package/dist/server/realtime-token.d.ts +1 -1
  236. package/dist/styles/agent-native.css +13 -0
  237. package/dist/templates/chat/app/components/layout/Layout.tsx +3 -0
  238. package/dist/templates/chat/app/components/layout/Sidebar.tsx +78 -45
  239. package/dist/templates/chat/app/global.css +1 -1
  240. package/dist/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  241. package/dist/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  242. package/dist/templates/default/app/global.css +1 -1
  243. package/dist/tracking/error-capture.d.ts +14 -0
  244. package/dist/tracking/error-capture.d.ts.map +1 -0
  245. package/dist/tracking/error-capture.js +113 -0
  246. package/dist/tracking/error-capture.js.map +1 -0
  247. package/dist/tracking/index.d.ts +1 -0
  248. package/dist/tracking/index.d.ts.map +1 -1
  249. package/dist/tracking/index.js +1 -0
  250. package/dist/tracking/index.js.map +1 -1
  251. package/package.json +2 -2
  252. package/src/cli/context-xray-local.ts +1 -1
  253. package/src/cli/index.ts +63 -18
  254. package/src/cli/skills.ts +5 -0
  255. package/src/cli/telemetry.ts +118 -1
  256. package/src/client/AgentPanel.tsx +10 -0
  257. package/src/client/CommandMenu.tsx +13 -1
  258. package/src/client/agent-page/AgentTabsPage.tsx +39 -4
  259. package/src/client/analytics.ts +13 -0
  260. package/src/client/error-capture.ts +6 -1
  261. package/src/client/extensions/ExtensionsListPage.tsx +12 -5
  262. package/src/client/extensions/agent-native-extension-runtime.ts +1 -1
  263. package/src/client/navigation/index.ts +2 -0
  264. package/src/client/org/OrgSwitcher.tsx +14 -32
  265. package/src/client/settings/ExtensionsSettingsContent.tsx +1 -1
  266. package/src/extensions/theme.ts +1 -1
  267. package/src/localization/default-messages.ts +2 -0
  268. package/src/server/core-routes-plugin.ts +10 -1
  269. package/src/server/framework-request-handler.ts +17 -26
  270. package/src/styles/agent-native.css +13 -0
  271. package/src/templates/chat/app/components/layout/Layout.tsx +3 -0
  272. package/src/templates/chat/app/components/layout/Sidebar.tsx +78 -45
  273. package/src/templates/chat/app/global.css +1 -1
  274. package/src/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  275. package/src/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  276. package/src/templates/default/app/global.css +1 -1
  277. package/src/tracking/error-capture.ts +151 -0
  278. package/src/tracking/index.ts +5 -0
@@ -644,16 +644,8 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
644
644
  }
645
645
  : undefined;
646
646
 
647
- // Live collaboration for everyone who can open the doc — editors and viewers
648
- // alike. Viewers join the shared Y.Doc read-only: they see live keystrokes,
649
- // cursors, and presence (Google-Docs style) instead of a lagging SQL snapshot.
650
- // The server enforces the split — collab READ routes (state / awareness GET /
651
- // users) require viewer access, WRITE routes (update) require editor — so a
652
- // viewer's client can subscribe but never push. The editor stays non-editable
653
- // for viewers (see `editable={canEdit}` below), and VisualEditor additionally
654
- // neutralizes every local Y.Doc mutation for viewers (no seed, no reconcile
655
- // apply) so a read-only client can never originate a rejected `/update` POST.
656
- // Local-file documents are still excluded (they have no SQL-backed collab doc).
647
+ // All SQL-backed readers subscribe for presence. Only editors bind the body
648
+ // to Yjs; viewers render canonical SQL so missing collab state cannot hide it.
657
649
  const collabEnabled = !isLocalFileDocument;
658
650
  const {
659
651
  ydoc,
@@ -671,6 +663,12 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
671
663
  const bodyHydrationPending = documentBodyHydrationIsPending(document);
672
664
  const editorCanEdit =
673
665
  canEdit && !bodyHydrationPending && (isLocalFileDocument || !collabLoading);
666
+ // Bind an editor's stable Y.Doc on its first mount, even while the initial
667
+ // state is loading. Editability remains gated by `editorCanEdit`, and the
668
+ // reconcile hook remains gated by `collabSynced`; keeping the Y.Doc binding
669
+ // stable avoids a snapshot -> collab remount that can seed the same SQL body
670
+ // beside freshly projected persisted CRDT content.
671
+ const collabEditorEnabled = collabEnabled && canEdit && !bodyHydrationPending;
674
672
  canEditRef.current = editorCanEdit;
675
673
 
676
674
  // Viewers intentionally join awareness so they receive live cursors, but
@@ -900,6 +898,12 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
900
898
  },
901
899
  [document, documentId, queryClient, updateDocument],
902
900
  );
901
+ // The document query can refresh its object identity without changing the
902
+ // flush request itself. Keep the latest save function behind a ref so those
903
+ // routine refreshes do not restart the one-shot flush reader and flood the
904
+ // browser with duplicate application-state requests.
905
+ const persistDocumentUpdatesRef = useRef(persistDocumentUpdates);
906
+ persistDocumentUpdatesRef.current = persistDocumentUpdates;
903
907
 
904
908
  const saveDocumentImmediately = useCallback(
905
909
  async (
@@ -1216,7 +1220,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1216
1220
  }
1217
1221
  try {
1218
1222
  if (Object.keys(updates).length > 0) {
1219
- const saved = await persistDocumentUpdates(updates);
1223
+ const saved = await persistDocumentUpdatesRef.current(updates);
1220
1224
  if (isDocumentUpdateConflict(saved)) {
1221
1225
  // Do not acknowledge a CAS loss as a successful flush. The
1222
1226
  // requester must stop instead of pushing/replacing stale SQL.
@@ -1290,7 +1294,6 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1290
1294
  flushRequestKey,
1291
1295
  flushRequestWake,
1292
1296
  isLocalFileDocument,
1293
- persistDocumentUpdates,
1294
1297
  t,
1295
1298
  ]);
1296
1299
 
@@ -1372,6 +1375,24 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1372
1375
  setHoveredThreadId(null);
1373
1376
  }, []);
1374
1377
 
1378
+ const activateCommentThread = useCallback((threadId: string) => {
1379
+ setPendingComment(null);
1380
+ setHoveredThreadId(null);
1381
+ setSelectedThreadId(threadId);
1382
+ setUtilityPanel("comments");
1383
+ }, []);
1384
+
1385
+ const handleUtilityPanelChange = useCallback(
1386
+ (nextPanel: DocumentUtilityPanel) => {
1387
+ setUtilityPanel(nextPanel);
1388
+ if (nextPanel !== "comments") {
1389
+ setPendingComment(null);
1390
+ clearCommentFocus();
1391
+ }
1392
+ },
1393
+ [clearCommentFocus],
1394
+ );
1395
+
1375
1396
  useEffect(() => {
1376
1397
  setPendingComment(null);
1377
1398
  setUtilityPanel(null);
@@ -1512,9 +1533,11 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1512
1533
  scrollContainerRef={scrollContainerRef}
1513
1534
  activeThreadId={activeThreadId}
1514
1535
  selectedThreadId={selectedThreadId}
1536
+ onActivateThread={activateCommentThread}
1515
1537
  onSelectedThreadChange={setSelectedThreadId}
1516
1538
  onHoveredThreadChange={setHoveredThreadId}
1517
1539
  currentUserEmail={session?.email}
1540
+ alignToAnchors={hasUtilityRailSpace}
1518
1541
  forceVisible
1519
1542
  />
1520
1543
  );
@@ -1566,7 +1589,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1566
1589
  type="button"
1567
1590
  className="ms-auto flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1568
1591
  aria-label={t("editor.toolbar.closeUtilityPanel")}
1569
- onClick={() => setUtilityPanel(null)}
1592
+ onClick={() => handleUtilityPanelChange(null)}
1570
1593
  >
1571
1594
  <IconX size={16} />
1572
1595
  </button>
@@ -1594,7 +1617,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1594
1617
  <div
1595
1618
  className="relative flex min-h-0 min-w-0 flex-1"
1596
1619
  data-document-print-root
1597
- onPointerDownCapture={(event) => {
1620
+ onClickCapture={(event) => {
1598
1621
  const target = event.target as HTMLElement | null;
1599
1622
  if (
1600
1623
  target?.closest("[data-comments-sidebar], [data-comment-thread]")
@@ -1626,7 +1649,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1626
1649
  }
1627
1650
  onDelete={handleDeleteDocument}
1628
1651
  utilityPanel={utilityPanel}
1629
- onUtilityPanelChange={setUtilityPanel}
1652
+ onUtilityPanelChange={handleUtilityPanelChange}
1630
1653
  showCommentsControl={editorCanEdit && !isLocalFileDocument}
1631
1654
  onOpenBreadcrumbItem={handleOpenToolbarBreadcrumb}
1632
1655
  />
@@ -1817,7 +1840,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1817
1840
  // fields.
1818
1841
  const primaryEditor = (
1819
1842
  <VisualEditor
1820
- key={`${documentId}:${editorCanEdit && !isLocalFileDocument ? "live" : "snapshot"}`}
1843
+ key={`${documentId}:${collabEditorEnabled && ydoc ? "live-ready" : canEdit && !isLocalFileDocument ? "live-pending" : `snapshot:${document.updatedAt}`}`}
1821
1844
  documentId={documentId}
1822
1845
  content={
1823
1846
  isLocalFileDocument
@@ -1831,13 +1854,11 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1831
1854
  }
1832
1855
  onChange={handleContentChange}
1833
1856
  onSaveContent={handleContentSaveNow}
1834
- // Bind the shared Y.Doc/awareness for viewers too — the
1835
- // editor is non-editable for them and VisualEditor blocks
1836
- // any local Y.Doc mutation, so they get live edits +
1837
- // cursors without ever writing. Excludes local-file docs.
1838
- ydoc={collabEnabled ? ydoc : null}
1839
- collabSynced={collabEnabled ? collabSynced : true}
1840
- awareness={collabEnabled ? awareness : null}
1857
+ ydoc={collabEditorEnabled ? ydoc : null}
1858
+ collabSynced={
1859
+ collabEditorEnabled ? collabSynced : true
1860
+ }
1861
+ awareness={collabEditorEnabled ? awareness : null}
1841
1862
  user={currentUser}
1842
1863
  editable={editorCanEdit}
1843
1864
  localFileMode={isLocalFileDocument}
@@ -1854,7 +1875,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1854
1875
  pendingHighlight={pendingComment?.range ?? null}
1855
1876
  onActivateThread={
1856
1877
  editorCanEdit && !isLocalFileDocument
1857
- ? setSelectedThreadId
1878
+ ? activateCommentThread
1858
1879
  : undefined
1859
1880
  }
1860
1881
  onJoinTitle={joinFirstBodyBlockToTitle}
@@ -1883,6 +1904,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1883
1904
  })()}
1884
1905
  {!bodyHydrationPending &&
1885
1906
  !isLocalFileDocument &&
1907
+ canEdit &&
1886
1908
  collabLoading ? (
1887
1909
  <div
1888
1910
  className="mt-4 inline-flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground"
@@ -1910,20 +1932,21 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1910
1932
  open={utilityPanel !== null}
1911
1933
  onOpenChange={(open) => {
1912
1934
  if (!open) {
1913
- setUtilityPanel(null);
1914
- setPendingComment(null);
1935
+ handleUtilityPanelChange(null);
1915
1936
  }
1916
1937
  }}
1917
1938
  >
1918
1939
  <SheetContent
1919
1940
  side="right"
1920
- className="w-[85vw] max-w-sm p-0"
1941
+ className="flex min-h-0 w-[85vw] max-w-sm flex-col overflow-hidden p-0"
1921
1942
  aria-describedby={undefined}
1922
1943
  >
1923
1944
  <SheetHeader className="sr-only">
1924
1945
  <SheetTitle>{utilityPanelTitle}</SheetTitle>
1925
1946
  </SheetHeader>
1926
- {utilityPanelContent}
1947
+ <div className="min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto">
1948
+ {utilityPanelContent}
1949
+ </div>
1927
1950
  </SheetContent>
1928
1951
  </Sheet>
1929
1952
  ) : null}
@@ -136,8 +136,14 @@ export function ContentFilesSidebarView({
136
136
  | "scroll"
137
137
  >;
138
138
  }) {
139
+ const usableData =
140
+ data?.database &&
141
+ Array.isArray(data.items) &&
142
+ Array.isArray(data.properties)
143
+ ? data
144
+ : undefined;
139
145
  const viewConfig = applyPersonalSidebarViewOverrides(
140
- data?.database.viewConfig ?? defaultDatabaseViewConfig(),
146
+ usableData?.database.viewConfig ?? defaultDatabaseViewConfig(),
141
147
  overrides,
142
148
  );
143
149
  const [selectedViewId, setSelectedViewId] = useState(
@@ -154,10 +160,10 @@ export function ContentFilesSidebarView({
154
160
  useEffect(() => {
155
161
  setConstraintsCleared(false);
156
162
  }, [activeFilterKey, activeView.id]);
157
- const items = data
163
+ const items = usableData
158
164
  ? applyDatabaseView(
159
- data.items,
160
- data.properties,
165
+ usableData.items,
166
+ usableData.properties,
161
167
  "",
162
168
  constraintsCleared ? [] : activeView.filters,
163
169
  activeView.sorts,
@@ -167,16 +173,18 @@ export function ContentFilesSidebarView({
167
173
  const groups = databaseVisibleGroups(
168
174
  databaseViewItemGroups(
169
175
  items,
170
- data?.properties ?? [],
176
+ usableData?.properties ?? [],
171
177
  activeView.groupByPropertyId,
172
178
  ),
173
179
  activeView.hideEmptyGroups === true,
174
180
  );
175
- const hasFilesHierarchy = data?.properties.some(
181
+ const hasFilesHierarchy = usableData?.properties.some(
176
182
  (property) => property.definition.systemRole === "files_parent",
177
183
  );
178
184
  const hierarchyItems = hasFilesHierarchy ? items : undefined;
179
- const hierarchyUniverseItems = hasFilesHierarchy ? data?.items : undefined;
185
+ const hierarchyUniverseItems = hasFilesHierarchy
186
+ ? usableData?.items
187
+ : undefined;
180
188
  return (
181
189
  <div className="min-w-0">
182
190
  {viewConfig.views.length > 1 && (
@@ -204,7 +212,10 @@ export function ContentFilesSidebarView({
204
212
  {...labels}
205
213
  groups={groups}
206
214
  grouped={
207
- !!databaseViewGroupingProperty(activeView, data?.properties ?? [])
215
+ !!databaseViewGroupingProperty(
216
+ activeView,
217
+ usableData?.properties ?? [],
218
+ )
208
219
  }
209
220
  isLoading={isLoading}
210
221
  hasActiveConstraints={
@@ -7,9 +7,10 @@ import {
7
7
  useActionMutation,
8
8
  useActionQuery,
9
9
  } from "@agent-native/core/client/hooks";
10
- import { useT } from "@agent-native/core/client/i18n";
10
+ import { LanguagePicker, useT } from "@agent-native/core/client/i18n";
11
11
  import { OrgSwitcher } from "@agent-native/core/client/org";
12
12
  import { FeedbackButton } from "@agent-native/core/client/ui";
13
+ import { SidebarFooterActions } from "@agent-native/toolkit/app-shell";
13
14
  import {
14
15
  closestCenter,
15
16
  DndContext,
@@ -1278,6 +1279,53 @@ export function DocumentSidebar({
1278
1279
  </Link>
1279
1280
  );
1280
1281
 
1282
+ const collapseButton = (
1283
+ <Tooltip>
1284
+ <TooltipTrigger asChild>
1285
+ <button
1286
+ type="button"
1287
+ aria-label={collapsed ? t("sidebar.expand") : t("sidebar.collapse")}
1288
+ className="flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
1289
+ onClick={onToggleCollapsed}
1290
+ >
1291
+ {collapsed ? (
1292
+ <IconLayoutSidebarLeftExpand size={16} />
1293
+ ) : (
1294
+ <IconLayoutSidebarLeftCollapse size={16} />
1295
+ )}
1296
+ </button>
1297
+ </TooltipTrigger>
1298
+ <TooltipContent side="right">
1299
+ {collapsed ? t("sidebar.expand") : t("sidebar.collapse")}
1300
+ </TooltipContent>
1301
+ </Tooltip>
1302
+ );
1303
+ const searchButton = (
1304
+ <Tooltip>
1305
+ <TooltipTrigger asChild>
1306
+ <button
1307
+ type="button"
1308
+ aria-label={t("sidebar.search")}
1309
+ className="flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
1310
+ onClick={() => setIsSearching((value) => !value)}
1311
+ >
1312
+ <IconSearch size={16} />
1313
+ </button>
1314
+ </TooltipTrigger>
1315
+ <TooltipContent side="right">{t("sidebar.search")}</TooltipContent>
1316
+ </Tooltip>
1317
+ );
1318
+ const translateButton = (
1319
+ <LanguagePicker variant="ghost-icon" label={t("settings.languageLabel")} />
1320
+ );
1321
+ const feedbackButton = (
1322
+ <FeedbackButton
1323
+ variant={collapsed ? "icon" : "sidebar"}
1324
+ side="right"
1325
+ className={collapsed ? "size-8" : "h-8 min-w-0"}
1326
+ />
1327
+ );
1328
+
1281
1329
  const renderAgentNavButton = () => (
1282
1330
  <Link
1283
1331
  to="/agent"
@@ -1832,17 +1880,6 @@ export function DocumentSidebar({
1832
1880
  if (collapsed) {
1833
1881
  return (
1834
1882
  <div className="agent-layout-left-drawer flex h-full w-12 flex-col items-center gap-1 border-e border-border bg-sidebar py-3 transition-[width] duration-200 ease-out">
1835
- <Tooltip>
1836
- <TooltipTrigger asChild>
1837
- <button
1838
- className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-accent text-muted-foreground hover:text-foreground"
1839
- onClick={onToggleCollapsed}
1840
- >
1841
- <IconLayoutSidebarLeftExpand size={18} />
1842
- </button>
1843
- </TooltipTrigger>
1844
- <TooltipContent>{t("sidebar.expand")}</TooltipContent>
1845
- </Tooltip>
1846
1883
  {renderCollapsedNewButton()}
1847
1884
  <Tooltip>
1848
1885
  <TooltipTrigger asChild>
@@ -1876,6 +1913,13 @@ export function DocumentSidebar({
1876
1913
  </TooltipTrigger>
1877
1914
  <TooltipContent>{t("navigation.settings")}</TooltipContent>
1878
1915
  </Tooltip>
1916
+ <SidebarFooterActions
1917
+ collapsed
1918
+ feedback={feedbackButton}
1919
+ translate={translateButton}
1920
+ search={searchButton}
1921
+ collapse={collapseButton}
1922
+ />
1879
1923
  </div>
1880
1924
  );
1881
1925
  }
@@ -1908,30 +1952,6 @@ export function DocumentSidebar({
1908
1952
  Content
1909
1953
  </span>
1910
1954
  </div>
1911
- <div className="flex items-center gap-0.5">
1912
- <Tooltip>
1913
- <TooltipTrigger asChild>
1914
- <button
1915
- className="w-9 h-9 flex items-center justify-center rounded-lg hover:bg-accent text-muted-foreground hover:text-foreground"
1916
- onClick={() => setIsSearching(!isSearching)}
1917
- >
1918
- <IconSearch size={16} />
1919
- </button>
1920
- </TooltipTrigger>
1921
- <TooltipContent>{t("sidebar.search")}</TooltipContent>
1922
- </Tooltip>
1923
- <Tooltip>
1924
- <TooltipTrigger asChild>
1925
- <button
1926
- className="w-9 h-9 flex items-center justify-center rounded-lg hover:bg-accent text-muted-foreground hover:text-foreground"
1927
- onClick={onToggleCollapsed}
1928
- >
1929
- <IconLayoutSidebarLeftCollapse size={16} />
1930
- </button>
1931
- </TooltipTrigger>
1932
- <TooltipContent>{t("sidebar.collapse")}</TooltipContent>
1933
- </Tooltip>
1934
- </div>
1935
1955
  </div>
1936
1956
 
1937
1957
  {/* Search */}
@@ -2098,13 +2118,17 @@ export function DocumentSidebar({
2098
2118
  {/* Footer */}
2099
2119
  <div className="shrink-0 space-y-2 px-3 py-2">
2100
2120
  {isCodeMode ? <DevDatabaseLink /> : null}
2101
- <div className="flex items-center justify-end gap-1">
2102
- <FeedbackButton className="h-8 min-w-0 flex-1 gap-2 rounded-md px-2 py-0" />
2103
- <div className="flex shrink-0 items-center gap-0.5">
2104
- <NotionButton />
2105
- <ThemeToggle />
2106
- </div>
2121
+ <div className="flex justify-end gap-0.5">
2122
+ <NotionButton />
2123
+ <ThemeToggle />
2107
2124
  </div>
2125
+ <SidebarFooterActions
2126
+ feedback={feedbackButton}
2127
+ translate={translateButton}
2128
+ search={searchButton}
2129
+ collapse={collapseButton}
2130
+ className="px-0 py-0"
2131
+ />
2108
2132
  </div>
2109
2133
 
2110
2134
  {/* Resize handle */}
@@ -80,7 +80,7 @@
80
80
  --accent: 0 0% 18%;
81
81
  --accent-foreground: 0 0% 90%;
82
82
 
83
- --destructive: 0 84% 60%;
83
+ --destructive: 0 91% 71%;
84
84
  --destructive-foreground: 0 0% 100%;
85
85
 
86
86
  --border: 0 0% 24%;
@@ -3085,6 +3085,7 @@ const enUS = {
3085
3085
  askAi: "Ask AI",
3086
3086
  resolve: "Resolve",
3087
3087
  resolved: "Resolved ({{count}})",
3088
+ unanchored: "Highlight unavailable",
3088
3089
  reply: "Reply...",
3089
3090
  reopen: "Reopen",
3090
3091
  agentRegardingText: 'Regarding this text: "{{text}}"',
@@ -8761,6 +8762,19 @@ const contentReferenceMessagesByLocale = {
8761
8762
  },
8762
8763
  } satisfies Partial<Record<LocaleCode, typeof enUS.editor.reference>>;
8763
8764
 
8765
+ const commentMessagesByLocale = {
8766
+ "zh-CN": { unanchored: "高亮不可用" },
8767
+ "zh-TW": { unanchored: "無法使用醒目提示" },
8768
+ "es-ES": { unanchored: "Resaltado no disponible" },
8769
+ "fr-FR": { unanchored: "Surlignage indisponible" },
8770
+ "de-DE": { unanchored: "Hervorhebung nicht verfügbar" },
8771
+ "ja-JP": { unanchored: "ハイライトを利用できません" },
8772
+ "ko-KR": { unanchored: "강조 표시를 사용할 수 없음" },
8773
+ "pt-BR": { unanchored: "Destaque indisponível" },
8774
+ "hi-IN": { unanchored: "हाइलाइट उपलब्ध नहीं है" },
8775
+ "ar-SA": { unanchored: "التمييز غير متاح" },
8776
+ } satisfies Partial<Record<LocaleCode, Partial<typeof enUS.comments>>>;
8777
+
8764
8778
  function mergeMessages(overrides: PartialMessages): Messages {
8765
8779
  return {
8766
8780
  root: { ...enUS.root, ...overrides.root },
@@ -8862,6 +8876,7 @@ function mergeMessagesForLocale(
8862
8876
  });
8863
8877
  return {
8864
8878
  ...base,
8879
+ comments: { ...base.comments, ...commentMessagesByLocale[locale] },
8865
8880
  root: { ...base.root, ...rawLiteralOverrides.root },
8866
8881
  team: { ...base.team, ...rawLiteralOverrides.team },
8867
8882
  settings: { ...base.settings, ...rawLiteralOverrides.settings },
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Shared pages now keep durable content visible, and comment threads stay stable and connected while you navigate.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Secondary controls and dashboard surfaces now use quieter borderless styling.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Comment drafts now stay open when saving fails, and long-lived document sessions no longer flood the browser with sync requests.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Sidebar utility controls now follow a consistent footer order.
@@ -5,7 +5,7 @@ import {
5
5
  applyText,
6
6
  seedFromText,
7
7
  } from "@agent-native/core/collab";
8
- import { isPostgres } from "@agent-native/core/db";
8
+ import { getDbExec, isPostgres } from "@agent-native/core/db";
9
9
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
10
10
  import { and, eq, isNull, sql } from "drizzle-orm";
11
11
  import { z } from "zod";
@@ -16,6 +16,30 @@ import { assertDesignHtmlEditIntegrity } from "../shared/html-integrity.js";
16
16
  import { assertLockedLayersPreserved } from "../shared/locked-layers.js";
17
17
  import { sourceContentHash } from "../shared/source-workspace.js";
18
18
 
19
+ // TEMPORARY diagnostic — remove once we've read a few real conflicts in prod.
20
+ // Conflicts are now benign 409s the framework no longer error-logs; this keeps
21
+ // them visible in Netlify, and `content-conflict`'s cacheIsStale field tells us
22
+ // whether they're the multi-instance stale-cache phantom (→ worth the core
23
+ // reload fix) or a genuine concurrent writer (→ 409-rebase is already correct).
24
+ function logSaveConflictDebug(
25
+ event: string,
26
+ detail: Record<string, unknown>,
27
+ ): void {
28
+ console.warn(`[update-file:debug] ${event}`, detail);
29
+ }
30
+
31
+ // 404 via statusCode (NOT status — the action route only reads statusCode) so
32
+ // the client save-outbox treats a gone file as terminal and drops it, instead
33
+ // of looping a masked 500. Used at every missing-file guard, including the
34
+ // post-lock rereads a concurrent delete can hit.
35
+ function fileNotFound(id: string): Error & { statusCode?: number } {
36
+ const err = new Error(`File not found: ${id}`) as Error & {
37
+ statusCode?: number;
38
+ };
39
+ err.statusCode = 404;
40
+ return err;
41
+ }
42
+
19
43
  function rowsAffected(result: unknown): number | undefined {
20
44
  const candidate = result as {
21
45
  rowsAffected?: unknown;
@@ -169,16 +193,8 @@ export default defineAction({
169
193
  .limit(1);
170
194
 
171
195
  if (!file) {
172
- // Terminal (not transient): the row is gone or out of access scope, so a
173
- // retry can never succeed. Mark it 404 so the client save-outbox drops the
174
- // entry instead of retrying forever (which turns one orphaned/deleted
175
- // screen into an update-file 500 storm — see design-save-outbox
176
- // isTerminalSaveError).
177
- const notFound = new Error(`File not found: ${id}`) as Error & {
178
- status?: number;
179
- };
180
- notFound.status = 404;
181
- throw notFound;
196
+ // The row is gone or out of access scope retry can't succeed.
197
+ throw fileNotFound(id);
182
198
  }
183
199
 
184
200
  await assertAccess("design", file.designId, "editor");
@@ -232,7 +248,9 @@ export default defineAction({
232
248
  .where(eq(schema.designFiles.id, id))
233
249
  .limit(1);
234
250
  if (!persistedFile) {
235
- throw new Error(`File not found: ${id}`);
251
+ // Delete-race: the row passed the access check but is gone now.
252
+ // Same 404 as the outer guard, not a bare 500 the outbox retries.
253
+ throw fileNotFound(id);
236
254
  }
237
255
 
238
256
  const persistedContentHash = sourceContentHash(persistedFile.content);
@@ -390,9 +408,47 @@ export default defineAction({
390
408
  skippedStaleMirror = true;
391
409
  }
392
410
  } else {
393
- throw new Error(
411
+ const liveContentHash = sourceContentHash(liveContent);
412
+ // Diagnostic probe (never affects the outcome): `getText` read this
413
+ // instance's Y.Doc cache, never re-hydrated from the DB. Compare it
414
+ // to the freshly persisted text_snapshot so the log distinguishes a
415
+ // multi-instance stale cache (cacheIsStale) from a genuine writer.
416
+ let freshDbLiveHash: string | null = null;
417
+ try {
418
+ const { rows } = await getDbExec().execute({
419
+ sql: "SELECT text_snapshot FROM _collab_docs WHERE doc_id = ?",
420
+ args: [id],
421
+ });
422
+ if (rows.length > 0) {
423
+ freshDbLiveHash = sourceContentHash(
424
+ String(rows[0]?.text_snapshot ?? ""),
425
+ );
426
+ }
427
+ } catch {
428
+ // diagnostic only
429
+ }
430
+ logSaveConflictDebug("content-conflict", {
431
+ id,
432
+ caller: context?.caller,
433
+ syncCollab,
434
+ operationRevision: operationRevision ?? null,
435
+ expectedVersionHash,
436
+ sentContentHash: sourceContentHash(content),
437
+ liveContentHash,
438
+ persistedMirrorHash: persistedContentHash,
439
+ cacheIsStale:
440
+ freshDbLiveHash !== null &&
441
+ freshDbLiveHash !== liveContentHash,
442
+ freshDbMatchesExpected: freshDbLiveHash === expectedVersionHash,
443
+ });
444
+ // 409 (statusCode), not a bare 500: an expected optimistic-
445
+ // concurrency outcome the framework returns verbatim and the client
446
+ // rebases from, instead of a Sentry-captured fault + retry storm.
447
+ const conflict = new Error(
394
448
  "File changed since it was read. Re-read the file and retry.",
395
- );
449
+ ) as Error & { statusCode?: number };
450
+ conflict.statusCode = 409;
451
+ throw conflict;
396
452
  }
397
453
  }
398
454
  }
@@ -461,7 +517,9 @@ export default defineAction({
461
517
  // guarded UPDATE alone can still race. Serialize design-file renames in
462
518
  // this rare path without using SQLite's fragile async savepoint wrapper.
463
519
  await (
464
- tx as unknown as { execute: (query: unknown) => Promise<unknown> }
520
+ tx as unknown as {
521
+ execute: (query: unknown) => Promise<unknown>;
522
+ }
465
523
  ).execute(sql`LOCK TABLE design_files IN SHARE ROW EXCLUSIVE MODE`);
466
524
  const [collision] = await tx
467
525
  .select({ id: schema.designFiles.id })
@@ -545,7 +603,7 @@ export default defineAction({
545
603
  .from(schema.designFiles)
546
604
  .where(eq(schema.designFiles.id, id))
547
605
  .limit(1);
548
- if (!confirmed) throw new Error(`File not found: ${id}`);
606
+ if (!confirmed) throw fileNotFound(id);
549
607
  const confirmedHash = sourceContentHash(confirmed.content);
550
608
  const exactOperationPersisted =
551
609
  confirmed.contentOperationSource === operationSource &&
@@ -622,9 +680,18 @@ export default defineAction({
622
680
  }
623
681
  return;
624
682
  }
625
- throw new Error(
683
+ logSaveConflictDebug("retry-exhausted", {
684
+ id,
685
+ caller: context?.caller,
686
+ operationSource: operationSource ?? null,
687
+ operationRevision: operationRevision ?? null,
688
+ expectedVersionHash,
689
+ });
690
+ const exhausted = new Error(
626
691
  "File changed repeatedly while it was being saved. Re-read the file and retry.",
627
- );
692
+ ) as Error & { statusCode?: number };
693
+ exhausted.statusCode = 409;
694
+ throw exhausted;
628
695
  });
629
696
 
630
697
  // Update the parent design's updatedAt timestamp. This still runs even