@agent-native/core 0.38.0 → 0.39.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 (156) hide show
  1. package/dist/cli/create.d.ts.map +1 -1
  2. package/dist/cli/create.js +8 -1
  3. package/dist/cli/create.js.map +1 -1
  4. package/dist/cli/skills.d.ts +5 -4
  5. package/dist/cli/skills.d.ts.map +1 -1
  6. package/dist/cli/skills.js +450 -125
  7. package/dist/cli/skills.js.map +1 -1
  8. package/dist/client/blocks/BlockView.d.ts +13 -4
  9. package/dist/client/blocks/BlockView.d.ts.map +1 -1
  10. package/dist/client/blocks/BlockView.js +34 -13
  11. package/dist/client/blocks/BlockView.js.map +1 -1
  12. package/dist/client/blocks/SchemaBlockEditor.d.ts.map +1 -1
  13. package/dist/client/blocks/SchemaBlockEditor.js +96 -3
  14. package/dist/client/blocks/SchemaBlockEditor.js.map +1 -1
  15. package/dist/client/blocks/index.d.ts +18 -1
  16. package/dist/client/blocks/index.d.ts.map +1 -1
  17. package/dist/client/blocks/index.js +26 -1
  18. package/dist/client/blocks/index.js.map +1 -1
  19. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts +6 -0
  20. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -0
  21. package/dist/client/blocks/library/AnnotatedCodeBlock.js +135 -0
  22. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -0
  23. package/dist/client/blocks/library/ApiEndpointBlock.d.ts +20 -0
  24. package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -0
  25. package/dist/client/blocks/library/ApiEndpointBlock.js +131 -0
  26. package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -0
  27. package/dist/client/blocks/library/DataModelBlock.d.ts +28 -0
  28. package/dist/client/blocks/library/DataModelBlock.d.ts.map +1 -0
  29. package/dist/client/blocks/library/DataModelBlock.js +222 -0
  30. package/dist/client/blocks/library/DataModelBlock.js.map +1 -0
  31. package/dist/client/blocks/library/DiffBlock.d.ts +6 -0
  32. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -0
  33. package/dist/client/blocks/library/DiffBlock.js +293 -0
  34. package/dist/client/blocks/library/DiffBlock.js.map +1 -0
  35. package/dist/client/blocks/library/FileTreeBlock.d.ts +23 -0
  36. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -0
  37. package/dist/client/blocks/library/FileTreeBlock.js +225 -0
  38. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -0
  39. package/dist/client/blocks/library/JsonExplorerBlock.d.ts +19 -0
  40. package/dist/client/blocks/library/JsonExplorerBlock.d.ts.map +1 -0
  41. package/dist/client/blocks/library/JsonExplorerBlock.js +171 -0
  42. package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -0
  43. package/dist/client/blocks/library/MermaidBlock.d.ts +17 -0
  44. package/dist/client/blocks/library/MermaidBlock.d.ts.map +1 -0
  45. package/dist/client/blocks/library/MermaidBlock.js +131 -0
  46. package/dist/client/blocks/library/MermaidBlock.js.map +1 -0
  47. package/dist/client/blocks/library/OpenApiSpecBlock.d.ts +19 -0
  48. package/dist/client/blocks/library/OpenApiSpecBlock.d.ts.map +1 -0
  49. package/dist/client/blocks/library/OpenApiSpecBlock.js +494 -0
  50. package/dist/client/blocks/library/OpenApiSpecBlock.js.map +1 -0
  51. package/dist/client/blocks/library/annotated-code.config.d.ts +58 -0
  52. package/dist/client/blocks/library/annotated-code.config.d.ts.map +1 -0
  53. package/dist/client/blocks/library/annotated-code.config.js +53 -0
  54. package/dist/client/blocks/library/annotated-code.config.js.map +1 -0
  55. package/dist/client/blocks/library/api-endpoint.config.d.ts +71 -0
  56. package/dist/client/blocks/library/api-endpoint.config.d.ts.map +1 -0
  57. package/dist/client/blocks/library/api-endpoint.config.js +91 -0
  58. package/dist/client/blocks/library/api-endpoint.config.js.map +1 -0
  59. package/dist/client/blocks/library/checklist.d.ts.map +1 -1
  60. package/dist/client/blocks/library/checklist.js +3 -1
  61. package/dist/client/blocks/library/checklist.js.map +1 -1
  62. package/dist/client/blocks/library/code-tabs.js +1 -1
  63. package/dist/client/blocks/library/code-tabs.js.map +1 -1
  64. package/dist/client/blocks/library/data-model.config.d.ts +72 -0
  65. package/dist/client/blocks/library/data-model.config.d.ts.map +1 -0
  66. package/dist/client/blocks/library/data-model.config.js +59 -0
  67. package/dist/client/blocks/library/data-model.config.js.map +1 -0
  68. package/dist/client/blocks/library/dev-doc-ui.d.ts +49 -0
  69. package/dist/client/blocks/library/dev-doc-ui.d.ts.map +1 -0
  70. package/dist/client/blocks/library/dev-doc-ui.js +50 -0
  71. package/dist/client/blocks/library/dev-doc-ui.js.map +1 -0
  72. package/dist/client/blocks/library/diff.config.d.ts +41 -0
  73. package/dist/client/blocks/library/diff.config.d.ts.map +1 -0
  74. package/dist/client/blocks/library/diff.config.js +34 -0
  75. package/dist/client/blocks/library/diff.config.js.map +1 -0
  76. package/dist/client/blocks/library/file-tree.config.d.ts +59 -0
  77. package/dist/client/blocks/library/file-tree.config.d.ts.map +1 -0
  78. package/dist/client/blocks/library/file-tree.config.js +45 -0
  79. package/dist/client/blocks/library/file-tree.config.js.map +1 -0
  80. package/dist/client/blocks/library/html.d.ts.map +1 -1
  81. package/dist/client/blocks/library/html.js +4 -1
  82. package/dist/client/blocks/library/html.js.map +1 -1
  83. package/dist/client/blocks/library/json-explorer.config.d.ts +46 -0
  84. package/dist/client/blocks/library/json-explorer.config.d.ts.map +1 -0
  85. package/dist/client/blocks/library/json-explorer.config.js +28 -0
  86. package/dist/client/blocks/library/json-explorer.config.js.map +1 -0
  87. package/dist/client/blocks/library/mermaid.config.d.ts +32 -0
  88. package/dist/client/blocks/library/mermaid.config.d.ts.map +1 -0
  89. package/dist/client/blocks/library/mermaid.config.js +24 -0
  90. package/dist/client/blocks/library/mermaid.config.js.map +1 -0
  91. package/dist/client/blocks/library/openapi-spec.config.d.ts +49 -0
  92. package/dist/client/blocks/library/openapi-spec.config.d.ts.map +1 -0
  93. package/dist/client/blocks/library/openapi-spec.config.js +24 -0
  94. package/dist/client/blocks/library/openapi-spec.config.js.map +1 -0
  95. package/dist/client/blocks/library/server-specs.d.ts +35 -0
  96. package/dist/client/blocks/library/server-specs.d.ts.map +1 -0
  97. package/dist/client/blocks/library/server-specs.js +171 -0
  98. package/dist/client/blocks/library/server-specs.js.map +1 -0
  99. package/dist/client/blocks/library/specs.d.ts +29 -0
  100. package/dist/client/blocks/library/specs.d.ts.map +1 -0
  101. package/dist/client/blocks/library/specs.js +229 -0
  102. package/dist/client/blocks/library/specs.js.map +1 -0
  103. package/dist/client/blocks/library/table.d.ts.map +1 -1
  104. package/dist/client/blocks/library/table.js +3 -1
  105. package/dist/client/blocks/library/table.js.map +1 -1
  106. package/dist/client/blocks/library/tabs.js +1 -1
  107. package/dist/client/blocks/library/tabs.js.map +1 -1
  108. package/dist/client/blocks/registry.d.ts +8 -0
  109. package/dist/client/blocks/registry.d.ts.map +1 -1
  110. package/dist/client/blocks/registry.js +15 -0
  111. package/dist/client/blocks/registry.js.map +1 -1
  112. package/dist/client/blocks/server.d.ts +9 -0
  113. package/dist/client/blocks/server.d.ts.map +1 -1
  114. package/dist/client/blocks/server.js +16 -0
  115. package/dist/client/blocks/server.js.map +1 -1
  116. package/dist/client/blocks/types.d.ts +40 -0
  117. package/dist/client/blocks/types.d.ts.map +1 -1
  118. package/dist/client/blocks/types.js.map +1 -1
  119. package/dist/client/index.d.ts +2 -1
  120. package/dist/client/index.d.ts.map +1 -1
  121. package/dist/client/index.js +10 -1
  122. package/dist/client/index.js.map +1 -1
  123. package/dist/client/rich-markdown-editor/DragHandle.d.ts +52 -0
  124. package/dist/client/rich-markdown-editor/DragHandle.d.ts.map +1 -0
  125. package/dist/client/rich-markdown-editor/DragHandle.js +403 -0
  126. package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -0
  127. package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts +97 -0
  128. package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -0
  129. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +214 -0
  130. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -0
  131. package/dist/client/rich-markdown-editor/RunId.d.ts +28 -0
  132. package/dist/client/rich-markdown-editor/RunId.d.ts.map +1 -0
  133. package/dist/client/rich-markdown-editor/RunId.js +60 -0
  134. package/dist/client/rich-markdown-editor/RunId.js.map +1 -0
  135. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +25 -1
  136. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
  137. package/dist/client/rich-markdown-editor/SharedRichEditor.js +14 -5
  138. package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
  139. package/dist/client/rich-markdown-editor/gfmDoc.d.ts +24 -0
  140. package/dist/client/rich-markdown-editor/gfmDoc.d.ts.map +1 -0
  141. package/dist/client/rich-markdown-editor/gfmDoc.js +83 -0
  142. package/dist/client/rich-markdown-editor/gfmDoc.js.map +1 -0
  143. package/dist/client/rich-markdown-editor/index.d.ts +5 -0
  144. package/dist/client/rich-markdown-editor/index.d.ts.map +1 -1
  145. package/dist/client/rich-markdown-editor/index.js +5 -0
  146. package/dist/client/rich-markdown-editor/index.js.map +1 -1
  147. package/dist/client/rich-markdown-editor/registrySlashCommands.d.ts +46 -0
  148. package/dist/client/rich-markdown-editor/registrySlashCommands.d.ts.map +1 -0
  149. package/dist/client/rich-markdown-editor/registrySlashCommands.js +13 -0
  150. package/dist/client/rich-markdown-editor/registrySlashCommands.js.map +1 -0
  151. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  152. package/dist/client/rich-markdown-editor/useCollabReconcile.js +33 -0
  153. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  154. package/docs/content/template-plan.md +19 -4
  155. package/docs/content/visual-plans.md +3 -1
  156. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ import { Editor } from "@tiptap/core";
