@agent-native/core 0.101.7 → 0.101.11

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 (219) 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/client.ts +23 -1
  16. package/corpus/core/src/a2a/handlers.ts +65 -1
  17. package/corpus/core/src/a2a/server.ts +124 -0
  18. package/corpus/core/src/a2a/task-store.ts +253 -0
  19. package/corpus/core/src/a2a/types.ts +26 -0
  20. package/corpus/core/src/agent/production-agent.ts +5 -2
  21. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
  22. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
  23. package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
  24. package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
  25. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
  26. package/corpus/core/src/mcp/builtin-tools.ts +25 -1
  27. package/corpus/core/src/scripts/call-agent.ts +22 -3
  28. package/corpus/core/src/server/agent-chat-plugin.ts +86 -1
  29. package/corpus/core/src/server/auth.ts +25 -1
  30. package/corpus/core/src/server/builder-browser.ts +353 -1
  31. package/corpus/core/src/server/core-routes-plugin.ts +601 -223
  32. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
  33. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
  34. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
  35. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
  36. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
  37. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
  38. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
  39. package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
  40. package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
  41. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
  42. package/corpus/templates/content/AGENTS.md +3 -2
  43. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
  44. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
  45. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  46. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
  47. package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
  48. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
  49. package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
  50. package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
  51. package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
  52. package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
  53. package/corpus/templates/content/actions/_database-utils.ts +89 -12
  54. package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
  55. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
  56. package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
  57. package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
  58. package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
  59. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
  60. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
  61. package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
  62. package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
  63. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
  64. package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
  65. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
  66. package/corpus/templates/content/app/components/editor/MathRenderer.tsx +43 -0
  67. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +260 -4
  68. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
  69. package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
  70. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
  71. package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
  72. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
  73. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +78 -0
  74. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
  75. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
  76. package/corpus/templates/content/app/global.css +71 -1
  77. package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
  78. package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
  79. package/corpus/templates/content/app/i18n/zh-TW.ts +38 -0
  80. package/corpus/templates/content/app/i18n-data.ts +450 -0
  81. package/corpus/templates/content/app/root.tsx +2 -0
  82. package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
  83. package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
  84. package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
  85. package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
  86. package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
  89. package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
  90. package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
  93. package/corpus/templates/content/changelog/2026-07-14-latex-equations-render-in-documents-public-pages-and-exports.md +6 -0
  94. package/corpus/templates/content/package.json +1 -0
  95. package/corpus/templates/content/parity/matrix.md +27 -25
  96. package/corpus/templates/content/parity/matrix.ts +50 -2
  97. package/corpus/templates/content/server/db/schema.ts +47 -0
  98. package/corpus/templates/content/server/plugins/auth.ts +1 -0
  99. package/corpus/templates/content/server/plugins/db.ts +53 -0
  100. package/corpus/templates/content/shared/api.ts +70 -1
  101. package/corpus/templates/content/shared/builder-mdx.ts +705 -15
  102. package/corpus/templates/content/shared/document-export.ts +296 -25
  103. package/corpus/templates/content/shared/inline-math.ts +128 -0
  104. package/corpus/templates/content/shared/math-rendering.ts +50 -0
  105. package/corpus/templates/content/shared/nfm.ts +20 -17
  106. package/corpus/templates/content/vite.config.ts +0 -1
  107. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
  108. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
  109. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  110. package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
  111. package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
  112. package/corpus/templates/design/app/i18n/en-US.ts +1 -0
  113. package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
  114. package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
  115. package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
  116. package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
  117. package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
  118. package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
  119. package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
  120. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  121. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
  122. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
  123. package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
  124. package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
  125. package/corpus/templates/design/shared/review-anchor.ts +48 -0
  126. package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +17 -0
  127. package/corpus/templates/mail/AGENTS.md +21 -19
  128. package/corpus/templates/mail/actions/create-attachment-upload.ts +49 -0
  129. package/corpus/templates/mail/changelog/2026-07-14-connected-agents-can-now-securely-upload-local-files-for-mai.md +6 -0
  130. package/corpus/templates/mail/server/handlers/media.ts +84 -61
  131. package/corpus/templates/mail/server/lib/attachment-upload-ticket.ts +234 -0
  132. package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -4
  133. package/corpus/templates/mail/server/lib/media-upload.ts +114 -0
  134. package/corpus/templates/mail/server/plugins/auth.ts +9 -1
  135. package/corpus/templates/mail/server/routes/api/media/attachment-upload/[uploadId].put.ts +1 -0
  136. package/corpus/templates/plan/server/plan-mdx.ts +53 -7
  137. package/dist/a2a/client.d.ts +5 -1
  138. package/dist/a2a/client.d.ts.map +1 -1
  139. package/dist/a2a/client.js +18 -1
  140. package/dist/a2a/client.js.map +1 -1
  141. package/dist/a2a/handlers.d.ts.map +1 -1
  142. package/dist/a2a/handlers.js +57 -8
  143. package/dist/a2a/handlers.js.map +1 -1
  144. package/dist/a2a/server.d.ts.map +1 -1
  145. package/dist/a2a/server.js +87 -1
  146. package/dist/a2a/server.js.map +1 -1
  147. package/dist/a2a/task-store.d.ts +27 -0
  148. package/dist/a2a/task-store.d.ts.map +1 -1
  149. package/dist/a2a/task-store.js +216 -0
  150. package/dist/a2a/task-store.js.map +1 -1
  151. package/dist/a2a/types.d.ts +24 -0
  152. package/dist/a2a/types.d.ts.map +1 -1
  153. package/dist/a2a/types.js.map +1 -1
  154. package/dist/agent/production-agent.d.ts +1 -0
  155. package/dist/agent/production-agent.d.ts.map +1 -1
  156. package/dist/agent/production-agent.js +5 -2
  157. package/dist/agent/production-agent.js.map +1 -1
  158. package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
  159. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  160. package/dist/client/review/ReviewCommentComposer.js +3 -3
  161. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  162. package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
  163. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  164. package/dist/client/review/ReviewThreadPanel.js +6 -4
  165. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  166. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  167. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  168. package/dist/client/settings/SettingsPanel.js +11 -4
  169. package/dist/client/settings/SettingsPanel.js.map +1 -1
  170. package/dist/client/settings/useBuilderStatus.d.ts +8 -4
  171. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  172. package/dist/client/settings/useBuilderStatus.js +19 -6
  173. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  174. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
  175. package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
  176. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
  177. package/dist/collab/awareness.d.ts +2 -2
  178. package/dist/collab/awareness.d.ts.map +1 -1
  179. package/dist/collab/struct-routes.d.ts +1 -1
  180. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  181. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  182. package/dist/mcp/builtin-tools.js +22 -3
  183. package/dist/mcp/builtin-tools.js.map +1 -1
  184. package/dist/notifications/routes.d.ts +3 -3
  185. package/dist/resources/handlers.d.ts +2 -2
  186. package/dist/scripts/call-agent.d.ts +1 -1
  187. package/dist/scripts/call-agent.d.ts.map +1 -1
  188. package/dist/scripts/call-agent.js +19 -1
  189. package/dist/scripts/call-agent.js.map +1 -1
  190. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  191. package/dist/server/agent-chat-plugin.js +71 -2
  192. package/dist/server/agent-chat-plugin.js.map +1 -1
  193. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  194. package/dist/server/auth.d.ts +1 -0
  195. package/dist/server/auth.d.ts.map +1 -1
  196. package/dist/server/auth.js +19 -2
  197. package/dist/server/auth.js.map +1 -1
  198. package/dist/server/builder-browser.d.ts +78 -0
  199. package/dist/server/builder-browser.d.ts.map +1 -1
  200. package/dist/server/builder-browser.js +246 -1
  201. package/dist/server/builder-browser.js.map +1 -1
  202. package/dist/server/core-routes-plugin.d.ts +41 -0
  203. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  204. package/dist/server/core-routes-plugin.js +260 -14
  205. package/dist/server/core-routes-plugin.js.map +1 -1
  206. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
  207. package/docs/content/deployment.mdx +9 -7
  208. package/docs/content/locales/ar-SA/deployment.mdx +9 -7
  209. package/docs/content/locales/de-DE/deployment.mdx +9 -7
  210. package/docs/content/locales/es-ES/deployment.mdx +9 -7
  211. package/docs/content/locales/fr-FR/deployment.mdx +9 -7
  212. package/docs/content/locales/hi-IN/deployment.mdx +9 -7
  213. package/docs/content/locales/ja-JP/deployment.mdx +9 -7
  214. package/docs/content/locales/ko-KR/deployment.mdx +9 -7
  215. package/docs/content/locales/pt-BR/deployment.mdx +9 -7
  216. package/docs/content/locales/zh-CN/deployment.mdx +9 -7
  217. package/docs/content/locales/zh-TW/deployment.mdx +9 -7
  218. package/package.json +2 -2
  219. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
