@agent-native/core 0.154.4 → 0.155.0

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 (207) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/account-health/SKILL.md +76 -0
  3. package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +3 -0
  4. package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +2 -1
  5. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +6 -5
  6. package/corpus/templates/analytics/AGENTS.md +3 -0
  7. package/corpus/templates/analytics/actions/account-deep-dive.ts +3 -2
  8. package/corpus/templates/analytics/actions/bigquery.ts +1 -1
  9. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -1
  10. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +12 -4
  12. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +5 -2
  13. package/corpus/templates/analytics/app/pages/adhoc/explorer/property-label.ts +32 -0
  14. package/corpus/templates/analytics/changelog/2026-08-13-account-health-readouts-verify-customer-scope-and-contract-metrics.md +7 -0
  15. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +43 -17
  16. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +17 -0
  17. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  18. package/corpus/templates/calendar/AGENTS.md +2 -0
  19. package/corpus/templates/calendar/DEVELOPING.md +4 -4
  20. package/corpus/templates/calendar/actions/get-settings.ts +4 -11
  21. package/corpus/templates/calendar/actions/update-availability.ts +15 -4
  22. package/corpus/templates/calendar/actions/update-settings.ts +15 -4
  23. package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +9 -3
  24. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +21 -19
  25. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +9 -6
  26. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +25 -3
  27. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +10 -3
  28. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +9 -9
  29. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -3
  30. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +10 -4
  31. package/corpus/templates/calendar/app/i18n-data.ts +30 -0
  32. package/corpus/templates/calendar/app/lib/availability-schedule.ts +53 -0
  33. package/corpus/templates/calendar/app/lib/event-form-utils.ts +5 -0
  34. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +90 -28
  35. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +135 -61
  36. package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -0
  37. package/corpus/templates/calendar/app/pages/CalendarView.tsx +30 -15
  38. package/corpus/templates/calendar/app/pages/Settings.tsx +60 -1
  39. package/corpus/templates/calendar/changelog/2026-08-13-find-a-time-now-closes-from-a-clear-header-action.md +6 -0
  40. package/corpus/templates/calendar/server/handlers/bookings.ts +2 -1
  41. package/corpus/templates/calendar/server/handlers/settings.ts +12 -16
  42. package/corpus/templates/calendar/shared/api.ts +1 -0
  43. package/corpus/templates/calendar/shared/availability-schedule.ts +64 -0
  44. package/corpus/templates/calendar/shared/calendar-week.ts +21 -0
  45. package/corpus/templates/calendar/shared/settings.ts +44 -0
  46. package/corpus/templates/clips/AGENTS.md +2 -0
  47. package/corpus/templates/clips/actions/add-comment.ts +5 -2
  48. package/corpus/templates/clips/actions/finalize-recording.ts +31 -1
  49. package/corpus/templates/clips/actions/get-recording-player-data.ts +9 -0
  50. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +21 -1
  51. package/corpus/templates/clips/actions/reply-to-comment.ts +5 -2
  52. package/corpus/templates/clips/actions/update-comment.ts +9 -2
  53. package/corpus/templates/clips/app/components/library/search-bar.tsx +26 -13
  54. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +135 -3
  55. package/corpus/templates/clips/app/components/player/comments-panel.tsx +5 -99
  56. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +7 -3
  57. package/corpus/templates/clips/app/components/player/video-player.tsx +84 -24
  58. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +11 -3
  59. package/corpus/templates/clips/app/i18n/en-US.ts +12 -0
  60. package/corpus/templates/clips/app/lib/public-meeting.ts +11 -3
  61. package/corpus/templates/clips/app/lib/search-focus.ts +15 -0
  62. package/corpus/templates/clips/app/root.tsx +2 -1
  63. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +94 -26
  64. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +24 -7
  65. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +8 -4
  66. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -7
  67. package/corpus/templates/clips/app/routes/share.$shareId.tsx +8 -4
  68. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +72 -26
  69. package/corpus/templates/clips/changelog/2026-08-13-inline-markdown-comments.md +6 -0
  70. package/corpus/templates/clips/changelog/2026-08-13-private-meeting-agent-links.md +6 -0
  71. package/corpus/templates/clips/desktop/src/app.tsx +116 -19
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +13 -29
  73. package/corpus/templates/clips/desktop/src/lib/recording-request.ts +39 -0
  74. package/corpus/templates/clips/desktop/src/lib/settings-navigation.ts +11 -0
  75. package/corpus/templates/clips/desktop/src/lib/whisper-model-picker.ts +7 -0
  76. package/corpus/templates/clips/desktop/src/styles.css +117 -0
  77. package/corpus/templates/clips/server/db/index.ts +10 -0
  78. package/corpus/templates/clips/server/db/schema.ts +1 -0
  79. package/corpus/templates/clips/server/lib/recordings.ts +3 -1
  80. package/corpus/templates/clips/server/lib/seekable-media-state.ts +90 -0
  81. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  82. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +37 -4
  83. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +16 -5
  84. package/corpus/templates/clips/shared/clips-ai-prefs.ts +3 -0
  85. package/corpus/templates/clips/shared/meeting-agent-access.ts +5 -0
  86. package/corpus/templates/content/.agents/skills/content/SKILL.md +2 -2
  87. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
  88. package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
  89. package/corpus/templates/content/actions/_database-utils.ts +44 -0
  90. package/corpus/templates/content/actions/_property-utils.ts +4 -1
  91. package/corpus/templates/content/actions/add-comment.ts +2 -1
  92. package/corpus/templates/content/actions/add-database-item.ts +62 -240
  93. package/corpus/templates/content/actions/configure-document-property.ts +165 -1
  94. package/corpus/templates/content/actions/delete-document-property.ts +18 -0
  95. package/corpus/templates/content/actions/set-document-property.ts +78 -1
  96. package/corpus/templates/content/actions/update-comment.ts +1 -1
  97. package/corpus/templates/content/actions/update-database-item.ts +62 -0
  98. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
  99. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +24 -15
  100. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
  101. package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
  102. package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
  103. package/corpus/templates/content/parity/matrix.md +1 -1
  104. package/corpus/templates/content/parity/matrix.ts +2 -0
  105. package/corpus/templates/content/server/db/schema.ts +39 -3
  106. package/corpus/templates/content/server/plugins/db.ts +30 -0
  107. package/corpus/templates/content/shared/api.ts +75 -2
  108. package/corpus/templates/design/actions/generate-design.ts +10 -7
  109. package/corpus/templates/design/actions/present-design-variants.ts +35 -6
  110. package/corpus/templates/design/actions/run-design-audit.ts +33 -0
  111. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-catches-a-screen-that-renders-behind-an.md +6 -0
  112. package/corpus/templates/design/shared/design-review.ts +1 -0
  113. package/corpus/templates/design/shared/html-integrity.ts +66 -0
  114. package/corpus/templates/dispatch/.env.example +5 -0
  115. package/corpus/templates/dispatch/AGENTS.md +24 -19
  116. package/corpus/templates/dispatch/app/i18n-data.ts +20 -0
  117. package/corpus/templates/dispatch/server/lib/identity-sso.ts +21 -90
  118. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +3 -12
  119. package/corpus/templates/dispatch/shared/feature-flags.ts +4 -0
  120. package/corpus/templates/factory/AGENTS.md +5 -1
  121. package/corpus/templates/factory/README.md +23 -0
  122. package/corpus/templates/factory/actions/view-screen.ts +17 -0
  123. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +239 -0
  124. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +2 -0
  125. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +130 -7
  126. package/corpus/templates/factory/app/i18n/en-US.ts +20 -0
  127. package/corpus/templates/factory/app/routes/factory.tsx +41 -0
  128. package/corpus/templates/factory/package.json +1 -0
  129. package/corpus/templates/factory/server/factory-graph/contracts.ts +2 -0
  130. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -0
  131. package/corpus/templates/factory/server/plugins/dispatch-db.ts +1 -0
  132. package/corpus/templates/factory/server/plugins/setup-dispatch.ts +3 -0
  133. package/corpus/templates/mail/app/lib/native-mcp-exclusions.ts +3 -0
  134. package/corpus/templates/mail/vite.config.ts +7 -0
  135. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +7 -2
  136. package/corpus/templates/plan/actions/resolve-plan-comment.ts +2 -2
  137. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  138. package/corpus/templates/plan/app/pages/PlansPage.tsx +29 -20
  139. package/corpus/templates/slides/actions/add-slide-comment.ts +1 -1
  140. package/corpus/templates/slides/actions/update-slide-comment.ts +1 -1
  141. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +9 -6
  142. package/dist/action.d.ts +22 -0
  143. package/dist/action.js +27 -0
  144. package/dist/agent/production-agent.js +5 -1
  145. package/dist/agent/thread-data-builder.js +17 -1
  146. package/dist/cli/skills-content/help.js +6 -4
  147. package/dist/cli/skills.js +17 -2
  148. package/dist/cli/workspacify.d.ts +5 -0
  149. package/dist/cli/workspacify.js +65 -8
  150. package/dist/client/AgentPanel.d.ts +5 -0
  151. package/dist/client/AgentPanel.js +20 -4
  152. package/dist/client/AssistantChat.d.ts +21 -0
  153. package/dist/client/AssistantChat.js +66 -68
  154. package/dist/client/active-run-state.d.ts +14 -1
  155. package/dist/client/active-run-state.js +0 -0
  156. package/dist/client/agent-chat-adapter.js +107 -5
  157. package/dist/client/analytics.js +9 -2
  158. package/dist/client/chat/tool-call-display.js +4 -4
  159. package/dist/client/chat-first/apps-rail.js +9 -4
  160. package/dist/client/chat-first/primary-nav.d.ts +2 -1
  161. package/dist/client/chat-first/primary-nav.js +3 -3
  162. package/dist/client/chat-first/types.d.ts +5 -1
  163. package/dist/client/markdown/InlineMarkdown.d.ts +23 -0
  164. package/dist/client/markdown/InlineMarkdown.js +65 -0
  165. package/dist/client/markdown/index.d.ts +1 -0
  166. package/dist/client/markdown/index.js +1 -0
  167. package/dist/client/review/ReviewThreadPanel.js +2 -1
  168. package/dist/client/run-stream-ownership.d.ts +39 -0
  169. package/dist/client/run-stream-ownership.js +65 -0
  170. package/dist/collab/routes.d.ts +1 -1
  171. package/dist/collab/struct-routes.d.ts +1 -1
  172. package/dist/feature-flags/index.d.ts +1 -1
  173. package/dist/feature-flags/index.js +1 -1
  174. package/dist/feature-flags/store.d.ts +10 -0
  175. package/dist/feature-flags/store.js +112 -1
  176. package/dist/guards/no-env-credentials.js +4 -0
  177. package/dist/index.browser.d.ts +1 -1
  178. package/dist/index.browser.js +1 -1
  179. package/dist/index.d.ts +2 -2
  180. package/dist/index.js +2 -2
  181. package/dist/integrations/adapters/slack.js +5 -1
  182. package/dist/localization/default-messages.d.ts +1 -0
  183. package/dist/localization/default-messages.js +1 -0
  184. package/dist/notifications/routes.d.ts +1 -1
  185. package/dist/observability/routes.d.ts +1 -1
  186. package/dist/progress/routes.d.ts +1 -1
  187. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  188. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  189. package/dist/resources/agents.js +40 -5
  190. package/dist/resources/handlers.d.ts +1 -1
  191. package/dist/resources/metadata.d.ts +10 -0
  192. package/dist/resources/metadata.js +7 -3
  193. package/dist/review/actions/create-review-comment.js +1 -1
  194. package/dist/review/actions/reply-review-comment.js +1 -1
  195. package/dist/review/actions/resolve-review-thread.js +1 -1
  196. package/dist/server/action-routes.js +42 -8
  197. package/dist/server/agent-discovery.js +14 -3
  198. package/dist/server/core-routes-plugin.js +1 -1
  199. package/dist/server/identity-sso-migrations.d.ts +7 -0
  200. package/dist/server/identity-sso-migrations.js +36 -0
  201. package/dist/server/identity-sso-store.d.ts +3 -2
  202. package/dist/server/identity-sso-store.js +8 -3
  203. package/dist/server/realtime-token.d.ts +1 -1
  204. package/dist/server/release-migrations.js +4 -0
  205. package/dist/tracking/providers.js +13 -3
  206. package/dist/workspace-files/tool.js +1 -1
  207. package/package.json +3 -2