2
+ import { createSharedEditorExtensions } from "./extensions.js";
3
+ import { RunId } from "./RunId.js";
4
+ /**
5
+ * The GFM ↔ ProseMirror primitive for the plan single-doc editor.
6
+ *
7
+ * Plans keep `PlanContent.blocks[]` as the source of truth, but the editor is
8
+ * ONE ProseMirror/Tiptap document. The `doc ↔ blocks[]` serializer
9
+ * (`templates/plan/shared/plan-doc.ts`) needs to turn a `rich-text` block's GFM
10
+ * markdown into prose nodes and back. This module is that primitive.
11
+ *
12
+ * Both directions go through a SINGLE headless Tiptap {@link Editor} built from
13
+ * the exact same `createSharedEditorExtensions` config the live plan editor
14
+ * uses (`dialect: "gfm"`, `features.image: true`) plus the {@link RunId}
15
+ * extension, so the schema and the GFM serializer can never drift from the live
16
+ * editor. The instance is created lazily on first use and reused across calls.
17
+ *
18
+ * The headless editor needs a DOM (ProseMirror's `EditorView`). It works under
19
+ * `happy-dom` in vitest (see `gfmDoc.spec.ts`) and under the real browser DOM
20
+ * in production. `createElement` is used rather than mounting into the page so
21
+ * nothing is ever attached to the document.
22
+ */
23
+ let sharedEditor = null;
24
+ /**
25
+ * Lazily build (and memoize) the single headless editor. Throws if no DOM is
26
+ * available — this primitive is for the client / jsdom-style test envs only.
27
+ */
28
+ function getSharedEditor() {
29
+ if (sharedEditor)
30
+ return sharedEditor;
31
+ if (typeof document === "undefined") {
32
+ throw new Error("gfmDoc requires a DOM (document). It runs in the browser and in jsdom/happy-dom tests, not in a bare Node server context.");
33
+ }
34
+ sharedEditor = new Editor({
35
+ element: document.createElement("div"),
36
+ extensions: createSharedEditorExtensions({
37
+ dialect: "gfm",
38
+ features: { image: true },
39
+ extraExtensions: [RunId],
40
+ }),
41
+ content: "",
42
+ });
43
+ return sharedEditor;
44
+ }
45
+ /** Reads the GFM markdown out of the tiptap-markdown storage. */
46
+ function getMarkdown(editor) {
47
+ const storage = editor.storage;
48
+ return storage.markdown?.getMarkdown?.() ?? "";
49
+ }
50
+ /**
51
+ * Parse a GFM markdown string into an array of top-level ProseMirror node JSON
52
+ * (paragraph / heading / list / table / code block / etc.), matching the live
53
+ * plan editor schema (`createSharedEditorExtensions({ dialect: "gfm",
54
+ * features: { image: true } })`) plus the {@link RunId} attribute.
55
+ *
56
+ * `tiptap-markdown` registers the markdown parser, so handing the raw markdown
57
+ * string to `setContent` deserializes it (the same path the live editor uses
58
+ * when it seeds `content: markdown`). Returns the doc's child nodes; an empty
59
+ * string yields a single empty paragraph.
60
+ */
61
+ export function gfmToProseJSON(markdown) {
62
+ const editor = getSharedEditor();
63
+ // `emitUpdate: false` keeps this a pure transform with no side effects on any
64
+ // (non-existent) consumers of the headless editor's update stream.
65
+ editor.commands.setContent(markdown, { emitUpdate: false });
66
+ return editor.getJSON().content ?? [];
67
+ }
68
+ /**
69
+ * Serialize an array of top-level ProseMirror node JSON into GFM markdown. The
70
+ * `runId` attribute is omitted by GFM (the {@link RunId} extension registers no
71
+ * markdown serializer), so it never leaks into the saved markdown.
72
+ *
73
+ * The nodes are wrapped in a `doc` and set on the shared editor, then the GFM
74
+ * markdown is read from the tiptap-markdown storage — the exact serializer the
75
+ * live editor persists with, so output is byte-stable with the live save path.
76
+ */
77
+ export function proseJSONToGfm(nodes) {
78
+ const editor = getSharedEditor();
79
+ const content = nodes.length > 0 ? nodes : [{ type: "paragraph" }];
80
+ editor.commands.setContent({ type: "doc", content }, { emitUpdate: false });
81
+ return getMarkdown(editor);
82
+ }
83
+ //# sourceMappingURL=gfmDoc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gfmDoc.js","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/gfmDoc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC;;;;;;;;;;;;;;;;;;GAkBG;AAEH,IAAI,YAAY,GAAkB,IAAI,CAAC;AAEvC;;;GAGG;AACH,SAAS,eAAe;IACtB,IAAI,YAAY;QAAE,OAAO,YAAY,CAAC;IACtC,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,2HAA2H,CAC5H,CAAC;IACJ,CAAC;IACD,YAAY,GAAG,IAAI,MAAM,CAAC;QACxB,OAAO,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC;QACtC,UAAU,EAAE,4BAA4B,CAAC;YACvC,OAAO,EAAE,KAAK;YACd,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;YACzB,eAAe,EAAE,CAAC,KAAK,CAAC;SACzB,CAAC;QACF,OAAO,EAAE,EAAE;KACZ,CAAC,CAAC;IACH,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,iEAAiE;AACjE,SAAS,WAAW,CAAC,MAAc;IACjC,MAAM,OAAO,GAAG,MAAM,CAAC,OAEtB,CAAC;IACF,OAAO,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;IACjC,8EAA8E;IAC9E,mEAAmE;IACnE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC;AACxC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,KAAoB;IACjD,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IACnE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC","sourcesContent":["import { Editor, type JSONContent } from \"@tiptap/core\";\nimport { createSharedEditorExtensions } from \"./extensions.js\";\nimport { RunId } from \"./RunId.js\";\n\n/**\n * The GFM ↔ ProseMirror primitive for the plan single-doc editor.\n *\n * Plans keep `PlanContent.blocks[]` as the source of truth, but the editor is\n * ONE ProseMirror/Tiptap document. The `doc ↔ blocks[]` serializer\n * (`templates/plan/shared/plan-doc.ts`) needs to turn a `rich-text` block's GFM\n * markdown into prose nodes and back. This module is that primitive.\n *\n * Both directions go through a SINGLE headless Tiptap {@link Editor} built from\n * the exact same `createSharedEditorExtensions` config the live plan editor\n * uses (`dialect: \"gfm\"`, `features.image: true`) plus the {@link RunId}\n * extension, so the schema and the GFM serializer can never drift from the live\n * editor. The instance is created lazily on first use and reused across calls.\n *\n * The headless editor needs a DOM (ProseMirror's `EditorView`). It works under\n * `happy-dom` in vitest (see `gfmDoc.spec.ts`) and under the real browser DOM\n * in production. `createElement` is used rather than mounting into the page so\n * nothing is ever attached to the document.\n */\n\nlet sharedEditor: Editor | null = null;\n\n/**\n * Lazily build (and memoize) the single headless editor. Throws if no DOM is\n * available — this primitive is for the client / jsdom-style test envs only.\n */\nfunction getSharedEditor(): Editor {\n if (sharedEditor) return sharedEditor;\n if (typeof document === \"undefined\") {\n throw new Error(\n \"gfmDoc requires a DOM (document). It runs in the browser and in jsdom/happy-dom tests, not in a bare Node server context.\",\n );\n }\n sharedEditor = new Editor({\n element: document.createElement(\"div\"),\n extensions: createSharedEditorExtensions({\n dialect: \"gfm\",\n features: { image: true },\n extraExtensions: [RunId],\n }),\n content: \"\",\n });\n return sharedEditor;\n}\n\n/** Reads the GFM markdown out of the tiptap-markdown storage. */\nfunction getMarkdown(editor: Editor): string {\n const storage = editor.storage as unknown as {\n markdown?: { getMarkdown?: () => string };\n };\n return storage.markdown?.getMarkdown?.() ?? \"\";\n}\n\n/**\n * Parse a GFM markdown string into an array of top-level ProseMirror node JSON\n * (paragraph / heading / list / table / code block / etc.), matching the live\n * plan editor schema (`createSharedEditorExtensions({ dialect: \"gfm\",\n * features: { image: true } })`) plus the {@link RunId} attribute.\n *\n * `tiptap-markdown` registers the markdown parser, so handing the raw markdown\n * string to `setContent` deserializes it (the same path the live editor uses\n * when it seeds `content: markdown`). Returns the doc's child nodes; an empty\n * string yields a single empty paragraph.\n */\nexport function gfmToProseJSON(markdown: string): JSONContent[] {\n const editor = getSharedEditor();\n // `emitUpdate: false` keeps this a pure transform with no side effects on any\n // (non-existent) consumers of the headless editor's update stream.\n editor.commands.setContent(markdown, { emitUpdate: false });\n return editor.getJSON().content ?? [];\n}\n\n/**\n * Serialize an array of top-level ProseMirror node JSON into GFM markdown. The\n * `runId` attribute is omitted by GFM (the {@link RunId} extension registers no\n * markdown serializer), so it never leaks into the saved markdown.\n *\n * The nodes are wrapped in a `doc` and set on the shared editor, then the GFM\n * markdown is read from the tiptap-markdown storage — the exact serializer the\n * live editor persists with, so output is byte-stable with the live save path.\n */\nexport function proseJSONToGfm(nodes: JSONContent[]): string {\n const editor = getSharedEditor();\n const content = nodes.length > 0 ? nodes : [{ type: \"paragraph\" }];\n editor.commands.setContent({ type: \"doc\", content }, { emitUpdate: false });\n return getMarkdown(editor);\n}\n"]}
@@ -6,4 +6,9 @@ export { uploadEditorImage } from "./uploadEditorImage.js";
6
6
  export { BubbleToolbar, buildDefaultBubbleItems, type BubbleToolbarItem, type BubbleToolbarProps, } from "./BubbleToolbar.js";
7
7
  export { SharedRichEditor, type SharedRichEditorProps, } from "./SharedRichEditor.js";
8
8
  export { RichMarkdownEditor, createRichMarkdownExtensions, type RichMarkdownEditorProps, type CreateRichMarkdownExtensionsOptions, } from "./RichMarkdownEditor.js";
9
+ export { RunId, RUN_ID_NODE_TYPES } from "./RunId.js";
10
+ export { gfmToProseJSON, proseJSONToGfm } from "./gfmDoc.js";
11
+ export { DragHandle, DEFAULT_DRAG_HANDLE_WRAPPER_SELECTOR, type DragHandleOptions, } from "./DragHandle.js";
12
+ export { createRegistryBlockNode, RegistryBlockNodeView, RegistryBlockDataProvider, useRegistryBlockData, type CreateRegistryBlockNodeOptions, type RegistryBlockDataValue, type RegistryBlockSideMapBlock, } from "./RegistryBlockNode.js";
13
+ export { buildRegistryBlockSlashItems, type BuildRegistryBlockSlashItemsOptions, } from "./registrySlashCommands.js";
9
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,mCAAmC,GACzC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,kBAAkB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,kBAAkB,EAClB,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,mCAAmC,GACzC,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,mCAAmC,GACzC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,kBAAkB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,kBAAkB,EAClB,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,mCAAmC,GACzC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACL,UAAU,EACV,oCAAoC,EACpC,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,yBAAyB,EACzB,oBAAoB,EACpB,KAAK,8BAA8B,EACnC,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,GAC/B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,4BAA4B,EAC5B,KAAK,mCAAmC,GACzC,MAAM,4BAA4B,CAAC"}
@@ -6,4 +6,9 @@ export { uploadEditorImage } from "./uploadEditorImage.js";
6
6
  export { BubbleToolbar, buildDefaultBubbleItems, } from "./BubbleToolbar.js";