@@ -1,8 +1,10 @@
1
1
  import { useSendToAgentChat, useT } from "@agent-native/core/client";
2
2
  import type { CreateInlineDatabaseResponse } from "@shared/api";
3
+ import { renderMathToHtml } from "@shared/math-rendering";
3
4
  import { collapseExactRepeatedNfm, docToNfm } from "@shared/nfm";
4
5
  import { serializeRegistryBlockToMdx } from "@shared/nfm-registry";
5
6
  import {
7
+ IconCheck,
6
8
  IconTypography,
7
9
  IconH1,
8
10
  IconH2,
@@ -23,6 +25,8 @@ import {
23
25
  IconFileText,
24
26
  IconDatabase,
25
27
  IconVideo,
28
+ IconMathFunction,
29
+ IconSquareRoot2,
26
30
  } from "@tabler/icons-react";
27
31
  import { Editor } from "@tiptap/react";
28
32
  import { useState, useEffect, useCallback, useRef, useMemo } from "react";
@@ -30,6 +34,7 @@ import { useNavigate } from "react-router";
30
34
  import { toast } from "sonner";
31
35
 
32
36
  import { contentBlockRegistry } from "@/blocks/contentBlockRegistry";
37
+ import { Button } from "@/components/ui/button";
33
38
  import {
34
39
  Popover,
35
40
  PopoverContent,
@@ -42,6 +47,7 @@ import { localContentComponents } from "@/local-components";
42
47
 
43
48
  import { focusMostRecentEmptyToggleSummary } from "./extensions/NotionExtensions";
44
49
  import { buildLocalComponentSlashItems } from "./localComponentSlashItems";
50
+ import { MathRenderer } from "./MathRenderer";
45
51
  import { buildRegistrySlashItems } from "./registrySlashItems";
46
52
 
47
53
  interface SlashCommandMenuProps {
@@ -64,6 +70,13 @@ interface EditorMenuPosition {
64
70
  left: number;
65
71
  }
66
72
 
73
+ interface EquationDraft {
74
+ displayMode: boolean;
75
+ insertionRange: { from: number; to: number };
76
+ slashRange: { from: number; to: number };
77
+ position: EditorMenuPosition;
78
+ }
79
+
67
80
  interface CommandItem {
68
81
  title: string;
69
82
  description: string;
@@ -178,6 +191,47 @@ export function insertInlineDatabaseBlock(
178
191
  : chain.insertContent(content).run();
179
192
  }
180
193
 
194
+ export function equationNodeContent(latex: string, displayMode: boolean) {
195
+ return displayMode
196
+ ? {
197
+ type: "notionBlockAtom",
198
+ attrs: { tagName: "equation", attrsJson: "{}", label: latex },
199
+ }
200
+ : {
201
+ type: "notionInlineAtom",
202
+ attrs: { tagName: "math", attrsJson: "{}", label: latex },
203
+ };
204
+ }
205
+
206
+ export function insertEquation(
207
+ editor: Editor,
208
+ latex: string,
209
+ displayMode: boolean,
210
+ range: { from: number; to: number },
211
+ ) {
212
+ const content = equationNodeContent(latex, displayMode);
213
+ return editor
214
+ .chain()
215
+ .focus()
216
+ .insertContentAt(
217
+ range,
218
+ displayMode ? [content, { type: "paragraph" }] : content,
219
+ )
220
+ .run();
221
+ }
222
+
223
+ export function getEquationInsertionRange(
224
+ editor: Editor,
225
+ slashRange: { from: number; to: number },
226
+ displayMode: boolean,
227
+ ) {
228
+ if (!displayMode) return slashRange;
229
+ const resolved = editor.state.doc.resolve(slashRange.from);
230
+ return resolved.parent.isTextblock
231
+ ? { from: resolved.before(), to: resolved.after() }
232
+ : slashRange;
233
+ }
234
+
181
235
  const commands: CommandTemplate[] = [
182
236
  {
183
237
  titleKey: "editor.slash.text",
@@ -460,6 +514,16 @@ export function SlashCommandMenu({
460
514
  );
461
515
  const generateTextareaRef = useRef<HTMLTextAreaElement>(null);
462
516
 
517
+ const [equationDraft, setEquationDraft] = useState<EquationDraft | null>(
518
+ null,
519
+ );
520
+ const [equationLatex, setEquationLatex] = useState("");
521
+ const equationInputRef = useRef<HTMLTextAreaElement>(null);
522
+ const equationResult = useMemo(
523
+ () => renderMathToHtml(equationLatex, equationDraft?.displayMode ?? false),
524
+ [equationDraft?.displayMode, equationLatex],
525
+ );
526
+
463
527
  const submitGeneratePrompt = useCallback(
464
528
  (prompt: string) => {
465
529
  const trimmed = prompt.trim();
@@ -684,6 +748,80 @@ export function SlashCommandMenu({
684
748
  },
685
749
  };
686
750
 
751
+ const openEquationComposer = useCallback(
752
+ (displayMode: boolean, slashRange: { from: number; to: number } | null) => {
753
+ const menuPosition = position ?? getSelectionMenuPosition();
754
+ if (!slashRange || !menuPosition) return false;
755
+ setEquationLatex("");
756
+ setEquationDraft({
757
+ displayMode,
758
+ slashRange,
759
+ insertionRange: getEquationInsertionRange(
760
+ editor,
761
+ slashRange,
762
+ displayMode,
763
+ ),
764
+ position: menuPosition,
765
+ });
766
+ setTimeout(() => equationInputRef.current?.focus(), 0);
767
+ return true;
768
+ },
769
+ [editor, getSelectionMenuPosition, position],
770
+ );
771
+
772
+ const cancelEquation = useCallback(() => {
773
+ const draft = equationDraft;
774
+ setEquationDraft(null);
775
+ setEquationLatex("");
776
+ if (draft) {
777
+ editor.chain().focus().deleteRange(draft.slashRange).run();
778
+ }
779
+ }, [editor, equationDraft]);
780
+
781
+ const submitEquation = useCallback(() => {
782
+ if (!equationDraft || !equationResult.ok) return;
783
+ const latex = equationLatex.trim();
784
+ const { displayMode, insertionRange } = equationDraft;
785
+ setEquationDraft(null);
786
+ setEquationLatex("");
787
+ const inserted = insertEquation(editor, latex, displayMode, insertionRange);
788
+ if (!inserted) {
789
+ toast.error(t("editor.slash.equationInsertFailed"));
790
+ return;
791
+ }
792
+ void onDraftCommitted?.();
793
+ }, [
794
+ editor,
795
+ equationDraft,
796
+ equationLatex,
797
+ equationResult.ok,
798
+ onDraftCommitted,
799
+ t,
800
+ ]);
801
+
802
+ const equationCommands: CommandItem[] = isTurnInto
803
+ ? []
804
+ : [
805
+ {
806
+ title: t("editor.slash.blockEquation"),
807
+ description: t("editor.slash.blockEquationDescription"),
808
+ searchText: "latex katex math formula",
809
+ icon: IconMathFunction,
810
+ preserveSlashRange: true,
811
+ action: (_editor, { slashRange }) =>
812
+ openEquationComposer(true, slashRange),
813
+ },
814
+ {
815
+ title: t("editor.slash.inlineEquation"),
816
+ description: t("editor.slash.inlineEquationDescription"),
817
+ searchText: "latex katex math formula",
818
+ icon: IconSquareRoot2,
819
+ preserveSlashRange: true,
820
+ action: (_editor, { slashRange }) =>
821
+ openEquationComposer(false, slashRange),
822
+ },
823
+ ];
824
+
687
825
  // Registry-derived block items (the shared dev-doc / OpenAPI / structured
688
826
  // library). Filtered to Notion-compatible specs when the document is linked to
689
827
  // a Notion page. "Turn into" only converts the current text block, so these
@@ -713,9 +851,10 @@ export function SlashCommandMenu({
713
851
  title: t(cmd.titleKey),
714
852
  description: t(cmd.descriptionKey),
715
853
  });
716
- const blockCommands = (isTurnInto ? turnIntoCommands : commands).map(
717
- localizeCommand,
718
- );
854
+ const blockCommands = [
855
+ ...(isTurnInto ? turnIntoCommands : commands).map(localizeCommand),
856
+ ...equationCommands,
857
+ ];
719
858
  const pageCommands = isTurnInto ? [] : [pageCommand, databaseCommand];
720
859
  const mediaCommands = isTurnInto
721
860
  ? []
@@ -1042,7 +1181,7 @@ export function SlashCommandMenu({
1042
1181
  align="start"
1043
1182
  side="bottom"
1044
1183
  className="w-[calc(100vw-2rem)] max-w-80 rounded-xl p-0"
1045
- onOpenAutoFocus={(e) => {
1184
+ onOpenAutoFocus={(e: Event) => {
1046
1185
  e.preventDefault();
1047
1186
  generateTextareaRef.current?.focus();
1048
1187
  }}
@@ -1086,6 +1225,123 @@ export function SlashCommandMenu({
1086
1225
  </PopoverContent>
1087
1226
  </Popover>
1088
1227
  )}
1228
+
1229
+ {equationDraft && (
1230
+ <Popover
1231
+ open
1232
+ onOpenChange={(open: boolean) => {
1233
+ if (!open) cancelEquation();
1234
+ }}
1235
+ >
1236
+ <PopoverTrigger asChild>
1237
+ <span
1238
+ className="pointer-events-none absolute size-0"
1239
+ style={{
1240
+ top: equationDraft.position.top,
1241
+ left: Math.min(equationDraft.position.left, 16),
1242
+ }}
1243
+ />
1244
+ </PopoverTrigger>
1245
+ <PopoverContent
1246
+ align="start"
1247
+ side="bottom"
1248
+ className="w-[calc(100vw-2rem)] max-w-md rounded-xl p-0"
1249
+ onOpenAutoFocus={(event: Event) => {
1250
+ event.preventDefault();
1251
+ equationInputRef.current?.focus();
1252
+ }}
1253
+ >
1254
+ <div className="p-4">
1255
+ <div className="flex items-center gap-2 text-sm font-semibold">
1256
+ {equationDraft.displayMode ? (
1257
+ <IconMathFunction
1258
+ className="text-muted-foreground"
1259
+ size={16}
1260
+ />
1261
+ ) : (
1262
+ <IconSquareRoot2
1263
+ className="text-muted-foreground"
1264
+ size={16}
1265
+ />
1266
+ )}
1267
+ {equationDraft.displayMode
1268
+ ? t("editor.slash.blockEquation")
1269
+ : t("editor.slash.inlineEquation")}
1270
+ </div>
1271
+ <textarea
1272
+ ref={equationInputRef}
1273
+ value={equationLatex}
1274
+ onChange={(event) => setEquationLatex(event.target.value)}
1275
+ onKeyDown={(event) => {
1276
+ if (
1277
+ event.key === "Enter" &&
1278
+ (event.metaKey || event.ctrlKey) &&
1279
+ equationResult.ok
1280
+ ) {
1281
+ event.preventDefault();
1282
+ submitEquation();
1283
+ }
1284
+ if (event.key === "Escape") {
1285
+ event.preventDefault();
1286
+ cancelEquation();
1287
+ }
1288
+ }}
1289
+ rows={equationDraft.displayMode ? 3 : 2}
1290
+ placeholder={t("editor.slash.equationPlaceholder")}
1291
+ aria-label={t("editor.slash.equationInputLabel")}
1292
+ aria-invalid={equationLatex.length > 0 && !equationResult.ok}
1293
+ aria-describedby="equation-preview-status"
1294
+ className="mt-3 w-full resize-y rounded-lg border border-input bg-background px-3 py-2 font-mono text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
1295
+ />
1296
+ <div className="mt-3 min-h-20 rounded-lg border border-border bg-muted/30 p-3">
1297
+ <div className="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
1298
+ {t("editor.slash.equationPreview")}
1299
+ </div>
1300
+ <div className="flex min-h-9 items-center justify-center overflow-x-auto text-foreground">
1301
+ {equationResult.ok ? (
1302
+ <MathRenderer
1303
+ latex={equationLatex}
1304
+ displayMode={equationDraft.displayMode}
1305
+ />
1306
+ ) : (
1307
+ <span className="text-sm text-muted-foreground">
1308
+ {equationLatex
1309
+ ? t("editor.slash.equationNeedsRepair")
1310
+ : t("editor.slash.equationPreviewEmpty")}
1311
+ </span>
1312
+ )}
1313
+ </div>
1314
+ </div>
1315
+ <p
1316
+ id="equation-preview-status"
1317
+ className={cn(
1318
+ "mt-2 min-h-5 text-xs",
1319
+ equationLatex && !equationResult.ok
1320
+ ? "text-destructive"
1321
+ : "text-muted-foreground",
1322
+ )}
1323
+ >
1324
+ {equationLatex && !equationResult.ok
1325
+ ? equationResult.error
1326
+ : t("editor.slash.equationSubmitHint")}
1327
+ </p>
1328
+ </div>
1329
+ <div className="flex items-center justify-end gap-2 border-t border-border px-4 py-3">
1330
+ <Button variant="ghost" size="sm" onClick={cancelEquation}>
1331
+ {t("editor.slash.cancelEquation")}
1332
+ </Button>
1333
+ <Button
1334
+ size="sm"
1335
+ onClick={submitEquation}
1336
+ disabled={!equationResult.ok}
1337
+ >
1338
+ <IconCheck />
1339
+ {t("editor.slash.insertEquation")}
1340
+ </Button>
1341
+ </div>
1342
+ </PopoverContent>
1343
+ </Popover>
1344
+ )}
1089
1345
  </>
1090
1346
  );
1091
1347
  }
@@ -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;