@agent-native/toolkit 0.4.10 → 0.5.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.
- package/README.md +17 -6
- package/dist/collab-ui/agent-identity.d.ts +9 -0
- package/dist/collab-ui/agent-identity.d.ts.map +1 -0
- package/dist/collab-ui/agent-identity.js +9 -0
- package/dist/collab-ui/agent-identity.js.map +1 -0
- package/dist/collab-ui/index.d.ts +2 -0
- package/dist/collab-ui/index.d.ts.map +1 -1
- package/dist/collab-ui/index.js +2 -0
- package/dist/collab-ui/index.js.map +1 -1
- package/dist/collab-ui/lead-client.d.ts +3 -0
- package/dist/collab-ui/lead-client.d.ts.map +1 -0
- package/dist/collab-ui/lead-client.js +29 -0
- package/dist/collab-ui/lead-client.js.map +1 -0
- package/dist/context-ui/ContextMeter.d.ts +10 -0
- package/dist/context-ui/ContextMeter.d.ts.map +1 -0
- package/dist/context-ui/ContextMeter.js +20 -0
- package/dist/context-ui/ContextMeter.js.map +1 -0
- package/dist/context-ui/ContextSegmentRow.d.ts +14 -0
- package/dist/context-ui/ContextSegmentRow.d.ts.map +1 -0
- package/dist/context-ui/ContextSegmentRow.js +25 -0
- package/dist/context-ui/ContextSegmentRow.js.map +1 -0
- package/dist/context-ui/ContextTreemap.d.ts +7 -0
- package/dist/context-ui/ContextTreemap.d.ts.map +1 -0
- package/dist/context-ui/ContextTreemap.js +55 -0
- package/dist/context-ui/ContextTreemap.js.map +1 -0
- package/dist/context-ui/ContextXRayPanel.d.ts +13 -0
- package/dist/context-ui/ContextXRayPanel.d.ts.map +1 -0
- package/dist/context-ui/ContextXRayPanel.js +120 -0
- package/dist/context-ui/ContextXRayPanel.js.map +1 -0
- package/dist/context-ui/format.d.ts +6 -0
- package/dist/context-ui/format.d.ts.map +1 -0
- package/dist/context-ui/format.js +58 -0
- package/dist/context-ui/format.js.map +1 -0
- package/dist/context-ui/index.d.ts +7 -0
- package/dist/context-ui/index.d.ts.map +1 -0
- package/dist/context-ui/index.js +6 -0
- package/dist/context-ui/index.js.map +1 -0
- package/dist/context-ui/types.d.ts +39 -0
- package/dist/context-ui/types.d.ts.map +1 -0
- package/dist/context-ui/types.js +2 -0
- package/dist/context-ui/types.js.map +1 -0
- package/dist/design-tweaks/index.d.ts +2 -0
- package/dist/design-tweaks/index.d.ts.map +1 -0
- package/dist/design-tweaks/index.js +2 -0
- package/dist/design-tweaks/index.js.map +1 -0
- package/dist/design-tweaks/visual-style-controls.d.ts +90 -0
- package/dist/design-tweaks/visual-style-controls.d.ts.map +1 -0
- package/dist/design-tweaks/visual-style-controls.js +499 -0
- package/dist/design-tweaks/visual-style-controls.js.map +1 -0
- package/dist/editor/BubbleToolbar.d.ts +37 -0
- package/dist/editor/BubbleToolbar.d.ts.map +1 -0
- package/dist/editor/BubbleToolbar.js +171 -0
- package/dist/editor/BubbleToolbar.js.map +1 -0
- package/dist/editor/CodeBlockNode.d.ts +49 -0
- package/dist/editor/CodeBlockNode.d.ts.map +1 -0
- package/dist/editor/CodeBlockNode.js +127 -0
- package/dist/editor/CodeBlockNode.js.map +1 -0
- package/dist/editor/DragHandle.d.ts +97 -0
- package/dist/editor/DragHandle.d.ts.map +1 -0
- package/dist/editor/DragHandle.js +1066 -0
- package/dist/editor/DragHandle.js.map +1 -0
- package/dist/editor/DragHandle.spec.d.ts +2 -0
- package/dist/editor/DragHandle.spec.d.ts.map +1 -0
- package/dist/editor/DragHandle.spec.js +407 -0
- package/dist/editor/DragHandle.spec.js.map +1 -0
- package/dist/editor/ImageExtension.d.ts +63 -0
- package/dist/editor/ImageExtension.d.ts.map +1 -0
- package/dist/editor/ImageExtension.js +243 -0
- package/dist/editor/ImageExtension.js.map +1 -0
- package/dist/editor/ImageExtension.roundtrip.spec.d.ts +2 -0
- package/dist/editor/ImageExtension.roundtrip.spec.d.ts.map +1 -0
- package/dist/editor/ImageExtension.roundtrip.spec.js +103 -0
- package/dist/editor/ImageExtension.roundtrip.spec.js.map +1 -0
- package/dist/editor/RichMarkdownEditor.collab.spec.d.ts +2 -0
- package/dist/editor/RichMarkdownEditor.collab.spec.d.ts.map +1 -0
- package/dist/editor/RichMarkdownEditor.collab.spec.js +65 -0
- package/dist/editor/RichMarkdownEditor.collab.spec.js.map +1 -0
- package/dist/editor/RichMarkdownEditor.d.ts +52 -0
- package/dist/editor/RichMarkdownEditor.d.ts.map +1 -0
- package/dist/editor/RichMarkdownEditor.js +38 -0
- package/dist/editor/RichMarkdownEditor.js.map +1 -0
- package/dist/editor/RichMarkdownEditor.roundtrip.spec.d.ts +2 -0
- package/dist/editor/RichMarkdownEditor.roundtrip.spec.d.ts.map +1 -0
- package/dist/editor/RichMarkdownEditor.roundtrip.spec.js +186 -0
- package/dist/editor/RichMarkdownEditor.roundtrip.spec.js.map +1 -0
- package/dist/editor/RunId.d.ts +28 -0
- package/dist/editor/RunId.d.ts.map +1 -0
- package/dist/editor/RunId.js +60 -0
- package/dist/editor/RunId.js.map +1 -0
- package/dist/editor/SharedRichEditor.d.ts +110 -0
- package/dist/editor/SharedRichEditor.d.ts.map +1 -0
- package/dist/editor/SharedRichEditor.js +144 -0
- package/dist/editor/SharedRichEditor.js.map +1 -0
- package/dist/editor/SlashCommandMenu.d.ts +41 -0
- package/dist/editor/SlashCommandMenu.d.ts.map +1 -0
- package/dist/editor/SlashCommandMenu.js +221 -0
- package/dist/editor/SlashCommandMenu.js.map +1 -0
- package/dist/editor/disableHistory.spec.d.ts +2 -0
- package/dist/editor/disableHistory.spec.d.ts.map +1 -0
- package/dist/editor/disableHistory.spec.js +73 -0
- package/dist/editor/disableHistory.spec.js.map +1 -0
- package/dist/editor/extensions.d.ts +178 -0
- package/dist/editor/extensions.d.ts.map +1 -0
- package/dist/editor/extensions.js +224 -0
- package/dist/editor/extensions.js.map +1 -0
- package/dist/editor/gfmDoc.d.ts +24 -0
- package/dist/editor/gfmDoc.d.ts.map +1 -0
- package/dist/editor/gfmDoc.js +83 -0
- package/dist/editor/gfmDoc.js.map +1 -0
- package/dist/editor/gfmDoc.spec.d.ts +2 -0
- package/dist/editor/gfmDoc.spec.d.ts.map +1 -0
- package/dist/editor/gfmDoc.spec.js +194 -0
- package/dist/editor/gfmDoc.spec.js.map +1 -0
- package/dist/editor/index.d.ts +13 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +13 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/structuralReorderUndo.spec.d.ts +2 -0
- package/dist/editor/structuralReorderUndo.spec.d.ts.map +1 -0
- package/dist/editor/structuralReorderUndo.spec.js +120 -0
- package/dist/editor/structuralReorderUndo.spec.js.map +1 -0
- package/dist/editor/surgical-apply.d.ts +66 -0
- package/dist/editor/surgical-apply.d.ts.map +1 -0
- package/dist/editor/surgical-apply.js +147 -0
- package/dist/editor/surgical-apply.js.map +1 -0
- package/dist/editor/surgical-apply.spec.d.ts +2 -0
- package/dist/editor/surgical-apply.spec.d.ts.map +1 -0
- package/dist/editor/surgical-apply.spec.js +257 -0
- package/dist/editor/surgical-apply.spec.js.map +1 -0
- package/dist/editor/useCollabReconcile.concurrent.spec.d.ts +2 -0
- package/dist/editor/useCollabReconcile.concurrent.spec.d.ts.map +1 -0
- package/dist/editor/useCollabReconcile.concurrent.spec.js +329 -0
- package/dist/editor/useCollabReconcile.concurrent.spec.js.map +1 -0
- package/dist/editor/useCollabReconcile.d.ts +110 -0
- package/dist/editor/useCollabReconcile.d.ts.map +1 -0
- package/dist/editor/useCollabReconcile.idempotent.spec.d.ts +2 -0
- package/dist/editor/useCollabReconcile.idempotent.spec.d.ts.map +1 -0
- package/dist/editor/useCollabReconcile.idempotent.spec.js +248 -0
- package/dist/editor/useCollabReconcile.idempotent.spec.js.map +1 -0
- package/dist/editor/useCollabReconcile.js +440 -0
- package/dist/editor/useCollabReconcile.js.map +1 -0
- package/dist/editor.css +510 -0
- package/package.json +63 -9
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { useEditor } from "@tiptap/react";
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { createRichMarkdownExtensions } from "./RichMarkdownEditor.js";
|
|
7
|
+
import { useCollabReconcile, getEditorMarkdown } from "./useCollabReconcile.js";
|
|
8
|
+
/**
|
|
9
|
+
* Concurrent-edit / lost-update coverage for the reconcile hook (non-collab
|
|
10
|
+
* controlled-value path — the same guards run there, and it's deterministic
|
|
11
|
+
* without a live Yjs peer). The idempotent spec covers the escalation loop;
|
|
12
|
+
* these cover the OTHER lost-update hazards the hook guards against:
|
|
13
|
+
*
|
|
14
|
+
* - A deliberate revert-to-a-previous-value AFTER a local edit must still land
|
|
15
|
+
* (it must not be swallowed as "our own echo").
|
|
16
|
+
* - registerEmitted must refuse to persist an empty doc in collab mode (so a
|
|
17
|
+
* pre-seed empty editor never writes "" over real stored content).
|
|
18
|
+
*
|
|
19
|
+
* NOTE: the "stale poll arrives WHILE the user is actively typing" guard is
|
|
20
|
+
* gated on `editor.isFocused`, which is always false under happy-dom (no real
|
|
21
|
+
* DOM focus). That path is verified in the browser E2E pass instead.
|
|
22
|
+
*/
|
|
23
|
+
let container;
|
|
24
|
+
let root;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
27
|
+
container = document.createElement("div");
|
|
28
|
+
document.body.appendChild(container);
|
|
29
|
+
root = createRoot(container);
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
act(() => root.unmount());
|
|
33
|
+
container.remove();
|
|
34
|
+
});
|
|
35
|
+
function makeHarness() {
|
|
36
|
+
const captured = { editor: null, emitted: [], setContentCalls: 0 };
|
|
37
|
+
function Harness({ value, contentUpdatedAt }) {
|
|
38
|
+
const guardsRef = React.useRef(null);
|
|
39
|
+
const editor = useEditor({
|
|
40
|
+
extensions: createRichMarkdownExtensions({ dialect: "gfm" }),
|
|
41
|
+
content: value,
|
|
42
|
+
onUpdate: ({ editor, transaction }) => {
|
|
43
|
+
const guards = guardsRef.current;
|
|
44
|
+
if (!guards || guards.shouldIgnoreUpdate(transaction))
|
|
45
|
+
return;
|
|
46
|
+
const markdown = getEditorMarkdown(editor);
|
|
47
|
+
if (!guards.registerEmitted(markdown))
|
|
48
|
+
return;
|
|
49
|
+
captured.emitted.push(markdown);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
captured.editor = editor;
|
|
53
|
+
const guards = useCollabReconcile({
|
|
54
|
+
editor,
|
|
55
|
+
value,
|
|
56
|
+
contentUpdatedAt,
|
|
57
|
+
editable: true,
|
|
58
|
+
getMarkdown: getEditorMarkdown,
|
|
59
|
+
setContent: (ed, v, options) => {
|
|
60
|
+
captured.setContentCalls += 1;
|
|
61
|
+
if (options.addToHistory === false) {
|
|
62
|
+
ed.chain()
|
|
63
|
+
.command(({ tr }) => {
|
|
64
|
+
tr.setMeta("addToHistory", false);
|
|
65
|
+
return true;
|
|
66
|
+
})
|
|
67
|
+
.setContent(v, { emitUpdate: options.emitUpdate })
|
|
68
|
+
.run();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
ed.commands.setContent(v);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
guardsRef.current = guards;
|
|
75
|
+
captured.registerEmitted = guards.registerEmitted;
|
|
76
|
+
return React.createElement("div", null);
|
|
77
|
+
}
|
|
78
|
+
return { captured, Harness };
|
|
79
|
+
}
|
|
80
|
+
function makeCollabSeedHarness(initialContent = "") {
|
|
81
|
+
const captured = { editor: null, emitted: [], setContentCalls: 0 };
|
|
82
|
+
function Harness({ collabSynced, fragmentLength }) {
|
|
83
|
+
const guardsRef = React.useRef(null);
|
|
84
|
+
const fragmentLengthRef = React.useRef(fragmentLength);
|
|
85
|
+
fragmentLengthRef.current = fragmentLength;
|
|
86
|
+
const fakeYdoc = React.useMemo(() => ({
|
|
87
|
+
clientID: 1,
|
|
88
|
+
getXmlFragment: () => ({ length: fragmentLengthRef.current }),
|
|
89
|
+
}), []);
|
|
90
|
+
const editor = useEditor({
|
|
91
|
+
extensions: createRichMarkdownExtensions({ dialect: "gfm" }),
|
|
92
|
+
content: initialContent,
|
|
93
|
+
onUpdate: ({ editor, transaction }) => {
|
|
94
|
+
const guards = guardsRef.current;
|
|
95
|
+
if (!guards || guards.shouldIgnoreUpdate(transaction))
|
|
96
|
+
return;
|
|
97
|
+
const markdown = getEditorMarkdown(editor);
|
|
98
|
+
if (!guards.registerEmitted(markdown))
|
|
99
|
+
return;
|
|
100
|
+
captured.emitted.push(markdown);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
captured.editor = editor;
|
|
104
|
+
const guards = useCollabReconcile({
|
|
105
|
+
editor,
|
|
106
|
+
ydoc: fakeYdoc,
|
|
107
|
+
collabSynced,
|
|
108
|
+
value: "seeded content",
|
|
109
|
+
contentUpdatedAt: "2024-01-01T00:00:01.000Z",
|
|
110
|
+
editable: true,
|
|
111
|
+
getMarkdown: getEditorMarkdown,
|
|
112
|
+
setContent: (ed, v) => {
|
|
113
|
+
captured.setContentCalls += 1;
|
|
114
|
+
ed.commands.setContent(v);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
guardsRef.current = guards;
|
|
118
|
+
return React.createElement("div", null);
|
|
119
|
+
}
|
|
120
|
+
return { captured, Harness };
|
|
121
|
+
}
|
|
122
|
+
async function flush() {
|
|
123
|
+
await act(async () => {
|
|
124
|
+
await Promise.resolve();
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
126
|
+
await Promise.resolve();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function render(root, Harness, props) {
|
|
130
|
+
act(() => {
|
|
131
|
+
root.render(React.createElement(Harness, props));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
describe("useCollabReconcile — concurrent edit / lost-update guards", () => {
|
|
135
|
+
it("does not seed until initial collab sync has completed", async () => {
|
|
136
|
+
const { captured, Harness } = makeCollabSeedHarness();
|
|
137
|
+
act(() => {
|
|
138
|
+
root.render(React.createElement(Harness, {
|
|
139
|
+
collabSynced: false,
|
|
140
|
+
fragmentLength: 0,
|
|
141
|
+
}));
|
|
142
|
+
});
|
|
143
|
+
await flush();
|
|
144
|
+
expect(captured.setContentCalls).toBe(0);
|
|
145
|
+
act(() => {
|
|
146
|
+
root.render(React.createElement(Harness, {
|
|
147
|
+
collabSynced: true,
|
|
148
|
+
fragmentLength: 0,
|
|
149
|
+
}));
|
|
150
|
+
});
|
|
151
|
+
await flush();
|
|
152
|
+
expect(captured.setContentCalls).toBe(1);
|
|
153
|
+
expect(getEditorMarkdown(captured.editor)).toBe("seeded content");
|
|
154
|
+
});
|
|
155
|
+
it("does not seed after initial collab sync reveals existing canonical content", async () => {
|
|
156
|
+
const { captured, Harness } = makeCollabSeedHarness("seeded content");
|
|
157
|
+
act(() => {
|
|
158
|
+
root.render(React.createElement(Harness, {
|
|
159
|
+
collabSynced: false,
|
|
160
|
+
fragmentLength: 0,
|
|
161
|
+
}));
|
|
162
|
+
});
|
|
163
|
+
await flush();
|
|
164
|
+
act(() => {
|
|
165
|
+
root.render(React.createElement(Harness, {
|
|
166
|
+
collabSynced: true,
|
|
167
|
+
fragmentLength: 1,
|
|
168
|
+
}));
|
|
169
|
+
});
|
|
170
|
+
await flush();
|
|
171
|
+
expect(captured.setContentCalls).toBe(0);
|
|
172
|
+
expect(getEditorMarkdown(captured.editor)).toBe("seeded content");
|
|
173
|
+
});
|
|
174
|
+
it("applies a deliberate REVERT to a previously-applied value after a local edit (not swallowed as echo)", async () => {
|
|
175
|
+
// Regression for the revert-safety carve-out: the doc-equivalence echo
|
|
176
|
+
// guards (value === lastAppliedValueRef) only fire when the editor is
|
|
177
|
+
// UNCHANGED since the last apply. If the user has since edited, an external
|
|
178
|
+
// snapshot equal to a previously-applied value is a REAL revert (e.g. the
|
|
179
|
+
// agent restored an earlier version) and must land, not be skipped.
|
|
180
|
+
const { captured, Harness } = makeHarness();
|
|
181
|
+
// 1. Agent applies V1.
|
|
182
|
+
render(root, Harness, {
|
|
183
|
+
value: "# V1 content",
|
|
184
|
+
contentUpdatedAt: "2024-01-01T00:00:01.000Z",
|
|
185
|
+
});
|
|
186
|
+
await flush();
|
|
187
|
+
expect(getEditorMarkdown(captured.editor)).toBe("# V1 content");
|
|
188
|
+
// 2. Agent applies V2 (newer). Now lastApplied tracks V2.
|
|
189
|
+
render(root, Harness, {
|
|
190
|
+
value: "# V2 content",
|
|
191
|
+
contentUpdatedAt: "2024-01-01T00:00:02.000Z",
|
|
192
|
+
});
|
|
193
|
+
await flush();
|
|
194
|
+
expect(getEditorMarkdown(captured.editor)).toBe("# V2 content");
|
|
195
|
+
// 3. The agent REVERTS back to the V1 content with a NEWER timestamp (a
|
|
196
|
+
// genuine "undo my last change" external edit). Even though "# V1 content"
|
|
197
|
+
// was applied before, it must re-apply — the newer timestamp makes it a real
|
|
198
|
+
// external change, and the editor currently shows V2 (not V1).
|
|
199
|
+
render(root, Harness, {
|
|
200
|
+
value: "# V1 content",
|
|
201
|
+
contentUpdatedAt: "2024-01-01T00:00:03.000Z",
|
|
202
|
+
});
|
|
203
|
+
await flush();
|
|
204
|
+
expect(getEditorMarkdown(captured.editor)).toBe("# V1 content");
|
|
205
|
+
});
|
|
206
|
+
it("applies a newer authoritative revert that matches a prior mount-time emission", async () => {
|
|
207
|
+
const { captured, Harness } = makeHarness();
|
|
208
|
+
render(root, Harness, {
|
|
209
|
+
value: "# V1 content",
|
|
210
|
+
contentUpdatedAt: "2024-01-01T00:00:01.000Z",
|
|
211
|
+
});
|
|
212
|
+
await flush();
|
|
213
|
+
render(root, Harness, {
|
|
214
|
+
value: "# V2 content",
|
|
215
|
+
contentUpdatedAt: "2024-01-01T00:00:02.000Z",
|
|
216
|
+
});
|
|
217
|
+
await flush();
|
|
218
|
+
expect(getEditorMarkdown(captured.editor)).toBe("# V2 content");
|
|
219
|
+
// A collab mount/schema-normalization transaction can emit the old V1
|
|
220
|
+
// bytes even though the authoritative apply has already moved the editor
|
|
221
|
+
// to V2. Record that echo without focusing/typing in the editor.
|
|
222
|
+
expect(captured.registerEmitted?.("# V1 content")).toBe(true);
|
|
223
|
+
render(root, Harness, {
|
|
224
|
+
value: "# V1 content",
|
|
225
|
+
contentUpdatedAt: "2024-01-01T00:00:03.000Z",
|
|
226
|
+
});
|
|
227
|
+
await flush();
|
|
228
|
+
expect(getEditorMarkdown(captured.editor)).toBe("# V1 content");
|
|
229
|
+
});
|
|
230
|
+
it("ignores local-looking editor updates until collaborative seeding completes", async () => {
|
|
231
|
+
const results = [];
|
|
232
|
+
function Probe() {
|
|
233
|
+
const editor = useEditor({
|
|
234
|
+
extensions: createRichMarkdownExtensions({ dialect: "gfm" }),
|
|
235
|
+
content: "",
|
|
236
|
+
});
|
|
237
|
+
const fakeYdoc = { clientID: 1, getXmlFragment: () => ({ length: 0 }) };
|
|
238
|
+
const guards = useCollabReconcile({
|
|
239
|
+
editor,
|
|
240
|
+
ydoc: fakeYdoc,
|
|
241
|
+
collabSynced: false,
|
|
242
|
+
value: "authoritative content",
|
|
243
|
+
contentUpdatedAt: "2024-01-01T00:00:01.000Z",
|
|
244
|
+
editable: true,
|
|
245
|
+
});
|
|
246
|
+
if (editor && results.length === 0) {
|
|
247
|
+
results.push(guards.shouldIgnoreUpdate(editor.state.tr));
|
|
248
|
+
}
|
|
249
|
+
return React.createElement("div", null);
|
|
250
|
+
}
|
|
251
|
+
act(() => root.render(React.createElement(Probe)));
|
|
252
|
+
await flush();
|
|
253
|
+
expect(results).toEqual([true]);
|
|
254
|
+
});
|
|
255
|
+
it("refuses to persist an empty doc in collab mode (registerEmitted guard)", async () => {
|
|
256
|
+
// Directly exercise the guard contract: in collab mode an empty markdown
|
|
257
|
+
// string must not be registered/persisted (would clobber stored content
|
|
258
|
+
// before the shared Y.Doc seeds).
|
|
259
|
+
const results = [];
|
|
260
|
+
function Probe() {
|
|
261
|
+
const editor = useEditor({
|
|
262
|
+
extensions: createRichMarkdownExtensions({ dialect: "gfm" }),
|
|
263
|
+
content: "",
|
|
264
|
+
});
|
|
265
|
+
const fakeYdoc = { clientID: 1, getXmlFragment: () => ({ length: 0 }) };
|
|
266
|
+
const guards = useCollabReconcile({
|
|
267
|
+
editor,
|
|
268
|
+
ydoc: fakeYdoc,
|
|
269
|
+
value: "seeded content",
|
|
270
|
+
contentUpdatedAt: "2024-01-01T00:00:01.000Z",
|
|
271
|
+
editable: true,
|
|
272
|
+
});
|
|
273
|
+
if (editor && results.length === 0) {
|
|
274
|
+
results.push(guards.registerEmitted(" ")); // whitespace-only → empty
|
|
275
|
+
results.push(guards.registerEmitted("real text")); // non-empty
|
|
276
|
+
}
|
|
277
|
+
return React.createElement("div", null);
|
|
278
|
+
}
|
|
279
|
+
act(() => root.render(React.createElement(Probe)));
|
|
280
|
+
await flush();
|
|
281
|
+
expect(results[0]).toBe(false); // empty in collab mode → refused
|
|
282
|
+
expect(results[1]).toBe(true); // real content → accepted
|
|
283
|
+
});
|
|
284
|
+
it("defers collab seed setContent to a cancellable timer task", async () => {
|
|
285
|
+
const setContentValues = [];
|
|
286
|
+
function Probe({ value }) {
|
|
287
|
+
const editor = useEditor({
|
|
288
|
+
extensions: createRichMarkdownExtensions({ dialect: "gfm" }),
|
|
289
|
+
content: "",
|
|
290
|
+
});
|
|
291
|
+
const fakeYdoc = { clientID: 1, getXmlFragment: () => ({ length: 0 }) };
|
|
292
|
+
useCollabReconcile({
|
|
293
|
+
editor,
|
|
294
|
+
ydoc: fakeYdoc,
|
|
295
|
+
value,
|
|
296
|
+
contentUpdatedAt: "2024-01-01T00:00:01.000Z",
|
|
297
|
+
editable: true,
|
|
298
|
+
setContent: (ed, v) => {
|
|
299
|
+
setContentValues.push(v);
|
|
300
|
+
ed.commands.setContent(v);
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
return React.createElement("div", null);
|
|
304
|
+
}
|
|
305
|
+
act(() => root.render(React.createElement(Probe, { value: "first seed" })));
|
|
306
|
+
expect(setContentValues).toEqual([]);
|
|
307
|
+
act(() => root.render(React.createElement(Probe, { value: "second seed" })));
|
|
308
|
+
expect(setContentValues).toEqual([]);
|
|
309
|
+
await flush();
|
|
310
|
+
expect(setContentValues).toEqual(["second seed"]);
|
|
311
|
+
});
|
|
312
|
+
it("applies a genuinely newer external value once the user is no longer focused", async () => {
|
|
313
|
+
const { captured, Harness } = makeHarness();
|
|
314
|
+
render(root, Harness, {
|
|
315
|
+
value: "# Doc",
|
|
316
|
+
contentUpdatedAt: "2024-01-01T00:00:01.000Z",
|
|
317
|
+
});
|
|
318
|
+
await flush();
|
|
319
|
+
// Blur the editor so the typing/focus guard does not defer.
|
|
320
|
+
act(() => captured.editor.commands.blur());
|
|
321
|
+
render(root, Harness, {
|
|
322
|
+
value: "# Doc updated by agent",
|
|
323
|
+
contentUpdatedAt: "2024-01-01T00:00:05.000Z",
|
|
324
|
+
});
|
|
325
|
+
await flush();
|
|
326
|
+
expect(getEditorMarkdown(captured.editor)).toBe("# Doc updated by agent");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
//# sourceMappingURL=useCollabReconcile.concurrent.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCollabReconcile.concurrent.spec.js","sourceRoot":"","sources":["../../src/editor/useCollabReconcile.concurrent.spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
|
2
|
+
import type { Transaction } from "@tiptap/pm/state";
|
|
3
|
+
import type { Editor } from "@tiptap/react";
|
|
4
|
+
import { type MutableRefObject } from "react";
|
|
5
|
+
import type { Awareness } from "y-protocols/awareness";
|
|
6
|
+
import type { Doc as YDoc } from "yjs";
|
|
7
|
+
import { RICH_MARKDOWN_PROGRAMMATIC_TRANSACTION } from "./surgical-apply.js";
|
|
8
|
+
export { RICH_MARKDOWN_PROGRAMMATIC_TRANSACTION };
|
|
9
|
+
/** Reads the current markdown out of the tiptap-markdown storage. */
|
|
10
|
+
export declare function getEditorMarkdown(editor: Editor): string;
|
|
11
|
+
export interface UseCollabReconcileOptions {
|
|
12
|
+
/** The live editor, or null until it mounts. */
|
|
13
|
+
editor: Editor | null;
|
|
14
|
+
/** Shared Y.Doc when collaborating; null disables all collab paths. */
|
|
15
|
+
ydoc?: YDoc | null;
|
|
16
|
+
/** True after the collab provider has loaded the persisted initial Y.Doc state. */
|
|
17
|
+
collabSynced?: boolean;
|
|
18
|
+
/** Shared awareness; null keeps the sole-client lead path. */
|
|
19
|
+
awareness?: Awareness | null;
|
|
20
|
+
/** Authoritative markdown value (SQL source of truth). */
|
|
21
|
+
value: string;
|
|
22
|
+
/** Timestamp of the authoritative value; gates newer-than reconcile. */
|
|
23
|
+
contentUpdatedAt?: string | null;
|
|
24
|
+
/** Whether the editor accepts edits. Reconcile/seed only run for the live editor. */
|
|
25
|
+
editable: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Reads the current markdown from the editor. Injected so a dialect could
|
|
28
|
+
* swap serializers; defaults to the tiptap-markdown storage reader. For an app
|
|
29
|
+
* with a custom serializer (e.g. Content's `docToNfm(editor.getJSON())`), pass
|
|
30
|
+
* it here so the seed/reconcile equality checks compare like-for-like.
|
|
31
|
+
*/
|
|
32
|
+
getMarkdown?: (editor: Editor) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Applies the authoritative `value` into the editor. Defaults to passing the
|
|
35
|
+
* raw markdown string to `editor.commands.setContent`. Apps whose serializer
|
|
36
|
+
* is NOT tiptap-markdown (Content parses `nfmToDoc(value)` into a PM doc)
|
|
37
|
+
* override this so seed + reconcile write the correct content shape. The
|
|
38
|
+
* supplied `options` carry the history/whitespace flags the default path uses;
|
|
39
|
+
* a custom implementation should forward them when relevant.
|
|
40
|
+
*/
|
|
41
|
+
setContent?: (editor: Editor, value: string, options: {
|
|
42
|
+
emitUpdate?: boolean;
|
|
43
|
+
addToHistory?: boolean;
|
|
44
|
+
}) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Parses the authoritative `value` into a full ProseMirror document for the
|
|
47
|
+
* SURGICAL reconcile path: instead of a whole-document `setContent` (which
|
|
48
|
+
* under Collaboration rewrites the entire Y.XmlFragment and tears down every
|
|
49
|
+
* block NodeView), the reconcile diffs the parsed doc against the live doc
|
|
50
|
+
* and replaces only the changed top-level run. Return null to skip the
|
|
51
|
+
* surgical path for a given value (falls back to `setContent`).
|
|
52
|
+
*
|
|
53
|
+
* Defaults to a best-effort tiptap-markdown parse; apps with their own
|
|
54
|
+
* serializers (Content's NFM, Plan's blocks[] doc) should supply the exact
|
|
55
|
+
* doc their `setContent` would write. Pass `false` to disable surgical
|
|
56
|
+
* application entirely.
|
|
57
|
+
*/
|
|
58
|
+
parseValue?: ((editor: Editor, value: string) => ProseMirrorNode | null) | false;
|
|
59
|
+
/**
|
|
60
|
+
* Normalizes the authoritative `value` to the canonical markdown the editor
|
|
61
|
+
* would emit, so the "already in sync / our own echo" equality checks match a
|
|
62
|
+
* serializer that re-canonicalizes (Content's `canonicalizeNfm`). Defaults to
|
|
63
|
+
* identity (GFM already round-trips byte-stably).
|
|
64
|
+
*/
|
|
65
|
+
normalizeValue?: (value: string) => string;
|
|
66
|
+
/**
|
|
67
|
+
* Decides whether the empty-doc seed should run for the current shared
|
|
68
|
+
* fragment. Defaults to "fragment has no nodes, or the editor holds no
|
|
69
|
+
* semantic markdown". Apps with sentinel-empty content (Content's
|
|
70
|
+
* `<empty-block/>` filler) override this. Receives the live fragment length
|
|
71
|
+
* and the editor's current markdown.
|
|
72
|
+
*/
|
|
73
|
+
shouldSeed?: (info: {
|
|
74
|
+
value: string;
|
|
75
|
+
currentMarkdown: string;
|
|
76
|
+
fragmentLength: number;
|
|
77
|
+
}) => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* The initial "applied" watermark. Default mirrors `contentUpdatedAt`, so a
|
|
80
|
+
* fresh mount whose Y.Doc already matches SQL doesn't re-apply. Pass `null`
|
|
81
|
+
* to force the first reconcile pass to adopt authoritative SQL even at the
|
|
82
|
+
* same timestamp — Content does this so a stale persisted Y.Doc (an agent that
|
|
83
|
+
* edited the CLOSED doc) is corrected on open. The editor is keyed per
|
|
84
|
+
* document upstream, so this only affects the first mount of each doc.
|
|
85
|
+
*/
|
|
86
|
+
initialAppliedUpdatedAt?: string | null;
|
|
87
|
+
}
|
|
88
|
+
export interface UseCollabReconcileResult {
|
|
89
|
+
/** True when a Y.Doc is bound (collaborative editing active). */
|
|
90
|
+
collab: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Set true around any programmatic `setContent` so the editor's `onUpdate`
|
|
93
|
+
* can ignore the resulting transaction (it isn't a user edit).
|
|
94
|
+
*/
|
|
95
|
+
isSettingContentRef: MutableRefObject<boolean>;
|
|
96
|
+
/**
|
|
97
|
+
* Call from `onUpdate` BEFORE serializing. Returns true when the update must
|
|
98
|
+
* be ignored: editor not editable, mid-programmatic-setContent, or (in collab
|
|
99
|
+
* mode) a remote-origin transaction. Also records the local typing time.
|
|
100
|
+
*/
|
|
101
|
+
shouldIgnoreUpdate: (transaction: Transaction) => boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Call from `onUpdate` AFTER computing the markdown to emit. Returns false
|
|
104
|
+
* when the value must NOT be persisted yet (an empty collab doc before the
|
|
105
|
+
* seed has run); records it as the last-emitted value otherwise.
|
|
106
|
+
*/
|
|
107
|
+
registerEmitted: (markdown: string) => boolean;
|
|
108
|
+
}
|
|
109
|
+
export declare function useCollabReconcile({ editor, ydoc, collabSynced, awareness, value, contentUpdatedAt, editable, getMarkdown, setContent, parseValue, normalizeValue, shouldSeed, initialAppliedUpdatedAt, }: UseCollabReconcileOptions): UseCollabReconcileResult;
|
|
110
|
+
//# sourceMappingURL=useCollabReconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCollabReconcile.d.ts","sourceRoot":"","sources":["../../src/editor/useCollabReconcile.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAA+B,KAAK,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,GAAG,IAAI,IAAI,EAAE,MAAM,KAAK,CAAC;AAGvC,OAAO,EACL,sCAAsC,EAGvC,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,sCAAsC,EAAE,CAAC;AAElD,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,mFAAmF;IACnF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,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;;;;;;;;;;;;OAYG;IACH,UAAU,CAAC,EACP,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,eAAe,GAAG,IAAI,CAAC,GAC3D,KAAK,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,YAAmB,EACnB,SAAgB,EAChB,KAAK,EACL,gBAAgB,EAChB,QAAQ,EACR,WAA+B,EAC/B,UAA8B,EAC9B,UAAU,EACV,cAAyB,EACzB,UAA8B,EAC9B,uBAAuB,GACxB,EAAE,yBAAyB,GAAG,wBAAwB,CAyWtD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCollabReconcile.idempotent.spec.d.ts","sourceRoot":"","sources":["../../src/editor/useCollabReconcile.idempotent.spec.ts"],"names":[],"mappings":""}
|