7
7
  export { SharedRichEditor, } from "./SharedRichEditor.js";
8
8
  export { RichMarkdownEditor, createRichMarkdownExtensions, } from "./RichMarkdownEditor.js";
9
+ export { RunId, RUN_ID_NODE_TYPES } from "./RunId.js";
10
+ export { gfmToProseJSON, proseJSONToGfm } from "./gfmDoc.js";
11
+ export { DragHandle, DEFAULT_DRAG_HANDLE_WRAPPER_SELECTOR, } from "./DragHandle.js";
12
+ export { createRegistryBlockNode, RegistryBlockNodeView, RegistryBlockDataProvider, useRegistryBlockData, } from "./RegistryBlockNode.js";
13
+ export { buildRegistryBlockSlashItems, } from "./registrySlashCommands.js";
9
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,GAOxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,GAGlB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,GAGxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,kBAAkB,GAGnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EACL,aAAa,EACb,uBAAuB,GAGxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,GAEjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,kBAAkB,EAClB,4BAA4B,GAG7B,MAAM,yBAAyB,CAAC","sourcesContent":["export {\n createSharedEditorExtensions,\n MARKDOWN_DIALECT_CONFIG,\n type RichMarkdownDialect,\n type RichMarkdownEditorPreset,\n type RichMarkdownCollabUser,\n type SharedEditorCollab,\n type SharedEditorFeatures,\n type CreateSharedEditorExtensionsOptions,\n} from \"./extensions.js\";\nexport {\n useCollabReconcile,\n getEditorMarkdown,\n type UseCollabReconcileOptions,\n type UseCollabReconcileResult,\n} from \"./useCollabReconcile.js\";\nexport {\n SlashCommandMenu,\n DEFAULT_SLASH_COMMANDS,\n createImageSlashCommand,\n type SlashCommandItem,\n type SlashCommandMenuProps,\n} from \"./SlashCommandMenu.js\";\nexport {\n SharedImage,\n createImageExtension,\n pickAndInsertImage,\n type ImageUploadFn,\n type SharedImageOptions,\n} from \"./ImageExtension.js\";\nexport { uploadEditorImage } from \"./uploadEditorImage.js\";\nexport {\n BubbleToolbar,\n buildDefaultBubbleItems,\n type BubbleToolbarItem,\n type BubbleToolbarProps,\n} from \"./BubbleToolbar.js\";\nexport {\n SharedRichEditor,\n type SharedRichEditorProps,\n} from \"./SharedRichEditor.js\";\nexport {\n RichMarkdownEditor,\n createRichMarkdownExtensions,\n type RichMarkdownEditorProps,\n type CreateRichMarkdownExtensionsOptions,\n} from \"./RichMarkdownEditor.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,GAOxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,GAGlB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,GAGxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,kBAAkB,GAGnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EACL,aAAa,EACb,uBAAuB,GAGxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,GAEjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,kBAAkB,EAClB,4BAA4B,GAG7B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACL,UAAU,EACV,oCAAoC,GAErC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,yBAAyB,EACzB,oBAAoB,GAIrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,4BAA4B,GAE7B,MAAM,4BAA4B,CAAC","sourcesContent":["export {\n createSharedEditorExtensions,\n MARKDOWN_DIALECT_CONFIG,\n type RichMarkdownDialect,\n type RichMarkdownEditorPreset,\n type RichMarkdownCollabUser,\n type SharedEditorCollab,\n type SharedEditorFeatures,\n type CreateSharedEditorExtensionsOptions,\n} from \"./extensions.js\";\nexport {\n useCollabReconcile,\n getEditorMarkdown,\n type UseCollabReconcileOptions,\n type UseCollabReconcileResult,\n} from \"./useCollabReconcile.js\";\nexport {\n SlashCommandMenu,\n DEFAULT_SLASH_COMMANDS,\n createImageSlashCommand,\n type SlashCommandItem,\n type SlashCommandMenuProps,\n} from \"./SlashCommandMenu.js\";\nexport {\n SharedImage,\n createImageExtension,\n pickAndInsertImage,\n type ImageUploadFn,\n type SharedImageOptions,\n} from \"./ImageExtension.js\";\nexport { uploadEditorImage } from \"./uploadEditorImage.js\";\nexport {\n BubbleToolbar,\n buildDefaultBubbleItems,\n type BubbleToolbarItem,\n type BubbleToolbarProps,\n} from \"./BubbleToolbar.js\";\nexport {\n SharedRichEditor,\n type SharedRichEditorProps,\n} from \"./SharedRichEditor.js\";\nexport {\n RichMarkdownEditor,\n createRichMarkdownExtensions,\n type RichMarkdownEditorProps,\n type CreateRichMarkdownExtensionsOptions,\n} from \"./RichMarkdownEditor.js\";\nexport { RunId, RUN_ID_NODE_TYPES } from \"./RunId.js\";\nexport { gfmToProseJSON, proseJSONToGfm } from \"./gfmDoc.js\";\nexport {\n DragHandle,\n DEFAULT_DRAG_HANDLE_WRAPPER_SELECTOR,\n type DragHandleOptions,\n} from \"./DragHandle.js\";\nexport {\n createRegistryBlockNode,\n RegistryBlockNodeView,\n RegistryBlockDataProvider,\n useRegistryBlockData,\n type CreateRegistryBlockNodeOptions,\n type RegistryBlockDataValue,\n type RegistryBlockSideMapBlock,\n} from \"./RegistryBlockNode.js\";\nexport {\n buildRegistryBlockSlashItems,\n type BuildRegistryBlockSlashItemsOptions,\n} from \"./registrySlashCommands.js\";\n"]}
@@ -0,0 +1,46 @@
1
+ import type { BlockRegistry, BlockSpec } from "../blocks/index.js";
2
+ /**
3
+ * Shared builder for the registry-derived block slash commands both the plan and
4
+ * content editors offer. Both apps take every `BlockSpec` whose `placement`
5
+ * includes `"block"`, gate it by Notion-compatibility when the open document is
6
+ * linked to a Notion page, and emit one slash item per surviving spec that
7
+ * inserts that block's atom node. The only legitimate per-app differences are:
8
+ *
9
+ * - the ITEM SHAPE (plan uses a text-glyph `icon`, content a React component),
10
+ * - the Notion-compat PREDICATE (plan unions in prose-only NFM analogs, content
11
+ * reads the registry `notionCompatible` flag directly), and
12
+ * - the INSERT behavior (plan inserts a `planBlock` node, content a
13
+ * `registryBlock` node seeded with inline `__raw`).
14
+ *
15
+ * Those three are injected; everything else (the `list("block")` source, the
16
+ * Notion filter wiring, the one-item-per-spec mapping) lives here so adding a
17
+ * new library block only touches the registry, never the slash builders.
18
+ */
19
+ export interface BuildRegistryBlockSlashItemsOptions<TItem, TEditor> {
20
+ /**
21
+ * When `true`, only specs the predicate accepts are offered (the open document
22
+ * is linked to a Notion page, so blocks that can't round-trip to NFM are
23
+ * hidden). When unset/false, every block-placed spec is offered.
24
+ */
25
+ notionCompatibleOnly?: boolean;
26
+ /**
27
+ * Decide whether a spec round-trips to Notion. Defaults to the spec's own
28
+ * `notionCompatible` flag (content's rule). Plan passes a predicate that unions
29
+ * in prose-only NFM analogs not carried as registry flags.
30
+ */
31
+ isNotionCompatible?: (spec: BlockSpec) => boolean;
32
+ /** Build one app-shaped slash item from a surviving block spec. */
33
+ toItem: (spec: BlockSpec, insert: (editor: TEditor) => void) => TItem;
34
+ /**
35
+ * Insert this spec's block atom into the editor. Plan inserts a `planBlock`
36
+ * node; content inserts a `registryBlock` node seeded with inline `__raw`.
37
+ */
38
+ insertBlock: (editor: TEditor, spec: BlockSpec) => void;
39
+ }
40
+ /**
41
+ * Build the registry-derived block slash items, shared by plan and content. Each
42
+ * app prepends its own prose/base commands and wraps the result in its own item
43
+ * type via {@link BuildRegistryBlockSlashItemsOptions.toItem}.
44
+ */
45
+ export declare function buildRegistryBlockSlashItems<TItem, TEditor>(registry: BlockRegistry, options: BuildRegistryBlockSlashItemsOptions<TItem, TEditor>): TItem[];
46
+ //# sourceMappingURL=registrySlashCommands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registrySlashCommands.d.ts","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/registrySlashCommands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEnE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,mCAAmC,CAAC,KAAK,EAAE,OAAO;IACjE;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC;IAClD,mEAAmE;IACnE,MAAM,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,KAAK,KAAK,CAAC;IACtE;;;OAGG;IACH,WAAW,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;CACzD;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,EACzD,QAAQ,EAAE,aAAa,EACvB,OAAO,EAAE,mCAAmC,CAAC,KAAK,EAAE,OAAO,CAAC,GAC3D,KAAK,EAAE,CAST"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Build the registry-derived block slash items, shared by plan and content. Each
3
+ * app prepends its own prose/base commands and wraps the result in its own item
4
+ * type via {@link BuildRegistryBlockSlashItemsOptions.toItem}.
5
+ */
6
+ export function buildRegistryBlockSlashItems(registry, options) {
7
+ const isCompatible = options.isNotionCompatible ?? ((spec) => Boolean(spec.notionCompatible));
8
+ return registry
9
+ .list("block")
10
+ .filter((spec) => !options.notionCompatibleOnly || isCompatible(spec))
11
+ .map((spec) => options.toItem(spec, (editor) => options.insertBlock(editor, spec)));
12
+ }
13
+ //# sourceMappingURL=registrySlashCommands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registrySlashCommands.js","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/registrySlashCommands.ts"],"names":[],"mappings":"AAyCA;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAC1C,QAAuB,EACvB,OAA4D;IAE5D,MAAM,YAAY,GAChB,OAAO,CAAC,kBAAkB,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC3E,OAAO,QAAQ;SACZ,IAAI,CAAC,OAAO,CAAC;SACb,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,oBAAoB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;SACrE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACZ,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CACpE,CAAC;AACN,CAAC","sourcesContent":["import type { BlockRegistry, BlockSpec } from \"../blocks/index.js\";\n\n/**\n * Shared builder for the registry-derived block slash commands both the plan and\n * content editors offer. Both apps take every `BlockSpec` whose `placement`\n * includes `\"block\"`, gate it by Notion-compatibility when the open document is\n * linked to a Notion page, and emit one slash item per surviving spec that\n * inserts that block's atom node. The only legitimate per-app differences are:\n *\n * - the ITEM SHAPE (plan uses a text-glyph `icon`, content a React component),\n * - the Notion-compat PREDICATE (plan unions in prose-only NFM analogs, content\n * reads the registry `notionCompatible` flag directly), and\n * - the INSERT behavior (plan inserts a `planBlock` node, content a\n * `registryBlock` node seeded with inline `__raw`).\n *\n * Those three are injected; everything else (the `list(\"block\")` source, the\n * Notion filter wiring, the one-item-per-spec mapping) lives here so adding a\n * new library block only touches the registry, never the slash builders.\n */\nexport interface BuildRegistryBlockSlashItemsOptions<TItem, TEditor> {\n /**\n * When `true`, only specs the predicate accepts are offered (the open document\n * is linked to a Notion page, so blocks that can't round-trip to NFM are\n * hidden). When unset/false, every block-placed spec is offered.\n */\n notionCompatibleOnly?: boolean;\n /**\n * Decide whether a spec round-trips to Notion. Defaults to the spec's own\n * `notionCompatible` flag (content's rule). Plan passes a predicate that unions\n * in prose-only NFM analogs not carried as registry flags.\n */\n isNotionCompatible?: (spec: BlockSpec) => boolean;\n /** Build one app-shaped slash item from a surviving block spec. */\n toItem: (spec: BlockSpec, insert: (editor: TEditor) => void) => TItem;\n /**\n * Insert this spec's block atom into the editor. Plan inserts a `planBlock`\n * node; content inserts a `registryBlock` node seeded with inline `__raw`.\n */\n insertBlock: (editor: TEditor, spec: BlockSpec) => void;\n}\n\n/**\n * Build the registry-derived block slash items, shared by plan and content. Each\n * app prepends its own prose/base commands and wraps the result in its own item\n * type via {@link BuildRegistryBlockSlashItemsOptions.toItem}.\n */\nexport function buildRegistryBlockSlashItems<TItem, TEditor>(\n registry: BlockRegistry,\n options: BuildRegistryBlockSlashItemsOptions<TItem, TEditor>,\n): TItem[] {\n const isCompatible =\n options.isNotionCompatible ?? ((spec) => Boolean(spec.notionCompatible));\n return registry\n .list(\"block\")\n .filter((spec) => !options.notionCompatibleOnly || isCompatible(spec))\n .map((spec) =>\n options.toItem(spec, (editor) => options.insertBlock(editor, spec)),\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"useCollabReconcile.d.ts","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/useCollabReconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EAAE,GAAG,IAAI,IAAI,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKxD;AAED,MAAM,WAAW,yBAAyB;IACxC,gDAAgD;IAChD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uEAAuE;IACvE,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,qFAAqF;IACrF,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CACX,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KACtD,IAAI,CAAC;IACV;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;KACxB,KAAK,OAAO,CAAC;IACd;;;;;;;OAOG;IACH,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,wBAAwB;IACvC,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,mBAAmB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC/C;;;;OAIG;IACH,kBAAkB,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC;IAC1D;;;;OAIG;IACH,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CAChD;AAoED,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,IAAW,EACX,SAAgB,EAChB,KAAK,EACL,gBAAgB,EAChB,QAAQ,EACR,WAA+B,EAC/B,UAA8B,EAC9B,cAAyB,EACzB,UAA8B,EAC9B,uBAAuB,GACxB,EAAE,yBAAyB,GAAG,wBAAwB,CAiStD"}
1
+ {"version":3,"file":"useCollabReconcile.d.ts","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/useCollabReconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EAAE,GAAG,IAAI,IAAI,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKxD;AAqBD,MAAM,WAAW,yBAAyB;IACxC,gDAAgD;IAChD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uEAAuE;IACvE,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,qFAAqF;IACrF,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CACX,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KACtD,IAAI,CAAC;IACV;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;KACxB,KAAK,OAAO,CAAC;IACd;;;;;;;OAOG;IACH,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,wBAAwB;IACvC,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,mBAAmB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC/C;;;;OAIG;IACH,kBAAkB,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC;IAC1D;;;;OAIG;IACH,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CAChD;AAoED,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,IAAW,EACX,SAAgB,EAChB,KAAK,EACL,gBAAgB,EAChB,QAAQ,EACR,WAA+B,EAC/B,UAA8B,EAC9B,cAAyB,EACzB,UAA8B,EAC9B,uBAAuB,GACxB,EAAE,yBAAyB,GAAG,wBAAwB,CA4StD"}
@@ -7,6 +7,28 @@ export function getEditorMarkdown(editor) {
7
7
  const markdownStorage = editor.storage;
8
8
  return markdownStorage.markdown?.getMarkdown?.() ?? "";
9
9
  }
