@agent-native/toolkit 0.6.0 → 0.8.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 (174) hide show
  1. package/README.md +17 -1
  2. package/agent-native.eject.json +411 -0
  3. package/dist/chat-history/ChatHistoryList.d.ts +103 -0
  4. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
  5. package/dist/chat-history/ChatHistoryList.js +109 -0
  6. package/dist/chat-history/ChatHistoryList.js.map +1 -0
  7. package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
  8. package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
  9. package/dist/chat-history/ChatHistoryList.spec.js +283 -0
  10. package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
  11. package/dist/chat-history/index.d.ts +2 -0
  12. package/dist/chat-history/index.d.ts.map +1 -0
  13. package/dist/chat-history/index.js +2 -0
  14. package/dist/chat-history/index.js.map +1 -0
  15. package/dist/chat-history.css +314 -0
  16. package/package.json +20 -1
  17. package/src/app-shell/header-actions.tsx +84 -0
  18. package/src/app-shell/index.ts +1 -0
  19. package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
  20. package/src/chat-history/ChatHistoryList.tsx +469 -0
  21. package/src/chat-history/index.ts +7 -0
  22. package/src/chat-history.css +314 -0
  23. package/src/collab-ui/AgentPresenceChip.tsx +69 -0
  24. package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
  25. package/src/collab-ui/PresenceBar.tsx +346 -0
  26. package/src/collab-ui/RecentEditHighlights.tsx +225 -0
  27. package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
  28. package/src/collab-ui/agent-identity.ts +16 -0
  29. package/src/collab-ui/index.ts +39 -0
  30. package/src/collab-ui/lead-client.ts +32 -0
  31. package/src/collab-ui/types.spec.ts +18 -0
  32. package/src/collab-ui/types.ts +96 -0
  33. package/src/composer/AgentComposerFrame.tsx +59 -0
  34. package/src/composer/ComposerPlusMenu.tsx +983 -0
  35. package/src/composer/MentionPopover.tsx +412 -0
  36. package/src/composer/PastedTextChip.tsx +150 -0
  37. package/src/composer/PromptComposer.spec.ts +73 -0
  38. package/src/composer/PromptComposer.tsx +718 -0
  39. package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
  40. package/src/composer/RealtimeVoiceMode.tsx +856 -0
  41. package/src/composer/TiptapComposer.spec.ts +390 -0
  42. package/src/composer/TiptapComposer.tsx +2936 -0
  43. package/src/composer/VoiceButton.spec.ts +33 -0
  44. package/src/composer/VoiceButton.tsx +255 -0
  45. package/src/composer/asset-picker-url.spec.ts +56 -0
  46. package/src/composer/asset-picker-url.ts +80 -0
  47. package/src/composer/attachment-accept.spec.ts +22 -0
  48. package/src/composer/attachment-accept.ts +71 -0
  49. package/src/composer/draft-key.spec.ts +19 -0
  50. package/src/composer/draft-key.ts +7 -0
  51. package/src/composer/extensions/FileReference.tsx +55 -0
  52. package/src/composer/extensions/MentionReference.tsx +93 -0
  53. package/src/composer/extensions/SkillReference.tsx +56 -0
  54. package/src/composer/index.ts +79 -0
  55. package/src/composer/pasted-text.spec.ts +162 -0
  56. package/src/composer/pasted-text.ts +160 -0
  57. package/src/composer/prompt-attachments.spec.ts +80 -0
  58. package/src/composer/prompt-attachments.ts +93 -0
  59. package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
  60. package/src/composer/realtime-voice-audio-level.ts +64 -0
  61. package/src/composer/realtime-voice-transcript.spec.ts +95 -0
  62. package/src/composer/realtime-voice-transcript.ts +134 -0
  63. package/src/composer/runtime-adapters.tsx +343 -0
  64. package/src/composer/types.ts +66 -0
  65. package/src/composer/use-file-search.ts +64 -0
  66. package/src/composer/use-mention-search.ts +90 -0
  67. package/src/composer/use-skills.ts +45 -0
  68. package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
  69. package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
  70. package/src/composer/useVoiceDictation.spec.ts +45 -0
  71. package/src/composer/useVoiceDictation.ts +1191 -0
  72. package/src/context-ui/ContextMeter.tsx +101 -0
  73. package/src/context-ui/ContextSegmentRow.tsx +184 -0
  74. package/src/context-ui/ContextTreemap.tsx +125 -0
  75. package/src/context-ui/ContextXRayPanel.tsx +377 -0
  76. package/src/context-ui/format.ts +42 -0
  77. package/src/context-ui/index.ts +15 -0
  78. package/src/context-ui/types.ts +43 -0
  79. package/src/design-tweaks/index.ts +1 -0
  80. package/src/design-tweaks/visual-style-controls.tsx +1084 -0
  81. package/src/editor/BubbleToolbar.tsx +253 -0
  82. package/src/editor/CodeBlockNode.tsx +254 -0
  83. package/src/editor/DragHandle.spec.ts +588 -0
  84. package/src/editor/DragHandle.ts +1437 -0
  85. package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
  86. package/src/editor/ImageExtension.ts +306 -0
  87. package/src/editor/RegistryBlockContext.tsx +107 -0
  88. package/src/editor/RegistryBlockNode.spec.ts +279 -0
  89. package/src/editor/RegistryBlockNode.tsx +652 -0
  90. package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
  91. package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
  92. package/src/editor/RichMarkdownEditor.tsx +81 -0
  93. package/src/editor/RunId.ts +61 -0
  94. package/src/editor/SharedRichEditor.tsx +321 -0
  95. package/src/editor/SlashCommandMenu.tsx +310 -0
  96. package/src/editor/disableHistory.spec.ts +85 -0
  97. package/src/editor/extensions.ts +402 -0
  98. package/src/editor/gfmDoc.spec.ts +230 -0
  99. package/src/editor/gfmDoc.ts +92 -0
  100. package/src/editor/index.ts +93 -0
  101. package/src/editor/registrySlashCommands.ts +131 -0
  102. package/src/editor/structuralReorderUndo.spec.ts +144 -0
  103. package/src/editor/surgical-apply.spec.ts +279 -0
  104. package/src/editor/surgical-apply.ts +192 -0
  105. package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
  106. package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
  107. package/src/editor/useCollabReconcile.ts +589 -0
  108. package/src/editor.css +510 -0
  109. package/src/hooks/index.ts +2 -0
  110. package/src/hooks/use-mobile.tsx +21 -0
  111. package/src/hooks/use-toast.ts +188 -0
  112. package/src/index.ts +8 -0
  113. package/src/onboarding/index.ts +1 -0
  114. package/src/provider.tsx +47 -0
  115. package/src/sharing/VisibilityBadge.spec.ts +96 -0
  116. package/src/sharing/VisibilityBadge.tsx +37 -0
  117. package/src/sharing/index.ts +4 -0
  118. package/src/styles.css +21 -0
  119. package/src/ui/accordion.tsx +56 -0
  120. package/src/ui/alert-dialog.tsx +141 -0
  121. package/src/ui/alert.tsx +59 -0
  122. package/src/ui/aspect-ratio.tsx +5 -0
  123. package/src/ui/avatar.tsx +48 -0
  124. package/src/ui/badge.tsx +37 -0
  125. package/src/ui/breadcrumb.tsx +115 -0
  126. package/src/ui/button.tsx +78 -0
  127. package/src/ui/calendar.tsx +214 -0
  128. package/src/ui/card.tsx +86 -0
  129. package/src/ui/carousel.tsx +260 -0
  130. package/src/ui/chart.tsx +375 -0
  131. package/src/ui/checkbox.tsx +28 -0
  132. package/src/ui/collapsible.tsx +9 -0
  133. package/src/ui/command.spec.tsx +51 -0
  134. package/src/ui/command.tsx +174 -0
  135. package/src/ui/context-menu.tsx +202 -0
  136. package/src/ui/date-picker.tsx +64 -0
  137. package/src/ui/dialog.spec.tsx +43 -0
  138. package/src/ui/dialog.tsx +146 -0
  139. package/src/ui/drawer.tsx +126 -0
  140. package/src/ui/dropdown-menu.tsx +218 -0
  141. package/src/ui/form.tsx +177 -0
  142. package/src/ui/hover-card.tsx +27 -0
  143. package/src/ui/index.ts +52 -0
  144. package/src/ui/input-otp.tsx +69 -0
  145. package/src/ui/input.tsx +22 -0
  146. package/src/ui/label.tsx +24 -0
  147. package/src/ui/logo.tsx +36 -0
  148. package/src/ui/menubar.tsx +235 -0
  149. package/src/ui/navigation-menu.tsx +128 -0
  150. package/src/ui/pagination.tsx +121 -0
  151. package/src/ui/popover.tsx +44 -0
  152. package/src/ui/progress.tsx +26 -0
  153. package/src/ui/radio-group.tsx +42 -0
  154. package/src/ui/resizable.tsx +43 -0
  155. package/src/ui/scroll-area.tsx +46 -0
  156. package/src/ui/select.tsx +158 -0
  157. package/src/ui/separator.tsx +29 -0
  158. package/src/ui/sheet.tsx +160 -0
  159. package/src/ui/sidebar.tsx +777 -0
  160. package/src/ui/skeleton.tsx +18 -0
  161. package/src/ui/slider.tsx +26 -0
  162. package/src/ui/sonner.tsx +58 -0
  163. package/src/ui/spinner.tsx +17 -0
  164. package/src/ui/switch.tsx +27 -0
  165. package/src/ui/table.tsx +117 -0
  166. package/src/ui/tabs.tsx +53 -0
  167. package/src/ui/textarea.tsx +23 -0
  168. package/src/ui/toast.tsx +127 -0
  169. package/src/ui/toaster.tsx +33 -0
  170. package/src/ui/toggle-group.tsx +59 -0
  171. package/src/ui/toggle.tsx +43 -0
  172. package/src/ui/tooltip.tsx +53 -0
  173. package/src/ui/use-toast.ts +3 -0
  174. package/src/utils.ts +8 -0
