@agent-native/core 0.101.6 → 0.101.10

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 (237) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/deployment.mdx +9 -7
  4. package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
  5. package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
  6. package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
  7. package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
  8. package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
  9. package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
  10. package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
  11. package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
  12. package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
  13. package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/artifact-response.ts +255 -12
  16. package/corpus/core/src/agent/thread-data-builder.ts +84 -11
  17. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
  18. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
  19. package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
  20. package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
  21. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
  22. package/corpus/core/src/integrations/a2a-continuation-processor.ts +99 -7
  23. package/corpus/core/src/integrations/adapters/discord.ts +3 -1
  24. package/corpus/core/src/integrations/adapters/email.ts +4 -1
  25. package/corpus/core/src/integrations/adapters/google-docs.ts +4 -1
  26. package/corpus/core/src/integrations/adapters/microsoft-teams.ts +3 -1
  27. package/corpus/core/src/integrations/adapters/slack.ts +38 -9
  28. package/corpus/core/src/integrations/adapters/telegram.ts +32 -10
  29. package/corpus/core/src/integrations/adapters/whatsapp.ts +5 -1
  30. package/corpus/core/src/integrations/index.ts +1 -0
  31. package/corpus/core/src/integrations/types.ts +9 -2
  32. package/corpus/core/src/integrations/webhook-handler.ts +69 -45
  33. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +1 -0
  34. package/corpus/core/src/server/auth.ts +25 -1
  35. package/corpus/core/src/server/builder-browser.ts +353 -1
  36. package/corpus/core/src/server/core-routes-plugin.ts +601 -223
  37. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
  38. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
  39. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
  40. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
  41. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
  42. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
  43. package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
  44. package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
  45. package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
  46. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +35 -21
  47. package/corpus/templates/content/AGENTS.md +32 -9
  48. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
  49. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
  50. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  51. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
  52. package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
  53. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
  54. package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
  55. package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
  56. package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
  57. package/corpus/templates/content/actions/_database-source-utils.ts +1071 -170
  58. package/corpus/templates/content/actions/_database-utils.ts +104 -13
  59. package/corpus/templates/content/actions/_local-file-documents.ts +53 -1
  60. package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
  61. package/corpus/templates/content/actions/_property-utils.ts +7 -1
  62. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
  63. package/corpus/templates/content/actions/configure-document-property.ts +11 -0
  64. package/corpus/templates/content/actions/create-content-database.ts +11 -0
  65. package/corpus/templates/content/actions/create-document.ts +9 -0
  66. package/corpus/templates/content/actions/create-inline-content-database.ts +3 -0
  67. package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
  68. package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
  69. package/corpus/templates/content/actions/export-content-source.ts +1 -0
  70. package/corpus/templates/content/actions/get-content-database.ts +13 -1
  71. package/corpus/templates/content/actions/get-document.ts +12 -2
  72. package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
  73. package/corpus/templates/content/actions/import-content-source.ts +6 -0
  74. package/corpus/templates/content/actions/list-documents.ts +3 -0
  75. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
  76. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
  77. package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
  78. package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
  79. package/corpus/templates/content/actions/pull-document.ts +2 -0
  80. package/corpus/templates/content/actions/search-documents.ts +5 -1
  81. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
  82. package/corpus/templates/content/actions/update-document.ts +16 -1
  83. package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
  84. package/corpus/templates/content/actions/view-screen.ts +15 -2
  85. package/corpus/templates/content/app/components/editor/DescriptionField.tsx +121 -0
  86. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +14 -0
  87. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +250 -49
  88. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
  89. package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
  90. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2414 -589
  91. package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
  92. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
  93. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
  94. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
  95. package/corpus/templates/content/app/global.css +18 -1
  96. package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
  97. package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
  98. package/corpus/templates/content/app/i18n/zh-TW.ts +30 -0
  99. package/corpus/templates/content/app/i18n-data.ts +339 -0
  100. package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
  101. package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
  102. package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
  103. package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
  104. package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
  105. package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
  106. package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
  107. package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
  108. package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
  109. package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
  110. package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
  111. package/corpus/templates/content/changelog/2026-07-14-pages-and-databases-can-describe-themselves-with-guidance-fo.md +6 -0
  112. package/corpus/templates/content/changelog/2026-07-14-slack-follow-ups-now-retain-created-content-identity-and-per.md +6 -0
  113. package/corpus/templates/content/parity/matrix.md +27 -25
  114. package/corpus/templates/content/parity/matrix.ts +50 -2
  115. package/corpus/templates/content/server/db/schema.ts +51 -0
  116. package/corpus/templates/content/server/lib/document-context.ts +76 -0
  117. package/corpus/templates/content/server/lib/public-documents.ts +3 -0
  118. package/corpus/templates/content/server/plugins/auth.ts +1 -0
  119. package/corpus/templates/content/server/plugins/db.ts +59 -0
  120. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +5 -0
  121. package/corpus/templates/content/shared/api.ts +87 -1
  122. package/corpus/templates/content/shared/builder-mdx.ts +705 -15
  123. package/corpus/templates/content/shared/content-source.ts +7 -0
  124. package/corpus/templates/content/shared/nfm.ts +3 -3
  125. package/corpus/templates/content/shared/properties.ts +2 -0
  126. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
  127. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
  128. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  129. package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
  130. package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
  131. package/corpus/templates/design/app/i18n/en-US.ts +1 -0
  132. package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
  133. package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
  134. package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
  135. package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
  136. package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
  137. package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
  138. package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
  139. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  140. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
  141. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
  142. package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
  143. package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
  144. package/corpus/templates/design/shared/review-anchor.ts +48 -0
  145. package/corpus/templates/plan/server/plan-mdx.ts +53 -7
  146. package/dist/a2a/artifact-response.d.ts +15 -0
  147. package/dist/a2a/artifact-response.d.ts.map +1 -1
  148. package/dist/a2a/artifact-response.js +204 -5
  149. package/dist/a2a/artifact-response.js.map +1 -1
  150. package/dist/agent/thread-data-builder.d.ts +6 -0
  151. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  152. package/dist/agent/thread-data-builder.js +71 -8
  153. package/dist/agent/thread-data-builder.js.map +1 -1
  154. package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
  155. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  156. package/dist/client/review/ReviewCommentComposer.js +3 -3
  157. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  158. package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
  159. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  160. package/dist/client/review/ReviewThreadPanel.js +6 -4
  161. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  162. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  163. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  164. package/dist/client/settings/SettingsPanel.js +11 -4
  165. package/dist/client/settings/SettingsPanel.js.map +1 -1
  166. package/dist/client/settings/useBuilderStatus.d.ts +8 -4
  167. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  168. package/dist/client/settings/useBuilderStatus.js +19 -6
  169. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  170. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
  171. package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
  172. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
  173. package/dist/collab/awareness.d.ts +2 -2
  174. package/dist/collab/awareness.d.ts.map +1 -1
  175. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  176. package/dist/integrations/a2a-continuation-processor.js +72 -6
  177. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  178. package/dist/integrations/adapters/discord.d.ts.map +1 -1
  179. package/dist/integrations/adapters/discord.js +1 -0
  180. package/dist/integrations/adapters/discord.js.map +1 -1
  181. package/dist/integrations/adapters/email.d.ts.map +1 -1
  182. package/dist/integrations/adapters/email.js +2 -0
  183. package/dist/integrations/adapters/email.js.map +1 -1
  184. package/dist/integrations/adapters/google-docs.d.ts.map +1 -1
  185. package/dist/integrations/adapters/google-docs.js +2 -0
  186. package/dist/integrations/adapters/google-docs.js.map +1 -1
  187. package/dist/integrations/adapters/microsoft-teams.d.ts.map +1 -1
  188. package/dist/integrations/adapters/microsoft-teams.js +1 -0
  189. package/dist/integrations/adapters/microsoft-teams.js.map +1 -1
  190. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  191. package/dist/integrations/adapters/slack.js +21 -6
  192. package/dist/integrations/adapters/slack.js.map +1 -1
  193. package/dist/integrations/adapters/telegram.d.ts.map +1 -1
  194. package/dist/integrations/adapters/telegram.js +11 -2
  195. package/dist/integrations/adapters/telegram.js.map +1 -1
  196. package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
  197. package/dist/integrations/adapters/whatsapp.js +3 -0
  198. package/dist/integrations/adapters/whatsapp.js.map +1 -1
  199. package/dist/integrations/index.d.ts +1 -1
  200. package/dist/integrations/index.d.ts.map +1 -1
  201. package/dist/integrations/index.js.map +1 -1
  202. package/dist/integrations/types.d.ts +8 -2
  203. package/dist/integrations/types.d.ts.map +1 -1
  204. package/dist/integrations/types.js.map +1 -1
  205. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  206. package/dist/integrations/webhook-handler.js +40 -43
  207. package/dist/integrations/webhook-handler.js.map +1 -1
  208. package/dist/observability/routes.d.ts +5 -5
  209. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  210. package/dist/secrets/routes.d.ts +9 -9
  211. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  212. package/dist/server/agent-chat/action-filters-a2a.js +1 -0
  213. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  214. package/dist/server/auth.d.ts +1 -0
  215. package/dist/server/auth.d.ts.map +1 -1
  216. package/dist/server/auth.js +19 -2
  217. package/dist/server/auth.js.map +1 -1
  218. package/dist/server/builder-browser.d.ts +78 -0
  219. package/dist/server/builder-browser.d.ts.map +1 -1
  220. package/dist/server/builder-browser.js +246 -1
  221. package/dist/server/builder-browser.js.map +1 -1
  222. package/dist/server/core-routes-plugin.d.ts +41 -0
  223. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  224. package/dist/server/core-routes-plugin.js +260 -14
  225. package/dist/server/core-routes-plugin.js.map +1 -1
  226. package/docs/content/deployment.mdx +9 -7
  227. package/docs/content/locales/ar-SA/deployment.mdx +9 -7
  228. package/docs/content/locales/de-DE/deployment.mdx +9 -7
  229. package/docs/content/locales/es-ES/deployment.mdx +9 -7
  230. package/docs/content/locales/fr-FR/deployment.mdx +9 -7
  231. package/docs/content/locales/hi-IN/deployment.mdx +9 -7
  232. package/docs/content/locales/ja-JP/deployment.mdx +9 -7
  233. package/docs/content/locales/ko-KR/deployment.mdx +9 -7
  234. package/docs/content/locales/pt-BR/deployment.mdx +9 -7
  235. package/docs/content/locales/zh-CN/deployment.mdx +9 -7
  236. package/docs/content/locales/zh-TW/deployment.mdx +9 -7
  237. package/package.json +2 -2