10
+ /**
11
+ * Push a value onto the bounded ring of recently-emitted markdown (most recent
12
+ * last, deduped, capped). The reconcile uses this to recognize a stale-but-recent
13
+ * echo of OUR OWN edits: a debounced autosave can persist a PARTIAL burst, and
14
+ * the next poll re-supplies that partial value with a newer timestamp — applying
15
+ * it would clobber the freshly-typed tail. An external change (agent/peer) never
16
+ * byte-matches one of our own recent emissions, and if it somehow did the content
17
+ * is identical, so skipping it is safe by construction.
18
+ */
19
+ const EMITTED_RING_MAX = 16;
20
+ function pushEmittedRing(ring, value) {
21
+ if (!value)
22
+ return;
23
+ if (ring[ring.length - 1] === value)
24
+ return;
25
+ const dupe = ring.indexOf(value);
26
+ if (dupe !== -1)
27
+ ring.splice(dupe, 1);
28
+ ring.push(value);
29
+ if (ring.length > EMITTED_RING_MAX)
30
+ ring.shift();
31
+ }
10
32
  /**
11
33
  * The subtle seed / reconcile / lead-client logic for the shared markdown
12
34
  * editor, extracted once so it can never be duplicated across embedders.
@@ -64,6 +86,10 @@ export function useCollabReconcile({ editor, ydoc = null, awareness = null, valu
64
86
  const collab = !!ydoc;
65
87
  const isSettingContentRef = useRef(false);
66
88
  const lastEmittedRef = useRef("");
89
+ // Ring of recent local emissions (see pushEmittedRing). Lets the reconcile
90
+ // recognize a stale-but-recent echo of our OWN (possibly partial, debounced)
91
+ // save so a lagging poll never clobbers freshly-typed text.
92
+ const recentEmittedRef = useRef([]);
67
93
  const lastTypedAtRef = useRef(0);
68
94
  // The raw authoritative `value` string the reconcile last applied. When the
69
95
  // SAME raw string is re-fetched (a lagging poll, or a source-sync that keeps
@@ -145,6 +171,7 @@ export function useCollabReconcile({ editor, ydoc = null, awareness = null, valu
145
171
  isSettingContentRef.current = false;
146
172
  const serialized = getMarkdown(editor);
147
173
  lastEmittedRef.current = serialized;
174
+ pushEmittedRing(recentEmittedRef.current, serialized);
148
175
  lastAppliedValueRef.current = value;
149
176
  lastAppliedSerializedRef.current = serialized;
150
177
  if (contentUpdatedAt)
@@ -217,6 +244,10 @@ export function useCollabReconcile({ editor, ydoc = null, awareness = null, valu
217
244
  // `<p>` → `&lt;p&gt;` → `&amp;lt;p&amp;gt;` escalation.
218
245
  if (currentMarkdown === normalizedValue ||
219
246
  value === lastEmittedRef.current ||
247
+ // A stale-but-recent echo of our own (possibly partial) save — applying
248
+ // it would clobber the freshly-typed tail. External edits never match.
249
+ recentEmittedRef.current.includes(value) ||
250
+ recentEmittedRef.current.includes(normalizedValue) ||
220
251
  (editorUnchangedSinceApply &&
221
252
  (value === lastAppliedValueRef.current ||
222
253
  normalizedValue === lastAppliedSerializedRef.current))) {
@@ -289,6 +320,7 @@ export function useCollabReconcile({ editor, ydoc = null, awareness = null, valu
289
320
  // own echo and skipped — stabilizing the doc after exactly one apply.
290
321
  const serialized = getMarkdown(editor);
291
322
  lastEmittedRef.current = serialized;
323
+ pushEmittedRing(recentEmittedRef.current, serialized);
292
324
  lastAppliedValueRef.current = value;
293
325
  lastAppliedSerializedRef.current = serialized;
294
326
  if (contentUpdatedAt) {
@@ -330,6 +362,7 @@ export function useCollabReconcile({ editor, ydoc = null, awareness = null, valu
330
362
  if (collab && !markdown.trim())
331
363
  return false;
332
364
  lastEmittedRef.current = markdown;
365
+ pushEmittedRing(recentEmittedRef.current, markdown);
333
366
  return true;
334
367
  };
335
368
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"useCollabReconcile.js","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/useCollabReconcile.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState, type MutableRefObject } from \"react\";\nimport type { Editor } from \"@tiptap/react\";\nimport type { Transaction } from \"@tiptap/pm/state\";\nimport { isChangeOrigin } from \"@tiptap/extension-collaboration\";\nimport type { Doc as YDoc } from \"yjs\";\nimport type { Awareness } from \"y-protocols/awareness\";\nimport { isReconcileLeadClient } from \"../../collab/client.js\";\nimport { AGENT_CLIENT_ID } from \"../../collab/agent-identity.js\";\n\n/** Reads the current markdown out of the tiptap-markdown storage. */\nexport function getEditorMarkdown(editor: Editor): string {\n const markdownStorage = editor.storage as unknown as {\n markdown?: { getMarkdown?: () => string };\n };\n return markdownStorage.markdown?.getMarkdown?.() ?? \"\";\n}\n\nexport interface UseCollabReconcileOptions {\n /** The live editor, or null until it mounts. */\n editor: Editor | null;\n /** Shared Y.Doc when collaborating; null disables all collab paths. */\n ydoc?: YDoc | null;\n /** Shared awareness; null keeps the sole-client lead path. */\n awareness?: Awareness | null;\n /** Authoritative markdown value (SQL source of truth). */\n value: string;\n /** Timestamp of the authoritative value; gates newer-than reconcile. */\n contentUpdatedAt?: string | null;\n /** Whether the editor accepts edits. Reconcile/seed only run for the live editor. */\n editable: boolean;\n /**\n * Reads the current markdown from the editor. Injected so a dialect could\n * swap serializers; defaults to the tiptap-markdown storage reader. For an app\n * with a custom serializer (e.g. Content's `docToNfm(editor.getJSON())`), pass\n * it here so the seed/reconcile equality checks compare like-for-like.\n */\n getMarkdown?: (editor: Editor) => string;\n /**\n * Applies the authoritative `value` into the editor. Defaults to passing the\n * raw markdown string to `editor.commands.setContent`. Apps whose serializer\n * is NOT tiptap-markdown (Content parses `nfmToDoc(value)` into a PM doc)\n * override this so seed + reconcile write the correct content shape. The\n * supplied `options` carry the history/whitespace flags the default path uses;\n * a custom implementation should forward them when relevant.\n */\n setContent?: (\n editor: Editor,\n value: string,\n options: { emitUpdate?: boolean; addToHistory?: boolean },\n ) => void;\n /**\n * Normalizes the authoritative `value` to the canonical markdown the editor\n * would emit, so the \"already in sync / our own echo\" equality checks match a\n * serializer that re-canonicalizes (Content's `canonicalizeNfm`). Defaults to\n * identity (GFM already round-trips byte-stably).\n */\n normalizeValue?: (value: string) => string;\n /**\n * Decides whether the empty-doc seed should run for the current shared\n * fragment. Defaults to \"fragment has no nodes, or the editor holds no\n * semantic markdown\". Apps with sentinel-empty content (Content's\n * `<empty-block/>` filler) override this. Receives the live fragment length\n * and the editor's current markdown.\n */\n shouldSeed?: (info: {\n value: string;\n currentMarkdown: string;\n fragmentLength: number;\n }) => boolean;\n /**\n * The initial \"applied\" watermark. Default mirrors `contentUpdatedAt`, so a\n * fresh mount whose Y.Doc already matches SQL doesn't re-apply. Pass `null`\n * to force the first reconcile pass to adopt authoritative SQL even at the\n * same timestamp — Content does this so a stale persisted Y.Doc (an agent that\n * edited the CLOSED doc) is corrected on open. The editor is keyed per\n * document upstream, so this only affects the first mount of each doc.\n */\n initialAppliedUpdatedAt?: string | null;\n}\n\nexport interface UseCollabReconcileResult {\n /** True when a Y.Doc is bound (collaborative editing active). */\n collab: boolean;\n /**\n * Set true around any programmatic `setContent` so the editor's `onUpdate`\n * can ignore the resulting transaction (it isn't a user edit).\n */\n isSettingContentRef: MutableRefObject<boolean>;\n /**\n * Call from `onUpdate` BEFORE serializing. Returns true when the update must\n * be ignored: editor not editable, mid-programmatic-setContent, or (in collab\n * mode) a remote-origin transaction. Also records the local typing time.\n */\n shouldIgnoreUpdate: (transaction: Transaction) => boolean;\n /**\n * Call from `onUpdate` AFTER computing the markdown to emit. Returns false\n * when the value must NOT be persisted yet (an empty collab doc before the\n * seed has run); records it as the last-emitted value otherwise.\n */\n registerEmitted: (markdown: string) => boolean;\n}\n\n/**\n * The subtle seed / reconcile / lead-client logic for the shared markdown\n * editor, extracted once so it can never be duplicated across embedders.\n *\n * Responsibilities (reproducing the Plan editor's behavior exactly):\n * - Track whether THIS client is the reconcile lead (sole client always leads;\n * otherwise elected via {@link isReconcileLeadClient}) and how many other\n * visible human peers are present.\n * - Seed an empty shared Y.Doc once from `value` — lead client only — so two\n * clients opening a brand-new block don't both insert the content.\n * - Reconcile authoritative external markdown (agent edit, source patch, peer\n * edit mirrored to SQL) into the editor: in collab mode only the lead client\n * applies it through `setContent` and Yjs propagates; in non-collab mode this\n * is the original controlled-value reconcile.\n * - Provide the `onUpdate` guards (`shouldIgnoreUpdate`, `registerEmitted`) so\n * the component never persists remote-origin or pre-seed empty content.\n */\n/** Default seed predicate: seed only when the shared doc is genuinely empty. */\nfunction defaultShouldSeed({\n currentMarkdown,\n fragmentLength,\n}: {\n value: string;\n currentMarkdown: string;\n fragmentLength: number;\n}): boolean {\n return fragmentLength === 0 || !currentMarkdown.trim();\n}\n\n/**\n * Default content writer: hand the raw markdown string to `setContent`, which\n * tiptap-markdown overrides to parse the markdown into a ProseMirror doc.\n *\n * IMPORTANT: do NOT pass `parseOptions: { preserveWhitespace: \"full\" }` here.\n * In tiptap v3 the core `setContent` command routes `preserveWhitespace: \"full\"`\n * through `insertContentAt`, which tiptap-markdown ALSO overrides to re-run its\n * markdown parser. That double-parse stringifies the already-parsed PM doc and\n * re-parses it as HTML, so a clean heading/list/code block comes back as the\n * escaped, non-idempotent `&lt;h1&gt;…` — which then escalates every reconcile\n * cycle (`<p>` → `&lt;p&gt;` → `&amp;lt;p&amp;gt;` …). Letting the markdown\n * override parse the string directly (no `parseOptions`) round-trips byte-stably\n * for the GFM corpus, including code-block and empty-line whitespace. Content's\n * NFM path supplies its own `setContent` (it passes a pre-parsed PM doc) and is\n * unaffected by this default.\n */\nfunction defaultSetContent(\n editor: Editor,\n value: string,\n options: { emitUpdate?: boolean; addToHistory?: boolean },\n): void {\n if (options.addToHistory === false) {\n editor\n .chain()\n .command(({ tr }) => {\n // addToHistory:false so cmd+z (or Yjs undo) doesn't erase\n // externally-loaded content.\n tr.setMeta(\"addToHistory\", false);\n return true;\n })\n .setContent(value, { emitUpdate: options.emitUpdate })\n .run();\n return;\n }\n editor.commands.setContent(value);\n}\n\nexport function useCollabReconcile({\n editor,\n ydoc = null,\n awareness = null,\n value,\n contentUpdatedAt,\n editable,\n getMarkdown = getEditorMarkdown,\n setContent = defaultSetContent,\n normalizeValue = (v) => v,\n shouldSeed = defaultShouldSeed,\n initialAppliedUpdatedAt,\n}: UseCollabReconcileOptions): UseCollabReconcileResult {\n const collab = !!ydoc;\n const isSettingContentRef = useRef(false);\n const lastEmittedRef = useRef(\"\");\n const lastTypedAtRef = useRef(0);\n // The raw authoritative `value` string the reconcile last applied. When the\n // SAME raw string is re-fetched (a lagging poll, or a source-sync that keeps\n // re-supplying the same stored markdown), applying it again would only\n // reproduce the doc we already hold — and if `value` is NON-idempotent\n // (serialize(parse(value)) !== value) re-applying compounds the divergence\n // every cycle (`<p>` → `&lt;p&gt;` → `&amp;lt;p&amp;gt;` …). Tracked so the\n // identical re-fetch is recognized and skipped.\n const lastAppliedValueRef = useRef<string | null>(null);\n // The editor's SERIALIZED output captured right AFTER the last reconcile/seed\n // apply (`getMarkdown(editor)` once the content settled). For non-idempotent\n // input this is what autosave actually persists, so the NEXT poll hands it\n // back as the new `value`. Comparing the incoming value against this lets the\n // reconcile recognize its own echo even when the raw string changed once, so\n // it never re-parses content the editor already represents. This is the\n // doc-equivalence guard that breaks the escalation loop.\n const lastAppliedSerializedRef = useRef<string | null>(null);\n const lastAppliedUpdatedAtRef = useRef<string | null>(\n initialAppliedUpdatedAt !== undefined\n ? initialAppliedUpdatedAt\n : (contentUpdatedAt ?? null),\n );\n\n // Whether THIS client is the one that seeds the empty shared doc / applies an\n // authoritative external snapshot into it. Exactly one client does, so the\n // content isn't inserted once per open editor. A sole client always leads.\n const [isLeadClient, setIsLeadClient] = useState(true);\n // Count of OTHER visible human collaborators. When >0, a peer's edit also\n // arrives via Yjs, so external markdown reconcile must defer (avoid applying\n // the same change through both Yjs and setContent).\n const peerCountRef = useRef(0);\n useEffect(() => {\n if (!collab || !awareness || !ydoc) {\n setIsLeadClient(true);\n peerCountRef.current = 0;\n return;\n }\n const update = () => {\n setIsLeadClient(isReconcileLeadClient(awareness, ydoc.clientID));\n let peers = 0;\n awareness.getStates().forEach((state, clientId) => {\n if (clientId === ydoc.clientID) return; // self\n if (clientId === AGENT_CLIENT_ID) return; // agent isn't a Yjs editor\n const s = state as { user?: unknown; visible?: boolean };\n if (s && s.user && s.visible !== false) peers += 1;\n });\n peerCountRef.current = peers;\n };\n update();\n awareness.on(\"change\", update);\n document.addEventListener(\"visibilitychange\", update);\n return () => {\n awareness.off(\"change\", update);\n document.removeEventListener(\"visibilitychange\", update);\n };\n }, [collab, awareness, ydoc]);\n\n // Collab seed: populate an empty shared Y.Doc from the markdown `value` once.\n // The Collaboration extension does NOT auto-seed; only the lead client does,\n // so two clients opening a brand-new block at once don't both seed (which\n // would duplicate the content via concurrent inserts at the same position).\n const seededRef = useRef(false);\n useEffect(() => {\n if (!collab || !editor || editor.isDestroyed || !ydoc) return;\n if (seededRef.current) return;\n if (!isLeadClient) return;\n if (!value.trim()) return;\n const fragment = ydoc.getXmlFragment(\"default\");\n const currentMarkdown = getMarkdown(editor);\n // Seed only when the shared doc is genuinely empty — either the fragment has\n // no nodes yet, or it holds no semantic markdown (an empty paragraph, or an\n // app's sentinel-empty filler via a custom `shouldSeed`).\n if (\n shouldSeed({ value, currentMarkdown, fragmentLength: fragment.length })\n ) {\n isSettingContentRef.current = true;\n setContent(editor, value, {});\n isSettingContentRef.current = false;\n const serialized = getMarkdown(editor);\n lastEmittedRef.current = serialized;\n lastAppliedValueRef.current = value;\n lastAppliedSerializedRef.current = serialized;\n if (contentUpdatedAt) lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n seededRef.current = true;\n }, [\n collab,\n editor,\n ydoc,\n value,\n isLeadClient,\n contentUpdatedAt,\n getMarkdown,\n setContent,\n shouldSeed,\n ]);\n\n // Reconcile authoritative external markdown (agent edit, source patch, or a\n // peer edit mirrored to SQL) into the live editor. In collab mode only the\n // lead client applies it through setContent; Yjs propagates the result to\n // every other client. In non-collab mode this is the original controlled-value\n // reconcile, unchanged.\n useEffect(() => {\n if (!editor || editor.isDestroyed) return;\n\n let cancelled = false;\n let retry: ReturnType<typeof setTimeout> | null = null;\n // With peers present, a peer's edit also arrives via Yjs. Defer one poll\n // cycle (+margin) and re-check before applying via setContent so the same\n // change isn't inserted twice (Yjs + setContent → duplicated region).\n const PEER_SETTLE_MS = 2500;\n\n const apply = (deferred = false) => {\n if (cancelled || editor.isDestroyed) return;\n // In collab mode, defer all reconcile until the shared doc is seeded so we\n // never setContent over an unseeded fragment.\n if (collab && !seededRef.current) {\n retry = setTimeout(() => apply(deferred), 300);\n return;\n }\n const currentMarkdown = getMarkdown(editor);\n // Compare against the canonical form the editor would emit so a serializer\n // that re-normalizes (Content's NFM) still recognizes \"already in sync\".\n const normalizedValue = normalizeValue(value);\n // Whether the editor still holds exactly what THIS hook last applied (the\n // user hasn't edited since). Only then are the round-trip echo guards\n // below safe: if the user has since edited away from the applied content,\n // an external snapshot equal to a previously-applied value is a real\n // revert and must NOT be swallowed as an echo.\n const editorUnchangedSinceApply =\n lastAppliedSerializedRef.current !== null &&\n currentMarkdown === lastAppliedSerializedRef.current;\n\n // Doc-equivalence skip. Never re-apply content the editor already\n // represents — comparing by DOC EQUIVALENCE, not raw strings/timestamps:\n // 1. `currentMarkdown === normalizedValue` — the editor's CURRENT\n // serialized doc already equals the (normalized) incoming value.\n // 2. `value === lastEmittedRef.current` — the incoming value is our own\n // just-emitted markdown echoing back.\n // 3. `value === lastAppliedValueRef.current` — the SAME raw value we\n // already applied is being re-supplied (a lagging poll or a\n // source-sync re-handing the same stored markdown). Applying it again\n // would only reproduce the doc we hold; for NON-idempotent input it\n // would compound divergence. Guarded by `editorUnchangedSinceApply`\n // so a deliberate revert-to-previous after a local edit still lands.\n // 4. `normalizedValue === lastAppliedSerializedRef.current` — the\n // incoming value round-trips to the serialized output we last\n // produced (our own autosaved echo coming back from SQL). For\n // non-idempotent input the raw string differs from what we were\n // handed, but it is doc-equivalent to what the editor already shows,\n // so re-parsing it must be skipped. This is the guard that stops the\n // `<p>` → `&lt;p&gt;` → `&amp;lt;p&amp;gt;` escalation.\n if (\n currentMarkdown === normalizedValue ||\n value === lastEmittedRef.current ||\n (editorUnchangedSinceApply &&\n (value === lastAppliedValueRef.current ||\n normalizedValue === lastAppliedSerializedRef.current))\n ) {\n if (contentUpdatedAt) {\n lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n return;\n }\n\n const externalNewer =\n !lastAppliedUpdatedAtRef.current ||\n !contentUpdatedAt ||\n contentUpdatedAt > lastAppliedUpdatedAtRef.current;\n\n // Only the lead client applies an authoritative snapshot into the shared\n // Y.Doc; peers receive it through Yjs sync.\n if (collab && !isLeadClient) {\n if (contentUpdatedAt && !externalNewer) {\n lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n return;\n }\n\n // Never clobber an in-progress edit. While the user is actively typing\n // (focused and a keystroke landed within the window) defer and re-check —\n // applying external content now would yank text out from under them and,\n // for non-idempotent input, fight every keystroke. Newer external content\n // retries so it still lands once they pause; older-or-equal content is a\n // stale poll and is dropped outright while focused.\n const typingRecently =\n editor.isFocused && Date.now() - lastTypedAtRef.current < 1500;\n if (typingRecently) {\n if (externalNewer) {\n retry = setTimeout(() => apply(deferred), 700);\n }\n return;\n }\n if (!externalNewer && editor.isFocused) return;\n\n // Race guard: with peers present, let Yjs deliver a peer's edit first.\n // Defer once and re-check — a peer edit makes the equality check above\n // no-op next pass; an agent/source edit still differs and applies.\n if (collab && externalNewer && !deferred && peerCountRef.current > 0) {\n retry = setTimeout(() => apply(true), PEER_SETTLE_MS);\n return;\n }\n\n queueMicrotask(() => {\n if (cancelled || editor.isDestroyed) return;\n // Re-check doc-equivalence at apply time. Between the decision above and\n // this microtask a peer/Yjs edit (or our own prior apply) may have made\n // the editor already represent this value — re-applying would be a\n // wasted setContent that, for non-idempotent input, re-triggers the\n // loop. Skip when the editor's current serialization already matches the\n // normalized value, or the value round-trips to what we last produced.\n const beforeMarkdown = getMarkdown(editor);\n const normalized = normalizeValue(value);\n const unchangedSinceApply =\n lastAppliedSerializedRef.current !== null &&\n beforeMarkdown === lastAppliedSerializedRef.current;\n if (\n beforeMarkdown === normalized ||\n (unchangedSinceApply &&\n normalized === lastAppliedSerializedRef.current)\n ) {\n lastAppliedValueRef.current = value;\n if (contentUpdatedAt) {\n lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n return;\n }\n isSettingContentRef.current = true;\n setContent(editor, value, { emitUpdate: false, addToHistory: false });\n isSettingContentRef.current = false;\n // Capture the SERIALIZED result, not the raw value. For non-idempotent\n // input these differ; recording the serialized output is what lets the\n // next poll (which returns this serialized form) be recognized as our\n // own echo and skipped — stabilizing the doc after exactly one apply.\n const serialized = getMarkdown(editor);\n lastEmittedRef.current = serialized;\n lastAppliedValueRef.current = value;\n lastAppliedSerializedRef.current = serialized;\n if (contentUpdatedAt) {\n lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n });\n };\n\n apply();\n return () => {\n cancelled = true;\n if (retry) clearTimeout(retry);\n };\n }, [\n contentUpdatedAt,\n editor,\n value,\n collab,\n isLeadClient,\n getMarkdown,\n setContent,\n normalizeValue,\n ]);\n\n const shouldIgnoreUpdate = (transaction: Transaction): boolean => {\n if (!editable || isSettingContentRef.current) return true;\n // In collab mode, never persist remote-originated changes (the initial Yjs\n // state load or a peer's edit arriving via sync). Each client saves only its\n // OWN local edits; a peer's edit is saved by that peer. Without this, a\n // lagging Y.Doc load would write stale markdown over newer SQL.\n if (collab && transaction && isChangeOrigin(transaction)) return true;\n lastTypedAtRef.current = Date.now();\n return false;\n };\n\n const registerEmitted = (markdown: string): boolean => {\n // Don't persist an empty doc before Collaboration has seeded — that would\n // clobber the saved block content with an empty string.\n if (collab && !markdown.trim()) return false;\n lastEmittedRef.current = markdown;\n return true;\n };\n\n return {\n collab,\n isSettingContentRef,\n shouldIgnoreUpdate,\n registerEmitted,\n };\n}\n"]}
1
+ {"version":3,"file":"useCollabReconcile.js","sourceRoot":"","sources":["../../../src/client/rich-markdown-editor/useCollabReconcile.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState, type MutableRefObject } from \"react\";\nimport type { Editor } from \"@tiptap/react\";\nimport type { Transaction } from \"@tiptap/pm/state\";\nimport { isChangeOrigin } from \"@tiptap/extension-collaboration\";\nimport type { Doc as YDoc } from \"yjs\";\nimport type { Awareness } from \"y-protocols/awareness\";\nimport { isReconcileLeadClient } from \"../../collab/client.js\";\nimport { AGENT_CLIENT_ID } from \"../../collab/agent-identity.js\";\n\n/** Reads the current markdown out of the tiptap-markdown storage. */\nexport function getEditorMarkdown(editor: Editor): string {\n const markdownStorage = editor.storage as unknown as {\n markdown?: { getMarkdown?: () => string };\n };\n return markdownStorage.markdown?.getMarkdown?.() ?? \"\";\n}\n\n/**\n * Push a value onto the bounded ring of recently-emitted markdown (most recent\n * last, deduped, capped). The reconcile uses this to recognize a stale-but-recent\n * echo of OUR OWN edits: a debounced autosave can persist a PARTIAL burst, and\n * the next poll re-supplies that partial value with a newer timestamp — applying\n * it would clobber the freshly-typed tail. An external change (agent/peer) never\n * byte-matches one of our own recent emissions, and if it somehow did the content\n * is identical, so skipping it is safe by construction.\n */\nconst EMITTED_RING_MAX = 16;\nfunction pushEmittedRing(ring: string[], value: string): void {\n if (!value) return;\n if (ring[ring.length - 1] === value) return;\n const dupe = ring.indexOf(value);\n if (dupe !== -1) ring.splice(dupe, 1);\n ring.push(value);\n if (ring.length > EMITTED_RING_MAX) ring.shift();\n}\n\nexport interface UseCollabReconcileOptions {\n /** The live editor, or null until it mounts. */\n editor: Editor | null;\n /** Shared Y.Doc when collaborating; null disables all collab paths. */\n ydoc?: YDoc | null;\n /** Shared awareness; null keeps the sole-client lead path. */\n awareness?: Awareness | null;\n /** Authoritative markdown value (SQL source of truth). */\n value: string;\n /** Timestamp of the authoritative value; gates newer-than reconcile. */\n contentUpdatedAt?: string | null;\n /** Whether the editor accepts edits. Reconcile/seed only run for the live editor. */\n editable: boolean;\n /**\n * Reads the current markdown from the editor. Injected so a dialect could\n * swap serializers; defaults to the tiptap-markdown storage reader. For an app\n * with a custom serializer (e.g. Content's `docToNfm(editor.getJSON())`), pass\n * it here so the seed/reconcile equality checks compare like-for-like.\n */\n getMarkdown?: (editor: Editor) => string;\n /**\n * Applies the authoritative `value` into the editor. Defaults to passing the\n * raw markdown string to `editor.commands.setContent`. Apps whose serializer\n * is NOT tiptap-markdown (Content parses `nfmToDoc(value)` into a PM doc)\n * override this so seed + reconcile write the correct content shape. The\n * supplied `options` carry the history/whitespace flags the default path uses;\n * a custom implementation should forward them when relevant.\n */\n setContent?: (\n editor: Editor,\n value: string,\n options: { emitUpdate?: boolean; addToHistory?: boolean },\n ) => void;\n /**\n * Normalizes the authoritative `value` to the canonical markdown the editor\n * would emit, so the \"already in sync / our own echo\" equality checks match a\n * serializer that re-canonicalizes (Content's `canonicalizeNfm`). Defaults to\n * identity (GFM already round-trips byte-stably).\n */\n normalizeValue?: (value: string) => string;\n /**\n * Decides whether the empty-doc seed should run for the current shared\n * fragment. Defaults to \"fragment has no nodes, or the editor holds no\n * semantic markdown\". Apps with sentinel-empty content (Content's\n * `<empty-block/>` filler) override this. Receives the live fragment length\n * and the editor's current markdown.\n */\n shouldSeed?: (info: {\n value: string;\n currentMarkdown: string;\n fragmentLength: number;\n }) => boolean;\n /**\n * The initial \"applied\" watermark. Default mirrors `contentUpdatedAt`, so a\n * fresh mount whose Y.Doc already matches SQL doesn't re-apply. Pass `null`\n * to force the first reconcile pass to adopt authoritative SQL even at the\n * same timestamp — Content does this so a stale persisted Y.Doc (an agent that\n * edited the CLOSED doc) is corrected on open. The editor is keyed per\n * document upstream, so this only affects the first mount of each doc.\n */\n initialAppliedUpdatedAt?: string | null;\n}\n\nexport interface UseCollabReconcileResult {\n /** True when a Y.Doc is bound (collaborative editing active). */\n collab: boolean;\n /**\n * Set true around any programmatic `setContent` so the editor's `onUpdate`\n * can ignore the resulting transaction (it isn't a user edit).\n */\n isSettingContentRef: MutableRefObject<boolean>;\n /**\n * Call from `onUpdate` BEFORE serializing. Returns true when the update must\n * be ignored: editor not editable, mid-programmatic-setContent, or (in collab\n * mode) a remote-origin transaction. Also records the local typing time.\n */\n shouldIgnoreUpdate: (transaction: Transaction) => boolean;\n /**\n * Call from `onUpdate` AFTER computing the markdown to emit. Returns false\n * when the value must NOT be persisted yet (an empty collab doc before the\n * seed has run); records it as the last-emitted value otherwise.\n */\n registerEmitted: (markdown: string) => boolean;\n}\n\n/**\n * The subtle seed / reconcile / lead-client logic for the shared markdown\n * editor, extracted once so it can never be duplicated across embedders.\n *\n * Responsibilities (reproducing the Plan editor's behavior exactly):\n * - Track whether THIS client is the reconcile lead (sole client always leads;\n * otherwise elected via {@link isReconcileLeadClient}) and how many other\n * visible human peers are present.\n * - Seed an empty shared Y.Doc once from `value` — lead client only — so two\n * clients opening a brand-new block don't both insert the content.\n * - Reconcile authoritative external markdown (agent edit, source patch, peer\n * edit mirrored to SQL) into the editor: in collab mode only the lead client\n * applies it through `setContent` and Yjs propagates; in non-collab mode this\n * is the original controlled-value reconcile.\n * - Provide the `onUpdate` guards (`shouldIgnoreUpdate`, `registerEmitted`) so\n * the component never persists remote-origin or pre-seed empty content.\n */\n/** Default seed predicate: seed only when the shared doc is genuinely empty. */\nfunction defaultShouldSeed({\n currentMarkdown,\n fragmentLength,\n}: {\n value: string;\n currentMarkdown: string;\n fragmentLength: number;\n}): boolean {\n return fragmentLength === 0 || !currentMarkdown.trim();\n}\n\n/**\n * Default content writer: hand the raw markdown string to `setContent`, which\n * tiptap-markdown overrides to parse the markdown into a ProseMirror doc.\n *\n * IMPORTANT: do NOT pass `parseOptions: { preserveWhitespace: \"full\" }` here.\n * In tiptap v3 the core `setContent` command routes `preserveWhitespace: \"full\"`\n * through `insertContentAt`, which tiptap-markdown ALSO overrides to re-run its\n * markdown parser. That double-parse stringifies the already-parsed PM doc and\n * re-parses it as HTML, so a clean heading/list/code block comes back as the\n * escaped, non-idempotent `&lt;h1&gt;…` — which then escalates every reconcile\n * cycle (`<p>` → `&lt;p&gt;` → `&amp;lt;p&amp;gt;` …). Letting the markdown\n * override parse the string directly (no `parseOptions`) round-trips byte-stably\n * for the GFM corpus, including code-block and empty-line whitespace. Content's\n * NFM path supplies its own `setContent` (it passes a pre-parsed PM doc) and is\n * unaffected by this default.\n */\nfunction defaultSetContent(\n editor: Editor,\n value: string,\n options: { emitUpdate?: boolean; addToHistory?: boolean },\n): void {\n if (options.addToHistory === false) {\n editor\n .chain()\n .command(({ tr }) => {\n // addToHistory:false so cmd+z (or Yjs undo) doesn't erase\n // externally-loaded content.\n tr.setMeta(\"addToHistory\", false);\n return true;\n })\n .setContent(value, { emitUpdate: options.emitUpdate })\n .run();\n return;\n }\n editor.commands.setContent(value);\n}\n\nexport function useCollabReconcile({\n editor,\n ydoc = null,\n awareness = null,\n value,\n contentUpdatedAt,\n editable,\n getMarkdown = getEditorMarkdown,\n setContent = defaultSetContent,\n normalizeValue = (v) => v,\n shouldSeed = defaultShouldSeed,\n initialAppliedUpdatedAt,\n}: UseCollabReconcileOptions): UseCollabReconcileResult {\n const collab = !!ydoc;\n const isSettingContentRef = useRef(false);\n const lastEmittedRef = useRef(\"\");\n // Ring of recent local emissions (see pushEmittedRing). Lets the reconcile\n // recognize a stale-but-recent echo of our OWN (possibly partial, debounced)\n // save so a lagging poll never clobbers freshly-typed text.\n const recentEmittedRef = useRef<string[]>([]);\n const lastTypedAtRef = useRef(0);\n // The raw authoritative `value` string the reconcile last applied. When the\n // SAME raw string is re-fetched (a lagging poll, or a source-sync that keeps\n // re-supplying the same stored markdown), applying it again would only\n // reproduce the doc we already hold — and if `value` is NON-idempotent\n // (serialize(parse(value)) !== value) re-applying compounds the divergence\n // every cycle (`<p>` → `&lt;p&gt;` → `&amp;lt;p&amp;gt;` …). Tracked so the\n // identical re-fetch is recognized and skipped.\n const lastAppliedValueRef = useRef<string | null>(null);\n // The editor's SERIALIZED output captured right AFTER the last reconcile/seed\n // apply (`getMarkdown(editor)` once the content settled). For non-idempotent\n // input this is what autosave actually persists, so the NEXT poll hands it\n // back as the new `value`. Comparing the incoming value against this lets the\n // reconcile recognize its own echo even when the raw string changed once, so\n // it never re-parses content the editor already represents. This is the\n // doc-equivalence guard that breaks the escalation loop.\n const lastAppliedSerializedRef = useRef<string | null>(null);\n const lastAppliedUpdatedAtRef = useRef<string | null>(\n initialAppliedUpdatedAt !== undefined\n ? initialAppliedUpdatedAt\n : (contentUpdatedAt ?? null),\n );\n\n // Whether THIS client is the one that seeds the empty shared doc / applies an\n // authoritative external snapshot into it. Exactly one client does, so the\n // content isn't inserted once per open editor. A sole client always leads.\n const [isLeadClient, setIsLeadClient] = useState(true);\n // Count of OTHER visible human collaborators. When >0, a peer's edit also\n // arrives via Yjs, so external markdown reconcile must defer (avoid applying\n // the same change through both Yjs and setContent).\n const peerCountRef = useRef(0);\n useEffect(() => {\n if (!collab || !awareness || !ydoc) {\n setIsLeadClient(true);\n peerCountRef.current = 0;\n return;\n }\n const update = () => {\n setIsLeadClient(isReconcileLeadClient(awareness, ydoc.clientID));\n let peers = 0;\n awareness.getStates().forEach((state, clientId) => {\n if (clientId === ydoc.clientID) return; // self\n if (clientId === AGENT_CLIENT_ID) return; // agent isn't a Yjs editor\n const s = state as { user?: unknown; visible?: boolean };\n if (s && s.user && s.visible !== false) peers += 1;\n });\n peerCountRef.current = peers;\n };\n update();\n awareness.on(\"change\", update);\n document.addEventListener(\"visibilitychange\", update);\n return () => {\n awareness.off(\"change\", update);\n document.removeEventListener(\"visibilitychange\", update);\n };\n }, [collab, awareness, ydoc]);\n\n // Collab seed: populate an empty shared Y.Doc from the markdown `value` once.\n // The Collaboration extension does NOT auto-seed; only the lead client does,\n // so two clients opening a brand-new block at once don't both seed (which\n // would duplicate the content via concurrent inserts at the same position).\n const seededRef = useRef(false);\n useEffect(() => {\n if (!collab || !editor || editor.isDestroyed || !ydoc) return;\n if (seededRef.current) return;\n if (!isLeadClient) return;\n if (!value.trim()) return;\n const fragment = ydoc.getXmlFragment(\"default\");\n const currentMarkdown = getMarkdown(editor);\n // Seed only when the shared doc is genuinely empty — either the fragment has\n // no nodes yet, or it holds no semantic markdown (an empty paragraph, or an\n // app's sentinel-empty filler via a custom `shouldSeed`).\n if (\n shouldSeed({ value, currentMarkdown, fragmentLength: fragment.length })\n ) {\n isSettingContentRef.current = true;\n setContent(editor, value, {});\n isSettingContentRef.current = false;\n const serialized = getMarkdown(editor);\n lastEmittedRef.current = serialized;\n pushEmittedRing(recentEmittedRef.current, serialized);\n lastAppliedValueRef.current = value;\n lastAppliedSerializedRef.current = serialized;\n if (contentUpdatedAt) lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n seededRef.current = true;\n }, [\n collab,\n editor,\n ydoc,\n value,\n isLeadClient,\n contentUpdatedAt,\n getMarkdown,\n setContent,\n shouldSeed,\n ]);\n\n // Reconcile authoritative external markdown (agent edit, source patch, or a\n // peer edit mirrored to SQL) into the live editor. In collab mode only the\n // lead client applies it through setContent; Yjs propagates the result to\n // every other client. In non-collab mode this is the original controlled-value\n // reconcile, unchanged.\n useEffect(() => {\n if (!editor || editor.isDestroyed) return;\n\n let cancelled = false;\n let retry: ReturnType<typeof setTimeout> | null = null;\n // With peers present, a peer's edit also arrives via Yjs. Defer one poll\n // cycle (+margin) and re-check before applying via setContent so the same\n // change isn't inserted twice (Yjs + setContent → duplicated region).\n const PEER_SETTLE_MS = 2500;\n\n const apply = (deferred = false) => {\n if (cancelled || editor.isDestroyed) return;\n // In collab mode, defer all reconcile until the shared doc is seeded so we\n // never setContent over an unseeded fragment.\n if (collab && !seededRef.current) {\n retry = setTimeout(() => apply(deferred), 300);\n return;\n }\n const currentMarkdown = getMarkdown(editor);\n // Compare against the canonical form the editor would emit so a serializer\n // that re-normalizes (Content's NFM) still recognizes \"already in sync\".\n const normalizedValue = normalizeValue(value);\n // Whether the editor still holds exactly what THIS hook last applied (the\n // user hasn't edited since). Only then are the round-trip echo guards\n // below safe: if the user has since edited away from the applied content,\n // an external snapshot equal to a previously-applied value is a real\n // revert and must NOT be swallowed as an echo.\n const editorUnchangedSinceApply =\n lastAppliedSerializedRef.current !== null &&\n currentMarkdown === lastAppliedSerializedRef.current;\n\n // Doc-equivalence skip. Never re-apply content the editor already\n // represents — comparing by DOC EQUIVALENCE, not raw strings/timestamps:\n // 1. `currentMarkdown === normalizedValue` — the editor's CURRENT\n // serialized doc already equals the (normalized) incoming value.\n // 2. `value === lastEmittedRef.current` — the incoming value is our own\n // just-emitted markdown echoing back.\n // 3. `value === lastAppliedValueRef.current` — the SAME raw value we\n // already applied is being re-supplied (a lagging poll or a\n // source-sync re-handing the same stored markdown). Applying it again\n // would only reproduce the doc we hold; for NON-idempotent input it\n // would compound divergence. Guarded by `editorUnchangedSinceApply`\n // so a deliberate revert-to-previous after a local edit still lands.\n // 4. `normalizedValue === lastAppliedSerializedRef.current` — the\n // incoming value round-trips to the serialized output we last\n // produced (our own autosaved echo coming back from SQL). For\n // non-idempotent input the raw string differs from what we were\n // handed, but it is doc-equivalent to what the editor already shows,\n // so re-parsing it must be skipped. This is the guard that stops the\n // `<p>` → `&lt;p&gt;` → `&amp;lt;p&amp;gt;` escalation.\n if (\n currentMarkdown === normalizedValue ||\n value === lastEmittedRef.current ||\n // A stale-but-recent echo of our own (possibly partial) save — applying\n // it would clobber the freshly-typed tail. External edits never match.\n recentEmittedRef.current.includes(value) ||\n recentEmittedRef.current.includes(normalizedValue) ||\n (editorUnchangedSinceApply &&\n (value === lastAppliedValueRef.current ||\n normalizedValue === lastAppliedSerializedRef.current))\n ) {\n if (contentUpdatedAt) {\n lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n return;\n }\n\n const externalNewer =\n !lastAppliedUpdatedAtRef.current ||\n !contentUpdatedAt ||\n contentUpdatedAt > lastAppliedUpdatedAtRef.current;\n\n // Only the lead client applies an authoritative snapshot into the shared\n // Y.Doc; peers receive it through Yjs sync.\n if (collab && !isLeadClient) {\n if (contentUpdatedAt && !externalNewer) {\n lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n return;\n }\n\n // Never clobber an in-progress edit. While the user is actively typing\n // (focused and a keystroke landed within the window) defer and re-check —\n // applying external content now would yank text out from under them and,\n // for non-idempotent input, fight every keystroke. Newer external content\n // retries so it still lands once they pause; older-or-equal content is a\n // stale poll and is dropped outright while focused.\n const typingRecently =\n editor.isFocused && Date.now() - lastTypedAtRef.current < 1500;\n if (typingRecently) {\n if (externalNewer) {\n retry = setTimeout(() => apply(deferred), 700);\n }\n return;\n }\n if (!externalNewer && editor.isFocused) return;\n\n // Race guard: with peers present, let Yjs deliver a peer's edit first.\n // Defer once and re-check — a peer edit makes the equality check above\n // no-op next pass; an agent/source edit still differs and applies.\n if (collab && externalNewer && !deferred && peerCountRef.current > 0) {\n retry = setTimeout(() => apply(true), PEER_SETTLE_MS);\n return;\n }\n\n queueMicrotask(() => {\n if (cancelled || editor.isDestroyed) return;\n // Re-check doc-equivalence at apply time. Between the decision above and\n // this microtask a peer/Yjs edit (or our own prior apply) may have made\n // the editor already represent this value — re-applying would be a\n // wasted setContent that, for non-idempotent input, re-triggers the\n // loop. Skip when the editor's current serialization already matches the\n // normalized value, or the value round-trips to what we last produced.\n const beforeMarkdown = getMarkdown(editor);\n const normalized = normalizeValue(value);\n const unchangedSinceApply =\n lastAppliedSerializedRef.current !== null &&\n beforeMarkdown === lastAppliedSerializedRef.current;\n if (\n beforeMarkdown === normalized ||\n (unchangedSinceApply &&\n normalized === lastAppliedSerializedRef.current)\n ) {\n lastAppliedValueRef.current = value;\n if (contentUpdatedAt) {\n lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n return;\n }\n isSettingContentRef.current = true;\n setContent(editor, value, { emitUpdate: false, addToHistory: false });\n isSettingContentRef.current = false;\n // Capture the SERIALIZED result, not the raw value. For non-idempotent\n // input these differ; recording the serialized output is what lets the\n // next poll (which returns this serialized form) be recognized as our\n // own echo and skipped — stabilizing the doc after exactly one apply.\n const serialized = getMarkdown(editor);\n lastEmittedRef.current = serialized;\n pushEmittedRing(recentEmittedRef.current, serialized);\n lastAppliedValueRef.current = value;\n lastAppliedSerializedRef.current = serialized;\n if (contentUpdatedAt) {\n lastAppliedUpdatedAtRef.current = contentUpdatedAt;\n }\n });\n };\n\n apply();\n return () => {\n cancelled = true;\n if (retry) clearTimeout(retry);\n };\n }, [\n contentUpdatedAt,\n editor,\n value,\n collab,\n isLeadClient,\n getMarkdown,\n setContent,\n normalizeValue,\n ]);\n\n const shouldIgnoreUpdate = (transaction: Transaction): boolean => {\n if (!editable || isSettingContentRef.current) return true;\n // In collab mode, never persist remote-originated changes (the initial Yjs\n // state load or a peer's edit arriving via sync). Each client saves only its\n // OWN local edits; a peer's edit is saved by that peer. Without this, a\n // lagging Y.Doc load would write stale markdown over newer SQL.\n if (collab && transaction && isChangeOrigin(transaction)) return true;\n lastTypedAtRef.current = Date.now();\n return false;\n };\n\n const registerEmitted = (markdown: string): boolean => {\n // Don't persist an empty doc before Collaboration has seeded — that would\n // clobber the saved block content with an empty string.\n if (collab && !markdown.trim()) return false;\n lastEmittedRef.current = markdown;\n pushEmittedRing(recentEmittedRef.current, markdown);\n return true;\n };\n\n return {\n collab,\n isSettingContentRef,\n shouldIgnoreUpdate,\n registerEmitted,\n };\n}\n"]}
@@ -10,7 +10,7 @@ Codex, Claude Code, Markdown, or pasted implementation plan into a structured
10
10
  review surface with rich text, diagrams, wireframes, prototypes, implementation
