openwriter 0.28.0 → 0.28.1
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.
- package/dist/server/markdown-parse.js +42 -0
- package/dist/server/markdown.js +1 -1
- package/dist/server/mcp.js +20 -15
- package/package.json +1 -1
|
@@ -830,3 +830,45 @@ function splitParagraphOnDoubleBreaks(content, firstId) {
|
|
|
830
830
|
content: g,
|
|
831
831
|
}));
|
|
832
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* Heal fused double-`<br>` paragraphs across a node array — the structured/JSON
|
|
835
|
+
* sibling of the per-paragraph `splitParagraphOnDoubleBreaks` heal that runs on
|
|
836
|
+
* the markdown-string parse path. Agent writes and Author's Voice rewrites can
|
|
837
|
+
* arrive as TipTap JSON, which never passes through markdown-it, so a tweet-style
|
|
838
|
+
* paragraph carrying a run of 2+ `hardBreak`s would otherwise enter canonical as
|
|
839
|
+
* a single fused node the parser refuses to reproduce — breaking the serialize→
|
|
840
|
+
* reparse round-trip (sync-check FAIL), destabilizing the node-identity matcher,
|
|
841
|
+
* and corrupting pending decorations.
|
|
842
|
+
*
|
|
843
|
+
* Canonical form for every doc type is separate paragraph nodes; intra-paragraph
|
|
844
|
+
* SINGLE `<br>`s (legitimate soft line breaks) pass through untouched. Idempotent:
|
|
845
|
+
* already-split content (e.g. from the markdown path) is returned unchanged, with
|
|
846
|
+
* the original node and all its attrs preserved when no fusion is present.
|
|
847
|
+
*
|
|
848
|
+
* adr: adr/tweet-paragraph-convention.md
|
|
849
|
+
*/
|
|
850
|
+
export function splitFusedParagraphs(nodes) {
|
|
851
|
+
if (!Array.isArray(nodes))
|
|
852
|
+
return nodes;
|
|
853
|
+
const out = [];
|
|
854
|
+
for (const node of nodes) {
|
|
855
|
+
if (node?.type === 'paragraph' && Array.isArray(node.content)) {
|
|
856
|
+
const split = splitParagraphOnDoubleBreaks(node.content, node.attrs?.id || null);
|
|
857
|
+
if (split.length <= 1) {
|
|
858
|
+
// No fusion — keep the original node intact (preserves all attrs).
|
|
859
|
+
out.push(node);
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
// Fusion healed: first chunk inherits the original node's attrs/id,
|
|
863
|
+
// the rest become fresh paragraph nodes.
|
|
864
|
+
split.forEach((p, idx) => {
|
|
865
|
+
out.push(idx === 0 ? { ...node, content: p.content } : p);
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
out.push(node);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return out;
|
|
874
|
+
}
|
package/dist/server/markdown.js
CHANGED
|
@@ -11,7 +11,7 @@ import { tiptapToMarkdown } from './markdown-serialize.js';
|
|
|
11
11
|
import { markdownToTiptap } from './markdown-parse.js';
|
|
12
12
|
import { shapeOfTiptap, compareShapes, formatSyncReport, } from './node-sync-check.js';
|
|
13
13
|
export { tiptapToMarkdown, tiptapToBody, nodeText, inlineToMarkdown } from './markdown-serialize.js';
|
|
14
|
-
export { markdownToTiptap, markdownToNodes } from './markdown-parse.js';
|
|
14
|
+
export { markdownToTiptap, markdownToNodes, splitFusedParagraphs } from './markdown-parse.js';
|
|
15
15
|
export { shapeOfTiptap, computeShape, compareShapes, formatSyncReport, } from './node-sync-check.js';
|
|
16
16
|
/**
|
|
17
17
|
* Serialize TipTap to markdown, then re-parse and verify that the round-trip
|
package/dist/server/mcp.js
CHANGED
|
@@ -25,7 +25,7 @@ import { toCompactFormat, compactNodes, parseMarkdownContent } from './compact.j
|
|
|
25
25
|
import matter from 'gray-matter';
|
|
26
26
|
import { getUpdateInfo } from './update-check.js';
|
|
27
27
|
import { listVersions, forceSnapshot, getVersionContent } from './versions.js';
|
|
28
|
-
import { tiptapToMarkdown } from './markdown.js';
|
|
28
|
+
import { tiptapToMarkdown, splitFusedParagraphs } from './markdown.js';
|
|
29
29
|
import { loadDocFromDisk } from './pending-overlay.js';
|
|
30
30
|
import { getComments, getCommentCount, getGlobalCommentSummary, resolveComments } from './comments.js';
|
|
31
31
|
import { readTasks, addTask, updateTask, removeTask } from './tasks.js';
|
|
@@ -293,14 +293,19 @@ export const TOOL_REGISTRY = [
|
|
|
293
293
|
if (typeof resolved.content === 'string') {
|
|
294
294
|
resolved.content = parseMarkdownContent(resolved.content);
|
|
295
295
|
}
|
|
296
|
-
//
|
|
297
|
-
// paragraph
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
296
|
+
// Canonical form for every doc type (including X templates) is separate
|
|
297
|
+
// paragraph nodes — one node per review unit, intra-paragraph single
|
|
298
|
+
// <br>s preserved. Heal any fused double-<br> paragraph that arrives as
|
|
299
|
+
// TipTap JSON: the markdown-string path above already splits via
|
|
300
|
+
// markdown-it, but JSON content bypasses it, so an X-style fused node
|
|
301
|
+
// would otherwise enter canonical and break the serialize→reparse
|
|
302
|
+
// round-trip (sync-check FAIL), the node-identity matcher, and the
|
|
303
|
+
// pending decorations. Idempotent on already-split content.
|
|
304
|
+
// adr: adr/tweet-paragraph-convention.md
|
|
305
|
+
if (resolved.content != null && resolved.operation !== 'delete') {
|
|
306
|
+
const asArray = Array.isArray(resolved.content) ? resolved.content : [resolved.content];
|
|
307
|
+
resolved.content = splitFusedParagraphs(asArray);
|
|
308
|
+
}
|
|
304
309
|
return resolved;
|
|
305
310
|
});
|
|
306
311
|
// Auto-clean: if doc has only a single empty paragraph and first change is
|
|
@@ -621,12 +626,8 @@ export const TOOL_REGISTRY = [
|
|
|
621
626
|
try {
|
|
622
627
|
let doc;
|
|
623
628
|
if (typeof content === 'string') {
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
// distinct review unit (its own pending-insert decoration); the
|
|
627
|
-
// user accepts/rejects per paragraph. Explicit thread structure
|
|
628
|
-
// (multiple tweets in a thread) comes from horizontalRule nodes
|
|
629
|
-
// in the agent's input, not from paragraph count.
|
|
629
|
+
// Canonical form is separate paragraph nodes for every doc type
|
|
630
|
+
// (including X templates) — each paragraph a distinct review unit.
|
|
630
631
|
doc = { type: 'doc', content: parseMarkdownContent(content) };
|
|
631
632
|
}
|
|
632
633
|
else if (content?.type === 'doc' && Array.isArray(content.content)) {
|
|
@@ -638,6 +639,10 @@ export const TOOL_REGISTRY = [
|
|
|
638
639
|
content: [{ type: 'text', text: 'Error: content must be a markdown string or TipTap JSON { type: "doc", content: [...] }' }],
|
|
639
640
|
};
|
|
640
641
|
}
|
|
642
|
+
// Heal fused double-<br> paragraphs that arrive as TipTap JSON (X-template
|
|
643
|
+
// content bypasses the markdown-string split). Idempotent on the string
|
|
644
|
+
// path above. adr: adr/tweet-paragraph-convention.md
|
|
645
|
+
doc.content = splitFusedParagraphs(doc.content);
|
|
641
646
|
// Non-active target: write directly to disk without disrupting the user's view
|
|
642
647
|
const targetIsNonActive = filename && filename !== getActiveFilename();
|
|
643
648
|
if (targetIsNonActive) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openwriter",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "The open-source writing surface for AI agents. Markdown-native editor with pending change review — your agent writes, you accept or reject.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|