@@ -105,6 +105,12 @@ import {
105
105
  PopoverTrigger,
106
106
  } from "@/components/ui/popover";
107
107
  import { Spinner } from "@/components/ui/spinner";
108
+ import { Textarea } from "@/components/ui/textarea";
109
+ import {
110
+ Tooltip,
111
+ TooltipContent,
112
+ TooltipTrigger,
113
+ } from "@/components/ui/tooltip";
108
114
  import { applySourceFieldPropertyToDatabaseResponse } from "@/hooks/use-content-database";
109
115
  import {
110
116
  useConfigureDocumentProperty,
@@ -483,6 +489,24 @@ export function filesMediaEditorValue(value: DocumentProperty["value"]) {
483
489
  return filesMediaItems(value).join("\n");
484
490
  }
485
491
 
492
+ export function isValidFilesMediaLink(value: string) {
493
+ try {
494
+ const url = new URL(value.trim());
495
+ return url.protocol === "http:" || url.protocol === "https:";
496
+ } catch {
497
+ return false;
498
+ }
499
+ }
500
+
501
+ export function mergeFilesMediaItems(items: string[], pendingLink: string) {
502
+ const trimmed = pendingLink.trim();
503
+ if (!trimmed || !isValidFilesMediaLink(trimmed)) return items;
504
+ if (items.some((item) => item.toLowerCase() === trimmed.toLowerCase())) {
505
+ return items;
506
+ }
507
+ return [...items, trimmed];
508
+ }
509
+
486
510
  export function filesMediaLabel(value: string) {
487
511
  const trimmed = value.trim();
488
512
  if (!trimmed) return "File";
@@ -551,7 +575,8 @@ export function filterPropertyOptions(
551
575
  return options.filter(
552
576
  (option) =>
553
577
  option.name.toLowerCase().includes(normalizedQuery) ||
554
- option.id.toLowerCase().includes(normalizedQuery),
578
+ option.id.toLowerCase().includes(normalizedQuery) ||
579
+ option.description?.toLowerCase().includes(normalizedQuery),
555
580
  );
556
581
  }
557
582
 
@@ -612,6 +637,76 @@ export function updatePropertyOptionColor(
612
637
  );
613
638
  }
614
639
 
640
+ export function updatePropertyOptionDescription(
641
+ options: DocumentPropertyOption[],
642
+ optionId: string,
643
+ description: string,
644
+ ) {
645
+ return options.map((option) =>
646
+ option.id === optionId ? { ...option, description } : option,
647
+ );
648
+ }
649
+
650
+ /**
651
+ * Keeps successive option edits based on the same local truth until the
652
+ * server catches up. A rename followed immediately by a usage-description
653
+ * edit must not let either request erase the other.
654
+ */
655
+ export function createPropertyOptionUpdateQueue(
656
+ initialOptions: DocumentPropertyOption[],
657
+ persist: (options: DocumentPropertyOption[]) => Promise<unknown>,
658
+ ) {
659
+ let current = initialOptions;
660
+ let tail: Promise<unknown> = Promise.resolve();
661
+
662
+ return {
663
+ replace(options: DocumentPropertyOption[]) {
664
+ current = options;
665
+ },
666
+ enqueue(
667
+ update: (options: DocumentPropertyOption[]) => DocumentPropertyOption[],
668
+ ) {
669
+ current = update(current);
670
+ const snapshot = current;
671
+ tail = tail.catch(() => undefined).then(() => persist(snapshot));
672
+ return tail;
673
+ },
674
+ };
675
+ }
676
+
677
+ type PropertyMetadataSnapshot = Pick<
678
+ DocumentProperty["definition"],
679
+ "name" | "type" | "description" | "visibility" | "options"
680
+ >;
681
+
682
+ /**
683
+ * Serializes property-definition edits against one local snapshot. The action
684
+ * accepts the complete definition, so composing each request from render-time
685
+ * props would let a fast description save restore the name from before an
686
+ * overlapping rename completed.
687
+ */
688
+ export function createPropertyMetadataUpdateQueue(
689
+ initialMetadata: PropertyMetadataSnapshot,
690
+ persist: (metadata: PropertyMetadataSnapshot) => Promise<unknown>,
691
+ ) {
692
+ let current = initialMetadata;
693
+ let tail: Promise<unknown> = Promise.resolve();
694
+
695
+ return {
696
+ replace(metadata: PropertyMetadataSnapshot) {
697
+ current = metadata;
698
+ },
699
+ enqueue(
700
+ update: (metadata: PropertyMetadataSnapshot) => PropertyMetadataSnapshot,
701
+ ) {
702
+ current = update(current);
703
+ const snapshot = current;
704
+ tail = tail.catch(() => undefined).then(() => persist(snapshot));
705
+ return tail;
706
+ },
707
+ };
708
+ }
709
+
615
710
  export function removePropertyOption(
616
711
  options: DocumentPropertyOption[],
617
712
  optionId: string,
@@ -833,7 +928,23 @@ function PropertyRow({
833
928
  ) : (
834
929
  <div className="flex min-w-0 items-center gap-2 text-muted-foreground">
835
930
  <Icon className="size-4 shrink-0" />
836
- <span className="truncate">{property.definition.name}</span>
931
+ {property.definition.description ? (
932
+ <Tooltip>
933
+ <TooltipTrigger asChild>
934
+ <button
935
+ type="button"
936
+ className="truncate text-left outline-none focus-visible:ring-2 focus-visible:ring-ring"
937
+ >
938
+ {property.definition.name}
939
+ </button>
940
+ </TooltipTrigger>
941
+ <TooltipContent className="max-w-64">
942
+ {property.definition.description}
943
+ </TooltipContent>
944
+ </Tooltip>
945
+ ) : (
946
+ <span className="truncate">{property.definition.name}</span>
947
+ )}
837
948
  </div>
838
949
  )}
839
950
  {canEdit && property.editable ? (
@@ -961,7 +1072,25 @@ export function PropertyManagementPopover({
961
1072
  const [open, setOpen] = useState(false);
962
1073
  const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
963
1074
  const [name, setName] = useState(property.definition.name);
1075
+ const [description, setDescription] = useState(
1076
+ property.definition.description,
1077
+ );
964
1078
  const [newOption, setNewOption] = useState("");
1079
+ const persistMetadataSnapshotRef = useRef<
1080
+ (metadata: PropertyMetadataSnapshot) => Promise<unknown>
1081
+ >(async () => undefined);
1082
+ const metadataUpdateQueueRef = useRef(
1083
+ createPropertyMetadataUpdateQueue(
1084
+ {
1085
+ name: property.definition.name,
1086
+ type: property.definition.type,
1087
+ description: property.definition.description,
1088
+ visibility: property.definition.visibility,
1089
+ options: property.definition.options,
1090
+ },
1091
+ (metadata) => persistMetadataSnapshotRef.current(metadata),
1092
+ ),
1093
+ );
965
1094
  const propertyNameInputRef = useRef<HTMLInputElement>(null);
966
1095
  const typeIsLocked = isComputedPropertyType(property.definition.type);
967
1096
  const typeNeedsOptions =
@@ -971,7 +1100,15 @@ export function PropertyManagementPopover({
971
1100
 
972
1101
  function resetDraft() {
973
1102
  setName(property.definition.name);
1103
+ setDescription(property.definition.description);
974
1104
  setNewOption("");
1105
+ metadataUpdateQueueRef.current.replace({
1106
+ name: property.definition.name,
1107
+ type: property.definition.type,
1108
+ description: property.definition.description,
1109
+ visibility: property.definition.visibility,
1110
+ options: property.definition.options,
1111
+ });
975
1112
  }
976
1113
 
977
1114
  useEffect(() => {
@@ -990,17 +1127,34 @@ export function PropertyManagementPopover({
990
1127
  type?: DocumentPropertyType;
991
1128
  visibility?: DocumentPropertyVisibility;
992
1129
  options?: DocumentProperty["definition"]["options"];
1130
+ description?: string;
993
1131
  }) {
994
- const nextType = next.type ?? property.definition.type;
995
- await configure.mutateAsync({
1132
+ await metadataUpdateQueueRef.current.enqueue((current) => ({
1133
+ name: next.name?.trim() || current.name,
1134
+ type: next.type ?? current.type,
1135
+ description: next.description ?? current.description,
1136
+ visibility: next.visibility ?? current.visibility,
1137
+ options: next.options ?? current.options,
1138
+ }));
1139
+ }
1140
+
1141
+ async function updateOptions(
1142
+ update: (options: DocumentPropertyOption[]) => DocumentPropertyOption[],
1143
+ ) {
1144
+ await metadataUpdateQueueRef.current.enqueue((current) => ({
1145
+ ...current,
1146
+ options: {
1147
+ options: update(current.options.options ?? []),
1148
+ },
1149
+ }));
1150
+ }
1151
+
1152
+ persistMetadataSnapshotRef.current = (metadata) =>
1153
+ configure.mutateAsync({
996
1154
  id: property.definition.id,
997
1155
  documentId,
998
- name: next.name?.trim() || property.definition.name,
999
- type: nextType,
1000
- visibility: next.visibility,
1001
- options: next.options ?? property.definition.options,
1156
+ ...metadata,
1002
1157
  });
1003
- }
1004
1158
 
1005
1159
  async function renameProperty() {
1006
1160
  const nextName = name.trim();
@@ -1008,6 +1162,12 @@ export function PropertyManagementPopover({
1008
1162
  await configureProperty({ name: nextName });
1009
1163
  }
1010
1164
 
1165
+ async function updateDescription() {
1166
+ const nextDescription = (description ?? "").trim();
1167
+ if (nextDescription === property.definition.description) return;
1168
+ await configureProperty({ description: nextDescription });
1169
+ }
1170
+
1011
1171
  async function updateType(nextType: DocumentPropertyType) {
1012
1172
  if (nextType === property.definition.type) return;
1013
1173
  await configureProperty({
@@ -1042,39 +1202,39 @@ export function PropertyManagementPopover({
1042
1202
  async function addOption() {
1043
1203
  const optionName = newOption.trim();
1044
1204
  if (!optionName) return;
1045
- const existing = property.definition.options.options ?? [];
1046
- const option = makeOption(
1047
- optionName,
1048
- existing.length,
1049
- existing.map((item) => item.id),
1050
- );
1051
- await configureProperty({
1052
- options: { options: [...existing, option] },
1053
- });
1205
+ await updateOptions((existing) => [
1206
+ ...existing,
1207
+ makeOption(
1208
+ optionName,
1209
+ existing.length,
1210
+ existing.map((item) => item.id),
1211
+ ),
1212
+ ]);
1054
1213
  setNewOption("");
1055
1214
  }
1056
1215
 
1057
1216
  async function removeOption(id: string) {
1058
- await configureProperty({
1059
- options: {
1060
- options: (property.definition.options.options ?? []).filter(
1061
- (option) => option.id !== id,
1062
- ),
1063
- },
1064
- });
1217
+ await updateOptions((options) =>
1218
+ options.filter((option) => option.id !== id),
1219
+ );
1065
1220
  }
1066
1221
 
1067
1222
  async function renameOption(id: string, optionName: string) {
1068
- const options = property.definition.options.options ?? [];
1069
- const nextOptions = renamePropertyOption(options, id, optionName);
1070
- if (nextOptions === options) return;
1071
- await configureProperty({ options: { options: nextOptions } });
1223
+ await updateOptions((options) =>
1224
+ renamePropertyOption(options, id, optionName),
1225
+ );
1072
1226
  }
1073
1227
 
1074
1228
  async function recolorOption(id: string, color: DocumentPropertyOptionColor) {
1075
- const options = property.definition.options.options ?? [];
1076
- const nextOptions = updatePropertyOptionColor(options, id, color);
1077
- await configureProperty({ options: { options: nextOptions } });
1229
+ await updateOptions((options) =>
1230
+ updatePropertyOptionColor(options, id, color),
1231
+ );
1232
+ }
1233
+
1234
+ async function describeOption(id: string, description: string) {
1235
+ await updateOptions((options) =>
1236
+ updatePropertyOptionDescription(options, id, description),
1237
+ );
1078
1238
  }
1079
1239
 
1080
1240
  return (
@@ -1092,6 +1252,7 @@ export function PropertyManagementPopover({
1092
1252
  aria-label={t("editor.properties.propertyMenuFor", {
1093
1253
  name: property.definition.name,
1094
1254
  })}
1255
+ title={property.definition.description || undefined}
1095
1256
  className={cn(
1096
1257
  "flex min-w-0 items-center gap-2 rounded px-1 py-0.5 text-left text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1097
1258
  triggerClassName,
@@ -1138,6 +1299,21 @@ export function PropertyManagementPopover({
1138
1299
  />
1139
1300
  </div>
1140
1301
 
1302
+ <div
1303
+ className="px-2 pb-2 pt-1"
1304
+ onKeyDown={(event) => event.stopPropagation()}
1305
+ >
1306
+ <Textarea
1307
+ rows={2}
1308
+ value={description}
1309
+ aria-label={t("editor.properties.description")}
1310
+ placeholder={t("editor.properties.addPropertyDescription")}
1311
+ onChange={(event) => setDescription(event.target.value)}
1312
+ onBlur={() => void updateDescription()}
1313
+ className="block w-full resize-none rounded border border-transparent bg-muted/30 px-2 py-1.5 text-xs leading-5 text-muted-foreground outline-none placeholder:text-muted-foreground/60 focus:border-input focus:bg-background focus:ring-1 focus:ring-ring"
1314
+ />
1315
+ </div>
1316
+
1141
1317
  <DropdownMenuSub>
1142
1318
  <DropdownMenuSubTrigger>
1143
1319
  <Icon className="mr-2 size-4 text-muted-foreground" />
@@ -1217,6 +1393,9 @@ export function PropertyManagementPopover({
1217
1393
  option={option}
1218
1394
  disabled={configure.isPending}
1219
1395
  onRename={(name) => void renameOption(option.id, name)}
1396
+ onDescriptionChange={(description) =>
1397
+ void describeOption(option.id, description)
1398
+ }
1220
1399
  onColorChange={(color) =>
1221
1400
  void recolorOption(option.id, color)
1222
1401
  }
@@ -1424,21 +1603,27 @@ function PropertyOptionSettingsRow({
1424
1603
  option,
1425
1604
  disabled,
1426
1605
  onRename,
1606
+ onDescriptionChange,
1427
1607
  onColorChange,
1428
1608
  onRemove,
1429
1609
  }: {
1430
1610
  option: DocumentPropertyOption;
1431
1611
  disabled: boolean;
1432
1612
  onRename: (name: string) => void;
1613
+ onDescriptionChange: (description: string) => void;
1433
1614
  onColorChange: (color: DocumentPropertyOptionColor) => void;
1434
1615
  onRemove: () => void;
1435
1616
  }) {
1436
1617
  const t = useT();
1437
1618
  const [draftName, setDraftName] = useState(option.name);
1619
+ const [draftDescription, setDraftDescription] = useState(
1620
+ option.description ?? "",
1621
+ );
1438
1622
 
1439
1623
  useEffect(() => {
1440
1624
  setDraftName(option.name);
1441
- }, [option.name]);
1625
+ setDraftDescription(option.description ?? "");
1626
+ }, [option.description, option.name]);
1442
1627
 
1443
1628
  function submitRename() {
1444
1629
  const nextName = draftName.trim();
@@ -1528,6 +1713,21 @@ function PropertyOptionSettingsRow({
1528
1713
  {t("editor.properties.remove")}
1529
1714
  </button>
1530
1715
  </div>
1716
+ <Textarea
1717
+ rows={1}
1718
+ value={draftDescription}
1719
+ disabled={disabled}
1720
+ aria-label={`${t("editor.properties.description")}: ${option.name}`}
1721
+ placeholder={t("editor.properties.addOptionDescription")}
1722
+ onChange={(event) => setDraftDescription(event.target.value)}
1723
+ onBlur={() => {
1724
+ const nextDescription = draftDescription.trim();
1725
+ if (nextDescription !== (option.description ?? "")) {
1726
+ onDescriptionChange(nextDescription);
1727
+ }
1728
+ }}
1729
+ className="ml-5 block w-[calc(100%-1.25rem)] resize-none rounded border-0 bg-transparent px-1 text-xs leading-5 text-muted-foreground shadow-none placeholder:text-muted-foreground/60 focus:bg-background focus:ring-1 focus:ring-ring"
1730
+ />
1531
1731
  </div>
1532
1732
  );
1533
1733
  }
@@ -1878,16 +2078,8 @@ function FilesMediaValueEditor({
1878
2078
  }, []);
1879
2079
 
1880
2080
  function addItem(value: string) {
1881
- const trimmed = value.trim();
1882
- if (!trimmed) return;
1883
- setItems((current) => {
1884
- if (
1885
- current.some((item) => item.toLowerCase() === trimmed.toLowerCase())
1886
- ) {
1887
- return current;
1888
- }
1889
- return [...current, trimmed];
1890
- });
2081
+ if (!isValidFilesMediaLink(value)) return;
2082
+ setItems((current) => mergeFilesMediaItems(current, value));
1891
2083
  setLinkValue("");
1892
2084
  }
1893
2085
 
@@ -1944,11 +2136,11 @@ function FilesMediaValueEditor({
1944
2136
  className="grid gap-3"
1945
2137
  onSubmit={(event) => {
1946
2138
  event.preventDefault();
1947
- if (linkValue.trim()) {
1948
- addItem(linkValue);
2139
+ if (linkValue.trim() && !isValidFilesMediaLink(linkValue)) {
2140
+ linkInputRef.current?.reportValidity();
1949
2141
  return;
1950
2142
  }
1951
- void save();
2143
+ void save(mergeFilesMediaItems(items, linkValue));
1952
2144
  }}
1953
2145
  >
1954
2146
  <div className="grid max-h-48 gap-1 overflow-auto">
@@ -1992,9 +2184,11 @@ function FilesMediaValueEditor({
1992
2184
  <div className="flex gap-1">
1993
2185
  <Input
1994
2186
  ref={linkInputRef}
1995
- aria-label={t("editor.properties.addPropertyLink", {
2187
+ aria-label={t("editor.properties.editValue", {
1996
2188
  name: property.definition.name,
1997
2189
  })}
2190
+ type="url"
2191
+ pattern="[hH][tT][tT][pP][sS]?://.*"
1998
2192
  value={linkValue}
1999
2193
  placeholder={t("editor.properties.pasteFileOrMediaLink")}
2000
2194
  onChange={(event) => setLinkValue(event.target.value)}
@@ -2011,7 +2205,7 @@ function FilesMediaValueEditor({
2011
2205
  size="sm"
2012
2206
  className="shrink-0"
2013
2207
  onClick={() => addItem(linkValue)}
2014
- disabled={!linkValue.trim() || mutation.isPending}
2208
+ disabled={!isValidFilesMediaLink(linkValue) || mutation.isPending}
2015
2209
  >
2016
2210
  <IconPlus className="size-3.5" />
2017
2211
  {t("editor.properties.add")}
@@ -2559,7 +2753,14 @@ function OptionValueEditor({
2559
2753
  className="flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-accent"
2560
2754
  onClick={() => void chooseOption(option)}
2561
2755
  >
2562
- <OptionPill option={option} />
2756
+ <span className="min-w-0 flex-1">
2757
+ <OptionPill option={option} />
2758
+ {option.description ? (
2759
+ <span className="mt-0.5 block truncate text-xs text-muted-foreground">
2760
+ {option.description}
2761
+ </span>
2762
+ ) : null}
2763
+ </span>
2563
2764
  {checked ? (
2564
2765
  <IconCheck className="size-4 text-muted-foreground" />
2565
2766
  ) : null}
@@ -924,6 +924,28 @@ export function shouldPersistLocalFileEditorUpdate({
924
924
  return Boolean(transactionUiEvent);
925
925
  }
926
926
 
927
+ function isEffectivelyEmptyEditorContent(value: string): boolean {
928
+ const normalized = value.trim();
929
+ return normalized === "" || normalized === "<empty-block/>";
930
+ }
931
+
932
+ export function shouldPersistEffectivelyEmptyEditorUpdate({
933
+ nextContent,
934
+ userInitiated,
935
+ }: {
936
+ nextContent: string;
937
+ userInitiated: boolean;
938
+ }): boolean {
939
+ if (!isEffectivelyEmptyEditorContent(nextContent)) return true;
940
+ // Empty editor state is never worth persisting without a user gesture. This
941
+ // also covers the preview remount window where `content` can briefly be an
942
+ // empty list snapshot even though its retained save controller still has a
943
+ // rich confirmed baseline. Comparing only to this render's prop would let
944
+ // that mount-time filler mark the retained controller dirty and its
945
+ // flush-on-release path would then overwrite SQL.
946
+ return userInitiated;
947
+ }
948
+
927
949
  function isActiveSlashCommandDraft(editor: CoreEditor): boolean {
928
950
  const { state } = editor;
929
951
  if (!state.selection.empty) return false;
@@ -1086,7 +1108,9 @@ function getVisualEditorPlaceholder({
1086
1108
  if (level === 1) return "Heading 1";
1087
1109
  if (level === 2) return "Heading 2";
1088
1110
  if (level === 3) return "Heading 3";
1089
- return "Heading 4";
1111
+ if (level === 4) return "Heading 4";
1112
+ if (level === 5) return "Heading 5";
1113
+ return "Heading 6";
1090
1114
  }
1091
1115
 
1092
1116
  if (
@@ -1300,6 +1324,7 @@ export function createVisualEditorExtensions({
1300
1324
  starterKit: {
1301
1325
  blockquote: false,
1302
1326
  paragraph: false,
1327
+ heading: { levels: [1, 2, 3, 4, 5, 6] },
1303
1328
  horizontalRule: {},
1304
1329
  dropcursor: { color: false, width: 3, class: "notion-dropcursor" },
1305
1330
  },
@@ -1742,7 +1767,11 @@ export function VisualEditor({
1742
1767
  const persistEditorContent = useCallback(
1743
1768
  (
1744
1769
  editorToPersist: CoreEditor,
1745
- options?: { markdown?: string; immediate?: boolean },
1770
+ options?: {
1771
+ markdown?: string;
1772
+ immediate?: boolean;
1773
+ userInitiated?: boolean;
1774
+ },
1746
1775
  ) => {
1747
1776
  const guards = guardsRef.current;
1748
1777
  if (!guards) return false;
@@ -1750,6 +1779,20 @@ export function VisualEditor({
1750
1779
  const normalized =
1751
1780
  options?.markdown ?? docToNfm(editorToPersist.getJSON() as any);
1752
1781
  if (localFileMode && normalized === content) return true;
1782
+ // TipTap/Yjs can emit a local-looking empty-paragraph transaction while
1783
+ // an editor is mounting or reconciling. Content serializes that filler
1784
+ // as `<empty-block/>`, so the generic whitespace-only collab guard does
1785
+ // not catch it. Never let that lifecycle normalization clear a saved
1786
+ // body. A real Select All/Delete (or Cut) records user intent through
1787
+ // the DOM handlers below and is still allowed to persist normally.
1788
+ if (
1789
+ !shouldPersistEffectivelyEmptyEditorUpdate({
1790
+ nextContent: normalized,
1791
+ userInitiated: options?.userInitiated === true,
1792
+ })
1793
+ ) {
1794
+ return true;
1795
+ }
1753
1796
  if (options?.immediate && onSaveContentRef.current) {
1754
1797
  return onSaveContentRef.current(normalized);
1755
1798
  }
@@ -1923,7 +1966,12 @@ export function VisualEditor({
1923
1966
  return;
1924
1967
  }
1925
1968
  if (isActiveSlashCommandDraft(editor)) return;
1926
- persistEditorContent(editor);
1969
+ persistEditorContent(editor, {
1970
+ userInitiated:
1971
+ transaction.getMeta(LOCAL_FILE_USER_EDIT_META) === true ||
1972
+ Date.now() - lastUserEditIntentAtRef.current < 2000 ||
1973
+ Boolean(transaction.getMeta("uiEvent")),
1974
+ });
1927
1975
  },
1928
1976
  });
1929
1977
 
@@ -2239,10 +2287,14 @@ export function VisualEditor({
2239
2287
  documentId={documentId}
2240
2288
  notionPageId={notionPageId}
2241
2289
  onDraftCommitted={() => {
2242
- void persistEditorContent(editor);
2290
+ void persistEditorContent(editor, { userInitiated: true });
2243
2291
  }}
2244
2292
  onDraftPersisted={(markdown) =>
2245
- persistEditorContent(editor, { markdown, immediate: true })
2293
+ persistEditorContent(editor, {
2294
+ markdown,
2295
+ immediate: true,
2296
+ userInitiated: true,
2297
+ })
2246
2298
  }
2247
2299
  />
2248
2300
  ) : null}
@@ -89,6 +89,33 @@ export function previewBodyHydrationIsPending(args: {
89
89
  );
90
90
  }
91
91
 
92
+ /**
93
+ * A draft that began from an empty source snapshot must not automatically
94
+ * replace a non-empty Builder body that arrived while hydration was running.
95
+ * Keep the draft dirty/recoverable and let the user decide which body to keep.
96
+ */
97
+ export function previewDraftConflictsWithHydratedBody(args: {
98
+ loadedContent: string | null | undefined;
99
+ loadedUpdatedAt: string | null | undefined;
100
+ loadedContentWasEmpty: boolean | undefined;
101
+ pendingContent: string | null | undefined;
102
+ hydratedContent: string | null | undefined;
103
+ hydratedUpdatedAt: string | null | undefined;
104
+ }) {
105
+ const sourceChangedSinceDraftLoaded =
106
+ (!!args.loadedUpdatedAt &&
107
+ !!args.hydratedUpdatedAt &&
108
+ args.loadedUpdatedAt !== args.hydratedUpdatedAt) ||
109
+ args.loadedContent !== args.hydratedContent;
110
+ return (
111
+ !isEffectivelyEmptyDocumentContent(args.pendingContent) &&
112
+ args.pendingContent !== args.hydratedContent &&
113
+ sourceChangedSinceDraftLoaded &&
114
+ (args.loadedContentWasEmpty === true ||
115
+ args.loadedContent !== args.hydratedContent)
116
+ );
117
+ }
118
+
92
119
  export function previewBodyHydrationIsTerminalError(args: {
93
120
  item: Pick<ContentDatabaseItem, "bodyHydration" | "document">;
94
121
  document: Pick<Document, "databaseMembership"> | null | undefined;