11
11
  maps, annotations, comments, and shareable links.
12
12
 
13
- ![Agent-Native Plans review surface](https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2Fb6f4213ac7cc42eeb10c12e8ccda8936?format=webp&width=1200)
13
+ ![Agent-Native Plans review surface](https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2Fdd73f749f8c54dbcb577420ab1a18788)
14
14
 
15
15
  ## Install the skill
16
16
 
@@ -31,6 +31,7 @@ agent-native skills add visual-plan
31
31
  The command installs `/visual-plan` plus the companion commands:
32
32
 
33
33
  - `/ui-plan` for UI-first plans with mockups, states, and screen-level review.
34
+ - `/prototype-plan` for clickable prototype-first plans with live comments.
34
35
  - `/visual-questions` for visual intake before a plan.
35
36
  - `/visualize-plan` for turning an existing text plan into a visual companion.
36
37
 
@@ -58,6 +59,8 @@ After installation, ask your agent for the command that fits the work:
58
59
  or mixed product work.
59
60
  - `/ui-plan` creates a UI-first plan with wireframes, mockups, states, and
60
61
  implementation notes.
62
+ - `/prototype-plan` creates a clickable prototype above the plan document, with
63
+ static mocks, comments, and a focused browser popout.
61
64
  - `/visual-questions` opens a visual intake questionnaire before planning.