@@ -0,0 +1,85 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { Editor } from "@tiptap/core";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import { createSharedEditorExtensions } from "./extensions.js";
7
+
8
+ /**
9
+ * The `disableHistory` lever turns OFF StarterKit's prosemirror-history for a
10
+ * controlled (non-collab) editor whose host owns its own undo authority — the
11
+ * plan editor, whose authoritative `blocks[]` tree holds block data the
12
+ * ProseMirror doc never stores, so PM history can't be the undo authority.
13
+ *
14
+ * Contract:
15
+ * - default (history ON): cmd+z reverts a text edit (every existing embedder).
16
+ * - disableHistory: true: the Mod-z keymap is not bound, so cmd+z is NOT
17
+ * handled and the edit is NOT reverted — leaving the host's own cmd+z
18
+ * handler (a capture-phase listener, in the plan editor) as the sole undo.
19
+ */
20
+
21
+ function makeEditor(disableHistory: boolean): Editor {
22
+ const element = document.createElement("div");
23
+ document.body.appendChild(element);
24
+ return new Editor({
25
+ element,
26
+ extensions: createSharedEditorExtensions({ disableHistory }),
27
+ // Seed via a PM JSON doc (not an HTML/markdown string) so the baseline is a
28
+ // real paragraph "hello" and the first undoable step is the edit below.
29
+ content: {
30
+ type: "doc",
31
+ content: [
32
+ { type: "paragraph", content: [{ type: "text", text: "hello" }] },
33
+ ],
34
+ },
35
+ });
36
+ }
37
+
38
+ function typeAtEnd(editor: Editor, text: string): void {
39
+ editor.chain().focus("end").insertContent(text).run();
40
+ }
41
+
42
+ function pressModZ(editor: Editor): boolean {
43
+ const isMac =
44
+ typeof navigator !== "undefined" &&
45
+ /Mac|iP(hone|ad|od)/.test(navigator.platform || "");
46
+ const event = new KeyboardEvent("keydown", {
47
+ key: "z",
48
+ code: "KeyZ",
49
+ keyCode: 90,
50
+ which: 90,
51
+ metaKey: isMac,
52
+ ctrlKey: !isMac,
53
+ bubbles: true,
54
+ cancelable: true,
55
+ } as KeyboardEventInit);
56
+ return (
57
+ editor.view.someProp("handleKeyDown", (f) => f(editor.view, event)) ?? false
58
+ );
59
+ }
60
+
61
+ describe("disableHistory editor lever", () => {
62
+ it("default (history ON): cmd+z reverts a text edit", () => {
63
+ const editor = makeEditor(false);
64
+ typeAtEnd(editor, " world");
65
+ expect(editor.getText()).toBe("hello world");
66
+
67
+ const handled = pressModZ(editor);
68
+ expect(handled).toBe(true);
69
+ expect(editor.getText()).toBe("hello");
70
+ editor.destroy();
71
+ });
72
+
73
+ it("disableHistory: true — cmd+z is NOT handled and the edit is NOT reverted", () => {
74
+ const editor = makeEditor(true);
75
+ typeAtEnd(editor, " world");
76
+ expect(editor.getText()).toBe("hello world");
77
+
78
+ const handled = pressModZ(editor);
79
+ // No history plugin → no Mod-z binding → the keymap does not consume it,
80
+ // and the document is unchanged. The host's own undo authority handles cmd+z.
81
+ expect(handled).toBe(false);
82
+ expect(editor.getText()).toBe("hello world");
83
+ editor.destroy();
84
+ });
85
+ });
@@ -0,0 +1,402 @@
1
+ import type { Extension, Node, Mark } from "@tiptap/core";
2
+ import Collaboration from "@tiptap/extension-collaboration";
3
+ import CollaborationCaret from "@tiptap/extension-collaboration-caret";
4
+ import Link from "@tiptap/extension-link";
5
+ import Placeholder from "@tiptap/extension-placeholder";
6
+ import { Table } from "@tiptap/extension-table";
7
+ import { TableCell } from "@tiptap/extension-table-cell";
8
+ import { TableHeader } from "@tiptap/extension-table-header";
9
+ import { TableRow } from "@tiptap/extension-table-row";
10
+ import TaskItem from "@tiptap/extension-task-item";
11
+ import TaskList from "@tiptap/extension-task-list";
12
+ import StarterKit from "@tiptap/starter-kit";
13
+ import type { StarterKitOptions } from "@tiptap/starter-kit";
14
+ import bash from "highlight.js/lib/languages/bash";
15
+ import css from "highlight.js/lib/languages/css";
16
+ import javascript from "highlight.js/lib/languages/javascript";
17
+ import json from "highlight.js/lib/languages/json";
18
+ import markdown from "highlight.js/lib/languages/markdown";
19
+ import python from "highlight.js/lib/languages/python";
20
+ import sql from "highlight.js/lib/languages/sql";
21
+ import typescript from "highlight.js/lib/languages/typescript";
22
+ import xml from "highlight.js/lib/languages/xml";
23
+ import yaml from "highlight.js/lib/languages/yaml";
24
+ import { createLowlight } from "lowlight";
25
+ import { Markdown } from "tiptap-markdown";
26
+
27
+ /**
28
+ * Shared lowlight instance for the editor's syntax-highlighted code blocks. A
29
+ * curated grammar set (aliases like ts/tsx, js/jsx, html, sh, py, yml, md come
30
+ * for free from each grammar) keeps the editor bundle lean while matching the
31
+ * languages the read-side Shiki surfaces (`code-tabs`) support. highlight.js is
32
+ * synchronous, which is what a live ProseMirror editor needs — Shiki is async
33
+ * and only used for read-only render paths.
34
+ */
35
+ const codeLowlight = createLowlight({
36
+ bash,
37
+ css,
38
+ javascript,
39
+ json,
40
+ markdown,
41
+ python,
42
+ sql,
43
+ typescript,
44
+ xml,
45
+ yaml,
46
+ });
47
+ import type { Awareness } from "y-protocols/awareness";
48
+ import type { Doc as YDoc } from "yjs";
49
+
50
+ import { createCodeBlockNode } from "./CodeBlockNode.js";
51
+ import { createImageExtension, type ImageUploadFn } from "./ImageExtension.js";
52
+
53
+ /**
54
+ * Markdown dialect the editor parses/serializes.
55
+ *
56
+ * - `gfm` — GitHub-Flavored Markdown. No raw HTML passthrough. The byte-stable
57
+ * serialization used by Plans (see RichMarkdownEditor.roundtrip.spec.ts).
58
+ * - `nfm` — the Notion-Flavored Markdown superset used by the Content editor,
59
+ * which opts into inline HTML so Notion-specific blocks round-trip.
60
+ */
61
+ export type RichMarkdownDialect = "gfm" | "nfm";
62
+
63
+ /**
64
+ * Editor preset. Schema-neutral today (both presets share the base schema),
65
+ * but threaded through so an app can branch schema/behavior per preset without
66
+ * a new factory. The collab/markdown wiring is preset-independent.
67
+ */
68
+ export type RichMarkdownEditorPreset = "plan" | "content";
69
+
70
+ /** User info used to label this client's collaborative cursor. */
71
+ export interface RichMarkdownCollabUser {
72
+ name: string;
73
+ color: string;
74
+ email?: string;
75
+ }
76
+
77
+ /** Optional collaborative-editing inputs for the shared editor. */
78
+ export interface SharedEditorCollab {
79
+ /**
80
+ * Yjs document for collaborative editing. When present the editor binds the
81
+ * shared {@link Collaboration} (+ {@link CollaborationCaret} when awareness
82
+ * is set) extensions and StarterKit's built-in undo/redo is disabled (Yjs
83
+ * owns history). When absent the editor is a controlled `value`/`onChange`
84
+ * editor.
85
+ */
86
+ ydoc?: YDoc | null;
87
+ /** Shared awareness instance for live multi-user cursors. */
88
+ awareness?: Awareness | null;
89
+ /** Current user info for the collaborative cursor label. */
90
+ user?: RichMarkdownCollabUser | null;
91
+ }
92
+
93
+ /** Toggle the optional base extensions on/off per app. All default to `true`. */
94
+ export interface SharedEditorFeatures {
95
+ /** GFM pipe tables (Table + TableRow + TableHeader + TableCell). */
96
+ tables?: boolean;
97
+ /** Task / checklist lists (TaskList + TaskItem). */
98
+ tasks?: boolean;
99
+ /** Inline links (the `Link` mark). When off, links fall back to plain text. */
100
+ link?: boolean;
101
+ /** Fenced code blocks. Disabling lets an app inject its own code-block node. */
102
+ codeBlock?: boolean;
103
+ /**
104
+ * The built-in {@link Placeholder} extension. Default `true`. Apps that need a
105
+ * bespoke placeholder resolver (per-node-type labels, ancestor-aware text)
106
+ * disable this and supply their own Placeholder via `extraExtensions`.
107
+ */
108
+ placeholder?: boolean;
109
+ /**
110
+ * The built-in dialect-keyed {@link Markdown} serializer. Default `true`.
111
+ * Apps with a custom serializer (e.g. Content's NFM converter, which does NOT
112
+ * round-trip through tiptap-markdown's storage) disable this and own the
113
+ * serialize/parse pipeline themselves. The Markdown extension is still added
114
+ * so paste/clipboard transforms work — disable it only when supplying your own
115
+ * Markdown configuration via the {@link CreateSharedEditorExtensionsOptions.markdown}
116
+ * option instead.
117
+ */
118
+ markdown?: boolean;
119
+ /**
120
+ * The shared block-level image node (`@tiptap/extension-image`). Default
121
+ * `false` so existing embedders are unchanged. When `true`, images
122
+ * serialize to GFM `![alt](src)` (source-syncable) and — when an
123
+ * {@link CreateSharedEditorExtensionsOptions.onImageUpload} function is
124
+ * supplied — paste / drop of local image files uploads through it. Content
125
+ * leaves this off and injects its own richer image node via
126
+ * `extraExtensions`, so the two never collide.
127
+ */
128
+ image?: boolean;
129
+ }
130
+
131
+ export interface CreateSharedEditorExtensionsOptions {
132
+ /** Markdown dialect; selects the keyed {@link Markdown} config. */
133
+ dialect?: RichMarkdownDialect;
134
+ /** Preset hook (schema-neutral today). */
135
+ preset?: RichMarkdownEditorPreset;
136
+ /** Empty-block placeholder text (headings get their own labels). */
137
+ placeholder?: string;
138
+ /** Toggle individual base extensions. */
139
+ features?: SharedEditorFeatures;
140
+ /**
141
+ * Extra StarterKit options merged over the shared defaults. Lets an app turn
142
+ * off StarterKit nodes it replaces (Content swaps in its own paragraph /
143
+ * blockquote / code block) or pass a custom dropcursor, while still sharing
144
+ * the rest of the StarterKit base + the collab undo/redo gating. The shared
145
+ * defaults (`heading` levels 1-4, `link: false`, the default dropcursor, and
146
+ * `undoRedo: false` in collab mode) are applied first and can be overridden
147
+ * key-by-key here.
148
+ */
149
+ starterKit?: Partial<StarterKitOptions>;
150
+ /**
151
+ * Custom {@link Markdown} configuration. Replaces the dialect-keyed config from
152
+ * {@link MARKDOWN_DIALECT_CONFIG} when provided. Only used when
153
+ * `features.markdown !== false`; apps that own the whole markdown pipeline (no
154
+ * tiptap-markdown serialization at all) should set `features.markdown: false`
155
+ * and add their own configured Markdown extension via `extraExtensions`.
156
+ */
157
+ markdown?: Parameters<typeof Markdown.configure>[0];
158
+ /**
159
+ * App-specific extensions (Notion nodes, media, drag handles, comment
160
+ * anchors, etc.) appended LAST so they bind over the shared base schema and
161
+ * the optional Collaboration extensions still mount after them.
162
+ */
163
+ extraExtensions?: Array<Extension | Node | Mark>;
164
+ /** Optional collaborative-editing wiring. */
165
+ collab?: SharedEditorCollab | null;
166
+ /**
167
+ * Disable StarterKit's built-in undo/redo (prosemirror-history) for a
168
+ * controlled (non-collab) editor whose host owns its own undo authority.
169
+ * Default `false`. When a `collab.ydoc` is present, undo/redo is ALWAYS
170
+ * disabled regardless of this flag (Yjs owns history); this flag is the
171
+ * non-collab equivalent. The plan editor sets it so a single app-level undo
172
+ * stack (over the authoritative `blocks[]` tree, which includes block data
173
+ * the ProseMirror doc never stores) is the sole cmd+z authority — otherwise
174
+ * PM history and the app stack would both fire, and PM history can't see
175
+ * block-option edits at all.
176
+ */
177
+ disableHistory?: boolean;
178
+ /**
179
+ * Injectable image uploader for the shared image block. Only used when
180
+ * `features.image` is on. Turns a picked / pasted / dropped image File into a
181
+ * hosted `{ src, alt? }`. Plans pass `uploadEditorImage` (the framework
182
+ * `upload-image` action). When omitted, the image block still renders and
183
+ * round-trips `![alt](src)` markdown but cannot ingest local files.
184
+ */
185
+ onImageUpload?: ImageUploadFn | null;
186
+ }
187
+
188
+ /**
189
+ * tiptap-markdown configuration, keyed by dialect. This is the single source of
190
+ * truth for how each dialect parses/serializes markdown so the editor component
191
+ * and the round-trip fidelity test can never drift apart.
192
+ *
193
+ * tiptap-markdown re-serializes the whole document on every edit, so the goal
194
+ * for GFM is `serialize(parse(markdown)) === markdown` for the markdown plans
195
+ * actually contain. We deliberately keep tiptap-markdown's own defaults
196
+ * (`bulletListMarker: "-"`, `tightLists: true`, `linkify: false`,
197
+ * `breaks: false`) because those produce the most byte-stable GFM. See
198
+ * RichMarkdownEditor.roundtrip.spec.ts for the pinned corpus.
199
+ *
200
+ * NFM (Content) opts into inline HTML passthrough (`html: true`) so
201
+ * Notion-specific blocks survive a markdown round-trip; the rest mirrors the
202
+ * Content editor's existing `Markdown.configure` call.
203
+ */
204
+ export const MARKDOWN_DIALECT_CONFIG: Record<
205
+ RichMarkdownDialect,
206
+ Parameters<typeof Markdown.configure>[0]
207
+ > = {
208
+ gfm: {
209
+ // GFM plans are the common case and must never gain raw HTML as a second
210
+ // representation.
211
+ html: false,
212
+ // Keep tiptap-markdown's defaults that minimise first-edit normalisation
213
+ // churn (see roundtrip spec). Listed explicitly so the contract is
214
+ // self-documenting rather than relying on the package defaults.
215
+ bulletListMarker: "-",
216
+ tightLists: true,
217
+ linkify: false,
218
+ breaks: false,
219
+ transformPastedText: true,
220
+ transformCopiedText: true,
221
+ },
222
+ nfm: {
223
+ // NFM is a superset that allows inline HTML so Notion blocks round-trip.
224
+ html: true,
225
+ transformPastedText: true,
226
+ transformCopiedText: true,
227
+ },
228
+ };
229
+
230
+ const DEFAULT_FEATURES: Required<SharedEditorFeatures> = {
231
+ tables: true,
232
+ tasks: true,
233
+ link: true,
234
+ codeBlock: true,
235
+ placeholder: true,
236
+ markdown: true,
237
+ // Off by default: only Plans opt in today. Content injects its own richer
238
+ // image node via `extraExtensions` and must not get a second `image` node.
239
+ image: false,
240
+ };
241
+
242
+ /**
243
+ * The ONE editor extension factory shared by every embedder (Plans today,
244
+ * Content next). It assembles the base Tiptap schema (StarterKit + Placeholder
245
+ * + Link + tasks + tables + code block), the dialect-keyed {@link Markdown}
246
+ * serializer, the optional Collaboration stack, and finally any app-specific
247
+ * `extraExtensions`.
248
+ *
249
+ * Ordering matters:
250
+ * 1. Base schema (StarterKit first so its nodes/marks register; `starterKit`
251
+ * overrides let an app disable replaced nodes / swap the dropcursor).
252
+ * 2. dialect-keyed Markdown serializer (suppressible via `features.markdown`
253
+ * for apps that own the whole serialize/parse pipeline, e.g. Content's NFM).
254
+ * 3. `extraExtensions` (Notion/media/etc.) — appended before Collaboration so
255
+ * apps can extend the schema and Collaboration still binds over the full
256
+ * schema.
257
+ * 4. Collaboration (+ CollaborationCaret) LAST so they bind over everything.
258
+ *
259
+ * Content (the NFM editor) drives this factory with `features.placeholder` and
260
+ * `features.markdown` off, `features.tasks/tables/link` off where it ships its
261
+ * own, a `starterKit` override disabling paragraph/blockquote/codeBlock, and all
262
+ * Notion/media/fidelity nodes + its own Markdown(NFM)/Placeholder via
263
+ * `extraExtensions` — so it shares the StarterKit base + the collab wiring while
264
+ * owning its byte-identical NFM serializer.
265
+ */
266
+ export function createSharedEditorExtensions({
267
+ dialect = "gfm",
268
+ // `preset` is accepted and forwarded for future preset-specific schema
269
+ // branches; it is currently schema-neutral.
270
+ preset: _preset = "plan",
271
+ placeholder = "Type '/' for commands...",
272
+ features,
273
+ starterKit,
274
+ markdown,
275
+ extraExtensions = [],
276
+ collab = null,
277
+ onImageUpload = null,
278
+ disableHistory = false,
279
+ }: CreateSharedEditorExtensionsOptions = {}): Array<Extension | Node | Mark> {
280
+ const feat = { ...DEFAULT_FEATURES, ...(features ?? {}) };
281
+ const ydoc = collab?.ydoc ?? null;
282
+ const awareness = collab?.awareness ?? null;
283
+ const user = collab?.user ?? null;
284
+
285
+ const exts: Array<Extension | Node | Mark> = [
286
+ StarterKit.configure({
287
+ heading: { levels: [1, 2, 3, 4] },
288
+ link: false,
289
+ // StarterKit's plain code block is always disabled; when enabled we add the
290
+ // syntax-highlighting `CodeBlockLowlight` (same `codeBlock` node) below.
291
+ codeBlock: false,
292
+ dropcursor: { color: "hsl(var(--ring))", width: 2 },
293
+ // Yjs owns undo/redo when Collaboration is active; the StarterKit history
294
+ // plugin and the CRDT cannot both track undo without corrupting state.
295
+ // `disableHistory` is the non-collab equivalent: a controlled editor whose
296
+ // host (e.g. the plan editor) owns a single app-level undo authority.
297
+ ...(ydoc || disableHistory ? { undoRedo: false } : {}),
298
+ // App overrides last so embedders can disable replaced nodes (paragraph,
299
+ // blockquote, code block) or swap the dropcursor while keeping the shared
300
+ // base + the collab undo/redo gating above.
301
+ ...(starterKit ?? {}),
302
+ }),
303
+ ];
304
+
305
+ // Notion-style syntax-highlighted code block (replaces StarterKit's plain one)
306
+ // only when the embedder opts in via `features.codeBlock`. The shared node adds
307
+ // a language picker header (Auto-detects by default) over `CodeBlockLowlight`,
308
+ // so fenced markdown still round-trips byte-identically. Content disables this
309
+ // and ships its own code node, so this affects Plans (and future opt-in apps)
310
+ // alone.
311
+ if (feat.codeBlock) {
312
+ exts.push(createCodeBlockNode({ lowlight: codeLowlight }));
313
+ }
314
+
315
+ if (feat.placeholder) {
316
+ exts.push(
317
+ Placeholder.configure({
318
+ placeholder: ({ node }) => {
319
+ if (node.type.name === "heading") {
320
+ const level = node.attrs.level;
321
+ if (level === 1) return "Heading 1";
322
+ if (level === 2) return "Heading 2";
323
+ if (level === 3) return "Heading 3";
324
+ return "Heading 4";
325
+ }
326
+ return placeholder;
327
+ },
328
+ showOnlyWhenEditable: true,
329
+ showOnlyCurrent: true,
330
+ }),
331
+ );
332
+ }
333
+
334
+ if (feat.link) {
335
+ exts.push(
336
+ Link.configure({
337
+ openOnClick: false,
338
+ HTMLAttributes: { class: "an-rich-md-link" },
339
+ }),
340
+ );
341
+ }
342
+
343
+ if (feat.tasks) {
344
+ exts.push(
345
+ TaskList.configure({
346
+ HTMLAttributes: { class: "an-rich-md-task-list" },
347
+ }),
348
+ TaskItem.configure({ nested: true }),
349
+ );
350
+ }
351
+
352
+ if (feat.tables) {
353
+ exts.push(
354
+ Table.configure({
355
+ resizable: false,
356
+ HTMLAttributes: { class: "an-rich-md-table" },
357
+ }),
358
+ TableRow,
359
+ TableHeader,
360
+ TableCell,
361
+ );
362
+ }
363
+
364
+ if (feat.markdown) {
365
+ exts.push(Markdown.configure(markdown ?? MARKDOWN_DIALECT_CONFIG[dialect]));
366
+ }
367
+
368
+ // Shared block-level image node. The node is named `image`, so when
369
+ // `features.markdown` is on, tiptap-markdown serializes it through its
370
+ // built-in `defaultMarkdownSerializer.nodes.image` fallback → `![alt](src)`
371
+ // (no width-as-HTML override here, so GFM stays byte-stable and
372
+ // source-syncable). With an `onImageUpload` it accepts paste/drop uploads.
373
+ if (feat.image) {
374
+ exts.push(createImageExtension({ onImageUpload }));
375
+ }
376
+
377
+ // App-specific extensions (Notion/media/drag handles/comments). Appended
378
+ // before Collaboration so they can extend the schema and Collaboration binds
379
+ // over the full set.
380
+ if (extraExtensions.length > 0) {
381
+ exts.push(...extraExtensions);
382
+ }
383
+
384
+ // Collaborative editing via the shared Y.Doc. Markdown stays the canonical
385
+ // saved representation (onChange serializes it); the Y.Doc is transient live
386
+ // state only. Appended last so it binds over the configured schema above.
387
+ if (ydoc) {
388
+ exts.push(Collaboration.configure({ document: ydoc }));
389
+ // Live multi-user cursors. Only mounted alongside a Y.Doc so the standalone
390
+ // controlled editor (today's plan/content behavior) is untouched.
391
+ if (awareness) {
392
+ exts.push(
393
+ CollaborationCaret.configure({
394
+ provider: { awareness },
395
+ user: user ?? { name: "Anonymous", color: "#999" },
396
+ }),
397
+ );
398
+ }
399
+ }
400
+
401
+ return exts;
402
+ }