@@ -1,6 +1,10 @@
1
1
  import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
2
2
  import { emailToName } from "@agent-native/core/client/collab";
3
3
  import { useT } from "@agent-native/core/client/i18n";
4
+ import {
5
+ InlineMarkdown,
6
+ type InlineMarkdownProtectedSpan,
7
+ } from "@agent-native/core/client/markdown";
4
8
  import {
5
9
  IconCheck,
6
10
  IconMessageCircle,
@@ -40,23 +44,28 @@ import { CommentComposer, type MentionEntry } from "./CommentComposer";
40
44
 
41
45
  /**
42
46
  * Render a comment body, styling any `@mention` tokens that match the comment's
43
- * stored mentions. Plain text otherwise — no HTML is interpreted.
47
+ * stored mentions. Raw HTML is never interpreted.
44
48
  */
45
- function renderCommentBody(content: string, mentions: CommentMention[]) {
49
+ function commentMentionSpans(
50
+ mentions: CommentMention[],
51
+ ): InlineMarkdownProtectedSpan[] {
46
52
  const labels = Array.from(
47
53
  new Set(mentions.map((m) => m.name).filter((n): n is string => !!n)),
48
54
  ).sort((a, b) => b.length - a.length);
49
- if (labels.length === 0) return content;
50
- const escaped = labels.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
51
- const re = new RegExp(`(@(?:${escaped.join("|")}))`, "g");
52
- return content.split(re).map((seg, i) =>
53
- seg.startsWith("@") && labels.includes(seg.slice(1)) ? (
54
- <span key={i} className="comment-mention">
55
- {seg}
56
- </span>
57
- ) : (
58
- seg
59
- ),
55
+ return labels.map((label) => ({
56
+ source: `@${label}`,
57
+ label: `@${label}`,
58
+ className: "comment-mention",
59
+ }));
60
+ }
61
+
62
+ function renderCommentBody(content: string, mentions: CommentMention[]) {
63
+ return (
64
+ <InlineMarkdown
65
+ content={content}
66
+ inline
67
+ protectedSpans={commentMentionSpans(mentions)}
68
+ />
60
69
  );
61
70
  }
62
71
 
@@ -942,9 +951,9 @@ function ThreadView({
942
951
  {formatDate(c.created_at)}
943
952
  </span>
944
953
  </div>
945
- <p className="text-[13px] text-foreground/90 pl-8 leading-relaxed whitespace-pre-wrap">
954
+ <div className="text-[13px] text-foreground/90 pl-8 leading-relaxed">
946
955
  {renderCommentBody(c.content, c.mentions)}
947
- </p>
956
+ </div>
948
957
  </div>
949
958
  ))}
950
959
  </div>
@@ -1631,6 +1631,11 @@ function DatabaseTable({
1631
1631
  ) {
1632
1632
  if (!databaseId) return null;
1633
1633
  if (isWorkspaceCatalog) return null;
1634
+ const mutationContract = data?.mutationContract;
1635
+ if (!mutationContract) {
1636
+ toast.error(dbText("failedToCreateRow"));
1637
+ return null;
1638
+ }
1634
1639
  const propertyValues = {
1635
1640
  ...databasePropertyValuesForNewItem(filters, properties, filterMode),
1636
1641
  ...propertyValueOverrides,
@@ -1638,8 +1643,10 @@ function DatabaseTable({
1638
1643
  let response;
1639
1644
  try {
1640
1645
  response = await addItem.mutateAsync({
1641
- databaseId,
1642
- title,
1646
+ target: mutationContract.target,
1647
+ expectedSchemaRevision: mutationContract.schemaRevision,
1648
+ idempotencyKey: crypto.randomUUID(),
1649
+ ...(title.trim() ? { title } : {}),
1643
1650
  propertyValues:
1644
1651
  Object.keys(propertyValues).length > 0 ? propertyValues : undefined,
1645
1652
  });
@@ -1650,12 +1657,7 @@ function DatabaseTable({
1650
1657
  });
1651
1658
  return null;
1652
1659
  }
1653
- const createdItem = databaseCreatedItemForImmediatePreview(response, {
1654
- databaseId,
1655
- parentDocument: document,
1656
- title,
1657
- propertyValues,
1658
- });
1660
+ const createdItem = response.createdItem ?? null;
1659
1661
  const needsPreview =
1660
1662
  !!createdItem &&
1661
1663
  databaseCreatedItemNeedsPreview(items, createdItem, options);
@@ -12,6 +12,7 @@ import type {
12
12
  CancelPreparedBuilderSourceUpdateResponse,
13
13
  ChangeContentDatabaseSourceRoleRequest,
14
14
  ContentDatabaseResponse,
15
+ ContentDatabaseRowMutationResult,
15
16
  ContentDatabaseSourceAttachmentAck,
16
17
  ContentDatabaseSourceAttachmentResult,
17
18
  ContentDatabaseItemsPageResponse,
@@ -819,40 +820,40 @@ export function useTrashedContentDatabases() {
819
820
 
820
821
  export function useAddDatabaseItem(documentId: string) {
821
822
  const queryClient = useQueryClient();
822
- return useActionMutation<ContentDatabaseResponse, AddDatabaseItemRequest>(
823
- "add-database-item",
824
- {
825
- skipActionQueryInvalidation: true,
826
- onSuccess: (data) => {
827
- if (data.createdItem) {
828
- queryClient.setQueriesData<ContentDatabaseResponse>(
829
- {
830
- queryKey: ["action", "get-content-database"],
831
- predicate: (query) => {
832
- if (
833
- !isContentDatabaseQueryForDocument(query.queryKey, documentId)
834
- ) {
835
- return false;
836
- }
837
- const params = query.queryKey[2] as {
838
- tableQuery?: unknown;
839
- };
840
- return params.tableQuery === undefined;
841
- },
823
+ return useActionMutation<
824
+ ContentDatabaseRowMutationResult,
825
+ AddDatabaseItemRequest
826
+ >("add-database-item", {
827
+ skipActionQueryInvalidation: true,
828
+ onSuccess: (data) => {
829
+ if (data.createdItem) {
830
+ queryClient.setQueriesData<ContentDatabaseResponse>(
831
+ {
832
+ queryKey: ["action", "get-content-database"],
833
+ predicate: (query) => {
834
+ if (
835
+ !isContentDatabaseQueryForDocument(query.queryKey, documentId)
836
+ ) {
837
+ return false;
838
+ }
839
+ const params = query.queryKey[2] as {
840
+ tableQuery?: unknown;
841
+ };
842
+ return params.tableQuery === undefined;
842
843
  },
843
- (current) =>
844
- applyOptimisticItemToContentDatabase(current, data.createdItem!),
845
- );
846
- }
847
- queryClient.invalidateQueries({
848
- queryKey: contentDatabaseQueryKey(documentId),
849
- });
850
- queryClient.invalidateQueries({
851
- queryKey: ["action", "list-documents"],
852
- });
853
- },
844
+ },
845
+ (current) =>
846
+ applyOptimisticItemToContentDatabase(current, data.createdItem!),
847
+ );
848
+ }
849
+ queryClient.invalidateQueries({
850
+ queryKey: contentDatabaseQueryKey(documentId),
851
+ });
852
+ queryClient.invalidateQueries({
853
+ queryKey: ["action", "list-documents"],
854
+ });
854
855
  },
855
- );
856
+ });
856
857
  }
857
858
 
858
859
  export function useSubmitContentDatabaseForm(documentId: string) {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ Database row actions now validate exact schemas and safe retries before changing data.
@@ -9,7 +9,7 @@ This generated matrix tracks whether high-value Content UI operations use the sa
9
9
  | database.lifecycle-and-trash | database | Create, soft-delete, restore, list, and inspect content databases | action-backed | `create-content-database`, `create-inline-content-database`, `delete-content-database`, `get-content-database`, `list-content-databases`, `list-trashed-content-databases`, `restore-content-database` | `app/components/editor/SlashCommandMenu.tsx`, `app/hooks/use-content-database.ts`, `app/hooks/use-documents.ts` | Database pages and database records are created, read, soft-deleted, restored, and listed. | - | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts` | `database-source-scope` | - |
10
10
  | database.private-preview-drafts | database | Persist and reconcile a user's private database-page preview draft | action-backed | `get-preview-document-draft`, `update-preview-document-draft` | `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-documents.ts` | A user's private preview draft is read, saved, conflict-checked, or deleted without changing the shared database page until the normal save flow applies it. | These per-user editor-state actions are intentionally hidden from agent tools because preview drafts are a private UI recovery mechanism. | - | P1 | covered | `actions/preview-document-draft.db.test.ts` | - | - |
11
11
  | database.properties-and-view-config | database | Configure properties, values, ordering, and saved views | action-backed | `configure-document-property`, `delete-document-property`, `duplicate-document-property`, `get-content-database-personal-view`, `list-document-properties`, `reorder-document-property`, `set-document-property`, `update-content-database-personal-view`, `update-content-database-view` | `app/components/editor/DocumentProperties.tsx`, `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-document-properties.ts` | Property schemas, property values, property order, filters, sorts, grouping, hidden columns, view type, and view settings are stored. | - | - | P0 | covered | `actions/bind-content-database-source-field.db.test.ts`, `actions/content-database-source-actions.test.ts`, `actions/resync-content-database-source.db.test.ts` | `database-source-scope` | - |
12
- | database.rows | database | Add, duplicate, move, open, edit, and remove database rows | action-backed | `add-database-item`, `upsert-database-item-by-key`, `remove-database-items`, `duplicate-database-items`, `duplicate-database-item`, `migrate-content-database-rows`, `move-database-item`, `set-document-property` | `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx` | Database row memberships and ordering are created, duplicated, moved, edited, and removed without deleting the backing page; bounded migrations atomically update row bodies and properties through the same canonical data model. | - | - | P0 | covered | `actions/database-row-batch-actions.db.test.ts`, `actions/migrate-content-database-rows.db.test.ts`, `parity/__tests__/database-row-batch-reliability.test.ts` | `database-bulk-row-reliability` | - |
12
+ | database.rows | database | Add, duplicate, move, open, edit, and remove database rows | action-backed | `add-database-item`, `update-database-item`, `upsert-database-item-by-key`, `remove-database-items`, `duplicate-database-items`, `duplicate-database-item`, `migrate-content-database-rows`, `move-database-item`, `set-document-property` | `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx` | Database row memberships and ordering are created, duplicated, moved, edited, and removed without deleting the backing page; bounded migrations atomically update row bodies and properties through the same canonical data model. | - | - | P0 | covered | `actions/database-row-batch-actions.db.test.ts`, `actions/upsert-database-item-by-key.db.test.ts`, `actions/migrate-content-database-rows.db.test.ts`, `parity/__tests__/database-row-batch-reliability.test.ts` | `database-bulk-row-reliability` | - |
13
13
  | database.table-query-page | database | Query one constrained page while retaining database metadata | action-backed | `query-content-database-items` | `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-content-database.ts` | - | This UI-only bounded projection is intentionally hidden with agentTool: false; agents use get-content-database for the complete database contract. | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts`, `app/hooks/use-content-database.test.ts` | - | - |
14
14
  | editor.agent-assist-prompts | editor | Ask AI from slash generation or comment context | client-assist | - | `app/components/editor/SlashCommandMenu.tsx`, `app/components/editor/CommentsSidebar.tsx` | No direct durable mutation; the prompt asks the agent to use document actions when it decides to write. | - | - | P1 | none | - | - | - |
15
15
  | editor.client-formatting-and-insertions | editor | Rich text formatting, selection state, slash block insertion, and copy actions | client-only-ephemeral | - | `app/components/editor/BubbleToolbar.tsx`, `app/components/editor/SlashCommandMenu.tsx`, `app/components/editor/DocumentToolbar.tsx` | - | - | - | P1 | none | - | - | - |
@@ -345,6 +345,7 @@ export const parityMatrix: ParityRow[] = [
345
345
  status: "action-backed",
346
346
  actions: [
347
347
  "add-database-item",
348
+ "update-database-item",
348
349
  "upsert-database-item-by-key",
349
350
  "remove-database-items",
350
351
  "duplicate-database-items",
@@ -360,6 +361,7 @@ export const parityMatrix: ParityRow[] = [
360
361
  followUpPR: null,
361
362
  coverageRefs: [
362
363
  "actions/database-row-batch-actions.db.test.ts",
364
+ "actions/upsert-database-item-by-key.db.test.ts",
363
365
  "actions/migrate-content-database-rows.db.test.ts",
364
366
  "parity/__tests__/database-row-batch-reliability.test.ts",
365
367
  ],
@@ -234,6 +234,7 @@ export const contentDatabases = table(
234
234
  ownerBlockId: text("owner_block_id"),
235
235
  title: text("title").notNull().default("Untitled database"),
236
236
  systemRole: text("system_role"),
237
+ naturalKeyPropertyId: text("natural_key_property_id"),
237
238
  viewConfigJson: text("view_config_json").notNull().default("{}"),
238
239
  filesSystemPropertiesSeeded: integer("files_system_properties_seeded")
239
240
  .notNull()
@@ -287,9 +288,9 @@ export const contentDatabaseItems = table(
287
288
  ],
288
289
  );
289
290
 
290
- // Opt-in stable-key claims are the durable concurrency fence for the generic
291
- // database-row upsert action. They intentionally do not constrain ordinary
292
- // property editing or change add-database-item behavior.
291
+ // Opt-in stable-key claims are the durable concurrency fence for configured
292
+ // natural-key upserts. Ordinary property editing stays independent until a
293
+ // text property is explicitly selected as the database's natural key.
293
294
  export const contentDatabaseItemKeyClaims = table(
294
295
  "content_database_item_key_claims",
295
296
  {
@@ -494,6 +495,41 @@ export const contentDatabaseMigrationReceipts = table(
494
495
  ],
495
496
  );
496
497
 
498
+ export const contentDatabaseRowMutationReceipts = table(
499
+ "content_database_row_mutation_receipts",
500
+ {
501
+ id: text("id").primaryKey(),
502
+ ownerEmail: text("owner_email").notNull().default("local@localhost"),
503
+ orgId: text("org_id"),
504
+ spaceId: text("space_id").notNull(),
505
+ databaseId: text("database_id").notNull(),
506
+ databaseDocumentId: text("database_document_id").notNull(),
507
+ operation: text("operation").notNull(),
508
+ itemId: text("item_id").notNull(),
509
+ documentId: text("document_id").notNull(),
510
+ idempotencyKey: text("idempotency_key").notNull(),
511
+ payloadDigest: text("payload_digest").notNull(),
512
+ schemaRevision: text("schema_revision").notNull(),
513
+ preRowRevision: text("pre_row_revision"),
514
+ postRowRevision: text("post_row_revision").notNull(),
515
+ resultJson: text("result_json").notNull().default("{}"),
516
+ createdAt: text("created_at").notNull().default(now()),
517
+ updatedAt: text("updated_at").notNull().default(now()),
518
+ },
519
+ (receipt) => [
520
+ uniqueIndex(
521
+ "content_database_row_mutation_receipts_database_key_unique",
522
+ ).on(receipt.databaseId, receipt.idempotencyKey),
523
+ index("content_database_row_mutation_receipts_owner_database_idx").on(
524
+ receipt.ownerEmail,
525
+ receipt.databaseId,
526
+ ),
527
+ index("content_database_row_mutation_receipts_document_idx").on(
528
+ receipt.documentId,
529
+ ),
530
+ ],
531
+ );
532
+
497
533
  export const documentPropertyValues = table("document_property_values", {
498
534
  id: text("id").primaryKey(),
499
535
  ownerEmail: text("owner_email").notNull().default("local@localhost"),
@@ -1011,6 +1011,36 @@ export const runContentMigrations = runMigrations(
1011
1011
  ALTER TABLE document_blocks ALTER COLUMN addressable SET DEFAULT true`,
1012
1012
  },
1013
1013
  },
1014
+ {
1015
+ version: 84,
1016
+ name: "content-database-row-mutation-contract",
1017
+ sql: `ALTER TABLE content_databases ADD COLUMN IF NOT EXISTS natural_key_property_id TEXT;
1018
+ CREATE TABLE IF NOT EXISTS content_database_row_mutation_receipts (
1019
+ id TEXT PRIMARY KEY,
1020
+ owner_email TEXT NOT NULL DEFAULT 'local@localhost',
1021
+ org_id TEXT,
1022
+ space_id TEXT NOT NULL,
1023
+ database_id TEXT NOT NULL,
1024
+ database_document_id TEXT NOT NULL,
1025
+ operation TEXT NOT NULL,
1026
+ item_id TEXT NOT NULL,
1027
+ document_id TEXT NOT NULL,
1028
+ idempotency_key TEXT NOT NULL,
1029
+ payload_digest TEXT NOT NULL,
1030
+ schema_revision TEXT NOT NULL,
1031
+ pre_row_revision TEXT,
1032
+ post_row_revision TEXT NOT NULL,
1033
+ result_json TEXT NOT NULL DEFAULT '{}',
1034
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1035
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1036
+ );
1037
+ CREATE UNIQUE INDEX IF NOT EXISTS content_database_row_mutation_receipts_database_key_unique
1038
+ ON content_database_row_mutation_receipts (database_id, idempotency_key);
1039
+ CREATE INDEX IF NOT EXISTS content_database_row_mutation_receipts_owner_database_idx
1040
+ ON content_database_row_mutation_receipts (owner_email, database_id);
1041
+ CREATE INDEX IF NOT EXISTS content_database_row_mutation_receipts_document_idx
1042
+ ON content_database_row_mutation_receipts (document_id)`,
1043
+ },
1014
1044
  ],
1015
1045
  { table: "content_migrations" },
1016
1046
  );
@@ -225,6 +225,7 @@ export interface ConfigureDocumentPropertyRequest {
225
225
  description?: string;
226
226
  visibility?: DocumentPropertyVisibility;
227
227
  options?: DocumentPropertyOptions;
228
+ naturalKey?: boolean;
228
229
  }
229
230
 
230
231
  export interface SetDocumentPropertyRequest {
@@ -258,8 +259,10 @@ export interface ReorderDocumentPropertyRequest {
258
259
  export interface ContentDatabase {
259
260
  id: string;
260
261
  documentId: string;
262
+ spaceId?: string | null;
261
263
  title: string;
262
264
  systemRole?: string | null;
265
+ naturalKeyPropertyId?: string | null;
263
266
  description?: string;
264
267
  viewConfig: ContentDatabaseViewConfig;
265
268
  createdAt: string;
@@ -469,6 +472,62 @@ export interface ContentDatabaseItem {
469
472
  // a secondary source contributes on top of it. Absent for non-federated rows.
470
473
  canonicalKey?: string | null;
471
474
  sourceOverlays?: ContentDatabaseSourceOverlay[];
475
+ rowRevision?: string;
476
+ }
477
+
478
+ export interface ContentDatabaseMutationTarget {
479
+ authorityScope:
480
+ | { kind: "personal"; id: string }
481
+ | { kind: "organization"; id: string };
482
+ spaceId: string;
483
+ databaseId: string;
484
+ databaseDocumentId: string;
485
+ }
486
+
487
+ export interface ContentDatabaseMutationContract {
488
+ target: ContentDatabaseMutationTarget;
489
+ schemaRevision: string;
490
+ naturalKeyPropertyId: string | null;
491
+ properties: Array<{
492
+ id: string;
493
+ name: string;
494
+ type: DocumentPropertyType;
495
+ writable: boolean;
496
+ sourceManaged: boolean;
497
+ acceptedShape: string | null;
498
+ options: DocumentPropertyOptions;
499
+ }>;
500
+ }
501
+
502
+ export interface ContentDatabaseRowMutationReceipt {
503
+ receiptId: string;
504
+ operation: "create" | "update" | "upsert";
505
+ outcome: "created" | "updated" | "unchanged";
506
+ target: ContentDatabaseMutationTarget;
507
+ schemaRevision: string;
508
+ row: {
509
+ itemId: string;
510
+ documentId: string;
511
+ urlPath: string;
512
+ rowRevision: string;
513
+ };
514
+ affected: { title: boolean; propertyIds: string[] };
515
+ idempotency: {
516
+ key: string;
517
+ result: "applied" | "replayed";
518
+ payloadDigest: string;
519
+ };
520
+ revisions: { before: string | null; after: string };
521
+ readback: {
522
+ verified: true;
523
+ title: string;
524
+ propertyValues: Record<string, DocumentPropertyValue>;
525
+ };
526
+ }
527
+
528
+ export interface ContentDatabaseRowMutationResult {
529
+ receipt: ContentDatabaseRowMutationReceipt;
530
+ createdItem?: ContentDatabaseItem;
472
531
  }
473
532
 
474
533
  // A secondary source's read-only contribution to a federated row, matched on the
@@ -828,6 +887,7 @@ export interface ContentDatabaseResponse {
828
887
  removedCount?: number;
829
888
  timings?: BuilderActionTiming[];
830
889
  tableQueryMode?: "server" | "client-required";
890
+ mutationContract?: ContentDatabaseMutationContract;
831
891
  /** Client-only optimistic state while real provider rows are being attached. */
832
892
  attachPreview?: {
833
893
  sourceTable: string;
@@ -892,9 +952,22 @@ export interface CreateInlineDatabaseResponse {
892
952
  }
893
953
 
894
954
  export interface AddDatabaseItemRequest {
895
- databaseId: string;
955
+ target: ContentDatabaseMutationTarget;
956
+ expectedSchemaRevision: string;
957
+ idempotencyKey: string;
896
958
  title?: string;
897
- propertyValues?: Record<string, DocumentPropertyValue>;
959
+ propertyValues?: Record<string, unknown>;
960
+ }
961
+
962
+ export interface UpdateDatabaseItemRequest extends AddDatabaseItemRequest {
963
+ itemId: string;
964
+ documentId: string;
965
+ expectedRowRevision: string;
966
+ }
967
+
968
+ export interface UpsertDatabaseItemByKeyRequest extends AddDatabaseItemRequest {
969
+ keyValue: string;
970
+ expectedRowRevision: string | null;
898
971
  }
899
972
 
900
973
  export interface SubmitContentDatabaseFormRequest {
@@ -171,12 +171,15 @@ const reuseLabelSchema = z
171
171
  }
172
172
  });
173
173
 
174
- function hasBreakpointSet(value: unknown): boolean {
175
- if (!value || typeof value !== "object" || Array.isArray(value)) {
176
- return false;
177
- }
174
+ function classifyBreakpointSet(
175
+ value: unknown,
176
+ ): "absent" | "malformed" | "present" {
177
+ if (value === null || value === undefined) return "absent";
178
+ if (typeof value !== "object" || Array.isArray(value)) return "malformed";
178
179
  const breakpoints = (value as { breakpoints?: unknown }).breakpoints;
179
- return Array.isArray(breakpoints) && breakpoints.length > 0;
180
+ if (breakpoints === undefined) return "malformed";
181
+ if (!Array.isArray(breakpoints)) return "malformed";
182
+ return breakpoints.length > 0 ? "present" : "malformed";
180
183
  }
181
184
 
182
185
  function jsonValuesEqual(left: unknown, right: unknown): boolean {
@@ -1137,7 +1140,7 @@ const generateDesignAction = defineAction({
1137
1140
  mergedData.breakpointSetUpdatedAt = updatedAt;
1138
1141
  } else if (
1139
1142
  generatedBreakpointSet.length > 0 &&
1140
- !hasBreakpointSet(mergedData.breakpointSet)
1143
+ classifyBreakpointSet(mergedData.breakpointSet) === "absent"
1141
1144
  ) {
1142
1145
  mergedData.breakpointSet = {
1143
1146
  id: "generated-responsive",
@@ -1200,7 +1203,7 @@ const generateDesignAction = defineAction({
1200
1203
  devices && devices.length > 0
1201
1204
  ? jsonValuesEqual(currentBreakpointWidths, expectedBreakpointWidths)
1202
1205
  : generatedBreakpointSet.length === 0 ||
1203
- hasBreakpointSet(current.breakpointSet);
1206
+ classifyBreakpointSet(current.breakpointSet) !== "absent";
1204
1207
  return framesApplied && breakpointSetApplied;
1205
1208
  },
1206
1209
  });
@@ -89,12 +89,25 @@ async function hasLinkedDesignSystem(designId: string): Promise<boolean> {
89
89
  return Boolean(design?.designSystemId?.trim());
90
90
  }
91
91
 
92
- function hasBreakpointSet(value: unknown): boolean {
93
- if (!value || typeof value !== "object" || Array.isArray(value)) {
94
- return false;
95
- }
92
+ /**
93
+ * Absent, or present but unreadable — the caller must not treat these alike.
94
+ * A malformed set is still the user's data: overwriting it with the generated
95
+ * default silently discards breakpoints they configured, so it reports
96
+ * `"malformed"` and the install is skipped rather than clobbering it.
97
+ */
98
+ function classifyBreakpointSet(
99
+ value: unknown,
100
+ ): "absent" | "malformed" | "present" {
101
+ if (value === null || value === undefined) return "absent";
102
+ if (typeof value !== "object" || Array.isArray(value)) return "malformed";
96
103
  const breakpoints = (value as { breakpoints?: unknown }).breakpoints;
97
- return Array.isArray(breakpoints) && breakpoints.length > 0;
104
+ if (breakpoints === undefined) return "malformed";
105
+ if (!Array.isArray(breakpoints)) return "malformed";
106
+ return breakpoints.length > 0 ? "present" : "malformed";
107
+ }
108
+
109
+ function hasBreakpointSet(value: unknown): boolean {
110
+ return classifyBreakpointSet(value) === "present";
98
111
  }
99
112
 
100
113
  function designDeepLink(designId: string): string {
@@ -1042,9 +1055,16 @@ export default defineAction({
1042
1055
  });
1043
1056
  }
1044
1057
 
1058
+ // Presenting options should not silently reconfigure the design. When it
1059
+ // does, the overview paints an extra preview beside EVERY primary frame
1060
+ // from then on — including screens created later — so the caller is told.
1061
+ let installedBreakpointSet = false;
1062
+
1045
1063
  await mutateDesignData({
1046
1064
  designId,
1047
1065
  mutate: (current, { updatedAt }) => {
1066
+ installedBreakpointSet =
1067
+ classifyBreakpointSet(current.breakpointSet) === "absent";
1048
1068
  // Placement depends on the breakpoint set in effect after this call, so
1049
1069
  // it is resolved here (inside the compare-and-set body) rather than
1050
1070
  // against a design snapshot that a concurrent write may have moved on
@@ -1106,7 +1126,9 @@ export default defineAction({
1106
1126
  canvasFrames: mergedFrames.canvasFrames,
1107
1127
  screenMetadata: previousMetadata,
1108
1128
  designVariantSets: previousVariantSets,
1109
- ...(hasBreakpointSet(current.breakpointSet)
1129
+ // Only when genuinely absent. A malformed set is the user's data in
1130
+ // a shape this action cannot read, not a blank slate to overwrite.
1131
+ ...(classifyBreakpointSet(current.breakpointSet) !== "absent"
1110
1132
  ? {}
1111
1133
  : {
1112
1134
  breakpointSet: {
@@ -1227,6 +1249,13 @@ export default defineAction({
1227
1249
  embed: true,
1228
1250
  cleanedUpPreviousVariantScreens: variantSetCleanup.removedFileIds.length,
1229
1251
  deletedSupersededSetIds: variantSetCleanup.removedSetIds,
1252
+ ...(installedBreakpointSet
1253
+ ? {
1254
+ installedBreakpointSet: DEFAULT_RESPONSIVE_BREAKPOINTS.map(
1255
+ (breakpoint) => breakpoint.widthPx,
1256
+ ),
1257
+ }
1258
+ : {}),
1230
1259
  fallbackInstructions: FALLBACK_INSTRUCTIONS,
1231
1260
  nextRequiredAction:
1232
1261
  'Wait for the user to pick a screen in chat. Then delete each unchosen variant screen with delete-file at most once, call get-design-snapshot exactly once with fileId for the chosen screen, and call edit-design with that same fileId in a bounded pass. Use mode "replace-file" to replace the representative direction screen with a complete but compact requested app/product UI in the chosen visual style. Prioritize the primary workflow and render secondary details as visible controls, states, or affordances if the full feature list is too large for one reliable edit. Do not leave a direction board, variant brief, or summary card as the final result. Do not repeat delete/snapshot cycles. Do not call generate-design after a variant pick. Stop after the first successful edit-design save.',
@@ -25,6 +25,10 @@ import type {
25
25
  A11yFindingCategory,
26
26
  A11ySeverity,
27
27
  } from "../shared/design-review.js";
28
+ import {
29
+ describeDesignHtmlIntegrityIssue,
30
+ inspectDesignHtmlDocumentIntegrity,
31
+ } from "../shared/html-integrity.js";
28
32
 
29
33
  // ---------------------------------------------------------------------------
30
34
  // HTML helpers (static analysis — no DOM runtime available server-side)
@@ -259,6 +263,34 @@ export function checkTapTargets(html: string): A11yFinding[] {
259
263
  return findings;
260
264
  }
261
265
 
266
+ /**
267
+ * Screens that render with an Alpine-controlled element covering them. Every
268
+ * other check here is a regex over the raw string; this one delegates to the
269
+ * save-time integrity parser so the audit and the write gate cannot drift into
270
+ * disagreeing about the same document. It is the only check that can see the
271
+ * failure a screenshot cannot: `take-design-screenshot` waits for Alpine to
272
+ * settle, so a pre-Alpine cover never appears in the agent's own capture.
273
+ */
274
+ export function checkRenderBlockingOverlays(html: string): A11yFinding[] {
275
+ const result = inspectDesignHtmlDocumentIntegrity(html);
276
+ const issues = [...(result.detail ?? []), ...(result.advisory ?? [])].filter(
277
+ (issue) =>
278
+ issue.issue === "runtime-overlay-unhidden" ||
279
+ issue.issue === "runtime-cloak-missing" ||
280
+ issue.issue === "runtime-alpine-missing",
281
+ );
282
+
283
+ return issues.map((issue, index) => ({
284
+ id: `render-blocking-overlay:${issue.issue}-${index}`,
285
+ severity: "error" as A11ySeverity,
286
+ category: "render-blocking-overlay" as A11yFindingCategory,
287
+ message:
288
+ "An Alpine-controlled element can cover the screen before Alpine starts.",
289
+ detail: describeDesignHtmlIntegrityIssue(issue),
290
+ fixAvailable: false,
291
+ }));
292
+ }
293
+
262
294
  /** Check for animations/transitions without a prefers-reduced-motion guard. */
263
295
  function checkReducedMotion(html: string): A11yFinding[] {
264
296
  const findings: A11yFinding[] = [];
@@ -848,6 +880,7 @@ export default defineAction({
848
880
  ...checkReducedMotion(html),
849
881
  ...checkFocusVisibility(html),
850
882
  ...checkContrastHint(html),
883
+ ...checkRenderBlockingOverlays(html),
851
884
  ...tokenDriftFindings,
852
885
  ...designSystemFindings,
853
886
  ];
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-13
4
+ ---
5
+
6
+ The design audit now catches a screen that renders behind a full-frame Alpine overlay — the failure a screenshot cannot show, because the capture waits for Alpine to settle and the overlay is gone by then.
@@ -25,6 +25,7 @@ export const A11Y_FINDING_CATEGORIES = [
25
25
  "role",
26
26
  "token-drift",
27
27
  "design-system-drift",
28
+ "render-blocking-overlay",
28
29
  "other",
29
30
  ] as const;
30
31