62
65
  - `/visualize-plan` imports a plan you already have and makes it reviewable.
63
66
 
@@ -66,12 +69,23 @@ wrong direction would be costly. The returned Plans link opens the review UI so
66
69
  you can annotate, correct, choose options, and ask for updates before code
67
70
  changes begin.
68
71
 
72
+ If the first pass still has answerable decisions, the agent can place an
73
+ **Open Questions** form at the bottom of the same plan. Answering it and sending
74
+ it to the agent starts a revision turn against the existing plan.
75
+
69
76
  ## What you can do with it
70
77
 
71
78
  - **Review before implementation.** React to diagrams, wireframes, option tabs,
72
- risk notes, file maps, and code previews before the agent edits files.
73
- - **Comment directly on the plan.** Pin feedback, request changes, and resolve
74
- comments as the plan evolves.
79
+ Open Questions forms, risk notes, file maps, and code previews before the
80
+ agent edits files.
81
+ - **Comment directly on the plan.** Pin feedback to text, images, wireframes, or
82
+ canvas locations; choose whether the comment is for the agent or a human
83
+ reviewer; @mention teammates with inline chips; and resolve comments as the
84
+ plan evolves.
85
+ - **Hand feedback to the agent clearly.** Text comments attach to the nearest
86
+ prose block, visual comments include exact target metadata, and browser
87
+ handoff includes focused screenshots for a small set of visual/canvas comment
88
+ locations instead of one hard-to-read giant image.
75
89
  - **Share with reviewers.** Hosted Plans can create private review links and
76
90
  account-backed sharing. Viewing shared plans works from the browser; saving
77
91
  and sharing require sign-in.
@@ -85,6 +99,7 @@ changes begin.
85
99
 
86
100
  - "Use `/visual-plan` before changing the auth flow."
87
101
  - "Create a `/ui-plan` for the new onboarding screen with mobile and desktop states."
102
+ - "Create a `/prototype-plan` for the checkout flow so I can click through it."
88
103
  - "Use `/visual-questions` to help me choose the dashboard direction first."
89
104
  - "Run `/visualize-plan` on the Markdown plan below and make it easier to review."
90
105
 
@@ -5,7 +5,7 @@ description: "Turn your coding agent's plans into interactive, reviewable docume
5
5
 
6
6
  # Visual Plans
7
7
 
8
- `/visual-plan` is a coding-agent skill that turns the plan your agent would normally write in Markdown into a **structured visual document**: an optional pan/zoom wireframe canvas on top and a Notion-like technical document below, with diagrams, mockups, prototype options, annotations, and comments you can react to before any code changes.
8
+ `/visual-plan` is a coding-agent skill that turns the plan your agent would normally write in Markdown into a **structured visual document**: an optional pan/zoom wireframe canvas on top and a Notion-like technical document below, with diagrams, mockups, prototype options, answerable Open Questions, annotations, and comments you can react to before any code changes.
9
9
 
10
10
  There are two ways into Plans:
11
11
 
@@ -45,6 +45,8 @@ Once installed, use the slash command that fits the work:
45
45
 
46
46
  The agent gates hard: it only builds a polished visual plan when a wrong direction would be costly, and skips it for trivial, unambiguous work. Each command generates a plan and opens the editor.
47
47
 
48
+ When a plan has unresolved decisions that are useful to answer after the first pass, the agent can put them in an **Open Questions** form at the bottom of the same plan. You can choose single or multiple options, fill in freeform answers, and send the answers back to the agent to revise the plan.
49
+
48
50
  ## Editing in the browser as a guest {#guest}
49
51
 
50
52
  People you share a plan with do not need to install anything. They open the Plans editor and **create and edit with no sign-up** — they work as a guest. Signing in is only required when someone wants to **save or share** their own work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"