@pilotiq/tiptap 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extensions/AiInlineDiffExtension.d.ts +15 -1
- package/dist/extensions/AiInlineDiffExtension.d.ts.map +1 -1
- package/dist/extensions/AiInlineDiffExtension.js +11 -0
- package/dist/extensions/AiInlineDiffExtension.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/react/useAiInlineDiff.d.ts.map +1 -1
- package/dist/react/useAiInlineDiff.js +184 -33
- package/dist/react/useAiInlineDiff.js.map +1 -1
- package/dist/surgicalOps.d.ts +75 -0
- package/dist/surgicalOps.d.ts.map +1 -0
- package/dist/surgicalOps.js +187 -0
- package/dist/surgicalOps.js.map +1 -0
- package/package.json +1 -1
- package/src/extensions/AiInlineDiffExtension.ts +23 -0
- package/src/index.ts +15 -0
- package/src/react/useAiInlineDiff.ts +200 -30
- package/src/surgicalOps.ts +209 -0
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { Extension } from '@tiptap/core';
|
|
35
35
|
import { PluginKey } from '@tiptap/pm/state';
|
|
36
|
-
import type { EditorState } from '@tiptap/pm/state';
|
|
36
|
+
import type { EditorState, Transaction } from '@tiptap/pm/state';
|
|
37
37
|
import type { Node as ProseMirrorNode, Slice } from '@tiptap/pm/model';
|
|
38
38
|
import { ChangeSet } from 'prosemirror-changeset';
|
|
39
39
|
declare module '@tiptap/core' {
|
|
@@ -49,6 +49,20 @@ declare module '@tiptap/core' {
|
|
|
49
49
|
* the queue entry.
|
|
50
50
|
*/
|
|
51
51
|
startAiInlineDiff: (id: string, newDocSlice: Slice) => ReturnType;
|
|
52
|
+
/**
|
|
53
|
+
* Start the inline-diff review session for a surgical edit.
|
|
54
|
+
* Snapshots the current doc as the baseline, then runs
|
|
55
|
+
* `applyFn(tr)` to mutate the transaction with a precise change
|
|
56
|
+
* (e.g. replace one block, insert before a position, set a mark
|
|
57
|
+
* on a range). The plugin folds the resulting steps into the
|
|
58
|
+
* changeset, so decorations land exactly on the modified ranges
|
|
59
|
+
* — no whole-doc replacement.
|
|
60
|
+
*
|
|
61
|
+
* Use this for `replace_block` / `insert_block_before` /
|
|
62
|
+
* `delete_block` / `update_block_mark` AI ops. Returns false (no
|
|
63
|
+
* dispatch) when `applyFn` produced no doc change.
|
|
64
|
+
*/
|
|
65
|
+
applySurgicalAiInlineDiff: (id: string, applyFn: (tr: Transaction) => void) => ReturnType;
|
|
52
66
|
/** Clear diff state. Current doc IS the accepted state. */
|
|
53
67
|
acceptAiInlineDiff: () => ReturnType;
|
|
54
68
|
/** Revert doc to the captured baseline and clear diff state. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiInlineDiffExtension.d.ts","sourceRoot":"","sources":["../../src/extensions/AiInlineDiffExtension.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAU,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"AiInlineDiffExtension.d.ts","sourceRoot":"","sources":["../../src/extensions/AiInlineDiffExtension.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAU,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAEhE,OAAO,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEjD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,YAAY,EAAE;YACZ;;;;;;;;eAQG;YACH,iBAAiB,EAAG,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,KAAK,UAAU,CAAA;YAClE;;;;;;;;;;;;eAYG;YACH,yBAAyB,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,KAAK,UAAU,CAAA;YACzF,2DAA2D;YAC3D,kBAAkB,EAAE,MAAM,UAAU,CAAA;YACpC,gEAAgE;YAChE,kBAAkB,EAAE,MAAM,UAAU,CAAA;SACrC,CAAA;KACF;CACF;AAED,UAAU,SAAS;IACjB,EAAE,EAAS,MAAM,CAAA;IACjB,2EAA2E;IAC3E,QAAQ,EAAG,eAAe,CAAA;IAC1B,mDAAmD;IACnD,SAAS,EAAE,SAAS,CAAA;CACrB;AAED,eAAO,MAAM,qBAAqB,6BAAyD,CAAA;AAE3F;kEACkE;AAClE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAEzE;AAMD,MAAM,WAAW,4BAA4B;IAC3C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,eAAO,MAAM,qBAAqB,8CAwHhC,CAAA"}
|
|
@@ -91,6 +91,17 @@ export const AiInlineDiffExtension = Extension.create({
|
|
|
91
91
|
dispatch(tr);
|
|
92
92
|
return true;
|
|
93
93
|
},
|
|
94
|
+
applySurgicalAiInlineDiff: (id, applyFn) => ({ tr, state, dispatch }) => {
|
|
95
|
+
const baseline = state.doc;
|
|
96
|
+
applyFn(tr);
|
|
97
|
+
if (!tr.docChanged)
|
|
98
|
+
return false;
|
|
99
|
+
const meta = { type: 'start', id, baseline };
|
|
100
|
+
tr.setMeta(aiInlineDiffPluginKey, meta);
|
|
101
|
+
if (dispatch)
|
|
102
|
+
dispatch(tr);
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
94
105
|
acceptAiInlineDiff: () => ({ tr, dispatch }) => {
|
|
95
106
|
const meta = { type: 'clear' };
|
|
96
107
|
tr.setMeta(aiInlineDiffPluginKey, meta);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiInlineDiffExtension.js","sourceRoot":"","sources":["../../src/extensions/AiInlineDiffExtension.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEpD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAE3D,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"AiInlineDiffExtension.js","sourceRoot":"","sources":["../../src/extensions/AiInlineDiffExtension.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,5 +7,7 @@ export { tiptap } from './plugin.js';
|
|
|
7
7
|
export { TiptapEditor } from './react/TiptapEditor.js';
|
|
8
8
|
export { AiSuggestionExtension, aiSuggestionPluginKey, upsertSuggestion, upsertSuggestions, removeSuggestion, remapSuggestions, sortForApproveAll, clampPos, type AiSuggestion, type AiSuggestionExtensionOptions, } from './extensions/AiSuggestionExtension.js';
|
|
9
9
|
export { useAiSuggestionBridge } from './react/useAiSuggestionBridge.js';
|
|
10
|
+
export { AiInlineDiffExtension, aiInlineDiffPluginKey, getAiInlineDiffState, type AiInlineDiffExtensionOptions, } from './extensions/AiInlineDiffExtension.js';
|
|
11
|
+
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planUpdateBlockMark, summarizeBlockStructure, type BlockMarkRange, type TransactionModifier, } from './surgicalOps.js';
|
|
10
12
|
export { renderRichTextToHtml, isRichTextValue, type RenderRichTextOptions, type TiptapNode, type TiptapMark, } from './render.js';
|
|
11
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,EACxB,KAAK,WAAW,EAChB,KAAK,4BAA4B,EACjC,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAClD,OAAO,EACL,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,mBAAmB,GACzB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EACL,qBAAqB,EACrB,WAAW,EACX,cAAc,EACd,KAAK,sBAAsB,GAC5B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,EACR,KAAK,YAAY,EACjB,KAAK,4BAA4B,GAClC,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AACxE,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,UAAU,EACf,KAAK,UAAU,GAChB,MAAM,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,EACxB,KAAK,WAAW,EAChB,KAAK,4BAA4B,EACjC,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAClD,OAAO,EACL,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,mBAAmB,GACzB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EACL,qBAAqB,EACrB,WAAW,EACX,cAAc,EACd,KAAK,sBAAsB,GAC5B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,EACR,KAAK,YAAY,EACjB,KAAK,4BAA4B,GAClC,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AACxE,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,4BAA4B,GAClC,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,UAAU,EACf,KAAK,UAAU,GAChB,MAAM,aAAa,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -7,5 +7,7 @@ export { tiptap } from './plugin.js';
|
|
|
7
7
|
export { TiptapEditor } from './react/TiptapEditor.js';
|
|
8
8
|
export { AiSuggestionExtension, aiSuggestionPluginKey, upsertSuggestion, upsertSuggestions, removeSuggestion, remapSuggestions, sortForApproveAll, clampPos, } from './extensions/AiSuggestionExtension.js';
|
|
9
9
|
export { useAiSuggestionBridge } from './react/useAiSuggestionBridge.js';
|
|
10
|
+
export { AiInlineDiffExtension, aiInlineDiffPluginKey, getAiInlineDiffState, } from './extensions/AiInlineDiffExtension.js';
|
|
11
|
+
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planUpdateBlockMark, summarizeBlockStructure, } from './surgicalOps.js';
|
|
10
12
|
export { renderRichTextToHtml, isRichTextValue, } from './render.js';
|
|
11
13
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,GAOzB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAkB,MAAM,YAAY,CAAA;AAClD,OAAO,EACL,eAAe,GAGhB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EACL,qBAAqB,EACrB,WAAW,EACX,cAAc,GAEf,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,GAGT,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AACxE,OAAO,EACL,oBAAoB,EACpB,eAAe,GAIhB,MAAM,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,GAOzB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAkB,MAAM,YAAY,CAAA;AAClD,OAAO,EACL,eAAe,GAGhB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EACL,qBAAqB,EACrB,WAAW,EACX,cAAc,GAEf,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,GAGT,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AACxE,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,GAErB,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,uBAAuB,GAGxB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,oBAAoB,EACpB,eAAe,GAIhB,MAAM,aAAa,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAiInlineDiff.d.ts","sourceRoot":"","sources":["../../src/react/useAiInlineDiff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"useAiInlineDiff.d.ts","sourceRoot":"","sources":["../../src/react/useAiInlineDiff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAkB7C,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;;;;OAWG;IACH,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,IAAI,CAAA;CACjE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAMtE;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,sBAAsB,GAC9B,IAAI,CA8GN"}
|
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
* suggestion at a time per id.
|
|
31
31
|
*/
|
|
32
32
|
import { useEffect, useRef } from 'react';
|
|
33
|
+
import { useEditorState } from '@tiptap/react';
|
|
33
34
|
import { registerPendingSuggestionApplier, usePendingSuggestionsForField, } from '@pilotiq/pilotiq/react';
|
|
34
35
|
import { aiInlineDiffPluginKey } from '../extensions/AiInlineDiffExtension.js';
|
|
36
|
+
import { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planUpdateBlockMark, } from '../surgicalOps.js';
|
|
35
37
|
/**
|
|
36
38
|
* Returns whether a diff is currently active in the editor. Hosts use
|
|
37
39
|
* this to gate the banner's UI between the legacy `onApplyWholeField`
|
|
@@ -39,20 +41,11 @@ import { aiInlineDiffPluginKey } from '../extensions/AiInlineDiffExtension.js';
|
|
|
39
41
|
* `rejectAiInlineDiff` to revert the doc).
|
|
40
42
|
*/
|
|
41
43
|
export function useIsAiInlineDiffActive(editor) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!editor)
|
|
48
|
-
return;
|
|
49
|
-
const handler = () => force();
|
|
50
|
-
editor.on('transaction', handler);
|
|
51
|
-
return () => { editor.off('transaction', handler); };
|
|
52
|
-
}, [editor, force]);
|
|
53
|
-
if (!editor)
|
|
54
|
-
return false;
|
|
55
|
-
return aiInlineDiffPluginKey.getState(editor.state) !== null;
|
|
44
|
+
const active = useEditorState({
|
|
45
|
+
editor,
|
|
46
|
+
selector: ({ editor: ed }) => !!ed && aiInlineDiffPluginKey.getState(ed.state) !== null,
|
|
47
|
+
});
|
|
48
|
+
return active ?? false;
|
|
56
49
|
}
|
|
57
50
|
export function useAiInlineDiff(editor, fieldName, options) {
|
|
58
51
|
const { list } = usePendingSuggestionsForField(fieldName);
|
|
@@ -62,20 +55,62 @@ export function useAiInlineDiff(editor, fieldName, options) {
|
|
|
62
55
|
// so we don't re-start the diff every render or for already-active
|
|
63
56
|
// suggestions.
|
|
64
57
|
const startedRef = useRef(new Set());
|
|
65
|
-
// Context → editor: start the diff for each new whole-field
|
|
58
|
+
// Context → editor: start the diff for each new whole-field /
|
|
59
|
+
// surgical-block suggestion. `meta.surgical` (if present) routes to a
|
|
60
|
+
// precise PM transaction; otherwise we treat the suggested value as a
|
|
61
|
+
// whole-field replacement. `meta.editorRange` (chip path) is filtered
|
|
62
|
+
// out — handled by AiSuggestionExtension elsewhere.
|
|
66
63
|
useEffect(() => {
|
|
67
64
|
if (!editor)
|
|
68
65
|
return;
|
|
69
|
-
const
|
|
70
|
-
for (const s of
|
|
66
|
+
const diffable = list.filter(s => !hasEditorRange(s));
|
|
67
|
+
for (const s of diffable) {
|
|
71
68
|
if (startedRef.current.has(s.id))
|
|
72
69
|
continue;
|
|
73
|
-
|
|
70
|
+
const diffActive = aiInlineDiffPluginKey.getState(editor.state) !== null;
|
|
71
|
+
const surgical = readSurgicalMeta(s);
|
|
72
|
+
// Cross-tool-call surgical stacking. When a diff is already active
|
|
73
|
+
// and a fresh surgical suggestion arrives (typically the model
|
|
74
|
+
// emitted a second `update_form_state` tool call instead of
|
|
75
|
+
// batching ops in one), fold the new modifier into the active
|
|
76
|
+
// diff. We dispatch a plain transaction with no extension meta;
|
|
77
|
+
// the plugin's existing "no explicit meta + tr.docChanged" branch
|
|
78
|
+
// adds the steps to the running changeset, so decorations update
|
|
79
|
+
// to cover both ops' ranges and the banner shows the combined
|
|
80
|
+
// count. Accept commits the union, Reject reverts to the same
|
|
81
|
+
// baseline captured when the FIRST suggestion started the diff —
|
|
82
|
+
// semantically "reject all pending suggested changes", which
|
|
83
|
+
// matches the banner copy.
|
|
84
|
+
//
|
|
85
|
+
// Whole-field suggestions still bail when a diff is active —
|
|
86
|
+
// dropping a fresh slice on top of an active review is too
|
|
87
|
+
// disruptive (it'd swap the entire doc mid-review).
|
|
88
|
+
if (diffActive) {
|
|
89
|
+
if (!surgical)
|
|
90
|
+
continue;
|
|
91
|
+
const modifier = planSurgicalModifier(editor, surgical);
|
|
92
|
+
if (!modifier)
|
|
93
|
+
continue;
|
|
94
|
+
editor.commands.command(({ tr, dispatch }) => {
|
|
95
|
+
modifier(tr);
|
|
96
|
+
if (!tr.docChanged)
|
|
97
|
+
return false;
|
|
98
|
+
if (dispatch)
|
|
99
|
+
dispatch(tr);
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
startedRef.current.add(s.id);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (surgical) {
|
|
106
|
+
const modifier = planSurgicalModifier(editor, surgical);
|
|
107
|
+
if (!modifier)
|
|
108
|
+
continue;
|
|
109
|
+
editor.commands.applySurgicalAiInlineDiff(s.id, modifier);
|
|
110
|
+
startedRef.current.add(s.id);
|
|
74
111
|
continue;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// sits in the queue until the first is approved/rejected.
|
|
78
|
-
if (aiInlineDiffPluginKey.getState(editor.state) !== null)
|
|
112
|
+
}
|
|
113
|
+
if (typeof s.suggestedValue !== 'string')
|
|
79
114
|
continue;
|
|
80
115
|
const slice = parseRef.current(editor, s.suggestedValue);
|
|
81
116
|
if (!slice)
|
|
@@ -92,16 +127,41 @@ export function useAiInlineDiff(editor, fieldName, options) {
|
|
|
92
127
|
startedRef.current.delete(id);
|
|
93
128
|
}
|
|
94
129
|
}, [editor, list]);
|
|
95
|
-
// Cross-tree applier —
|
|
96
|
-
//
|
|
97
|
-
// accept
|
|
130
|
+
// Cross-tree applier — two paths:
|
|
131
|
+
//
|
|
132
|
+
// 1. Review-mode accept. Banner / chat-sidebar pill calls
|
|
133
|
+
// `pendingSuggestions.approve(id)` for a suggestion we've already
|
|
134
|
+
// started a diff for. Clear the diff state — current doc IS the
|
|
135
|
+
// accepted state.
|
|
136
|
+
// 2. Auto-mode direct apply. AI tool binding bypassed the queue and
|
|
137
|
+
// called the registry's applier with a synthetic suggestion
|
|
138
|
+
// carrying `meta.surgical` (the suggestion was never pushed to
|
|
139
|
+
// the context, so it's not in `startedRef`). Plan the modifier
|
|
140
|
+
// and dispatch it as a plain transaction — no diff overlay, no
|
|
141
|
+
// Accept / Reject step. Mirrors `set_value` auto-mode, which
|
|
142
|
+
// writes directly via the same registry.
|
|
98
143
|
useEffect(() => {
|
|
99
144
|
if (!editor)
|
|
100
145
|
return;
|
|
101
146
|
const applier = (suggestion) => {
|
|
102
|
-
if (
|
|
147
|
+
if (startedRef.current.has(suggestion.id)) {
|
|
148
|
+
editor.commands.acceptAiInlineDiff();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const surgical = readSurgicalMeta(suggestion);
|
|
152
|
+
if (!surgical)
|
|
153
|
+
return;
|
|
154
|
+
const modifier = planSurgicalModifier(editor, surgical);
|
|
155
|
+
if (!modifier)
|
|
103
156
|
return;
|
|
104
|
-
editor.commands.
|
|
157
|
+
editor.commands.command(({ tr, dispatch }) => {
|
|
158
|
+
modifier(tr);
|
|
159
|
+
if (!tr.docChanged)
|
|
160
|
+
return false;
|
|
161
|
+
if (dispatch)
|
|
162
|
+
dispatch(tr);
|
|
163
|
+
return true;
|
|
164
|
+
});
|
|
105
165
|
};
|
|
106
166
|
return registerPendingSuggestionApplier(undefined, fieldName, applier);
|
|
107
167
|
}, [editor, fieldName]);
|
|
@@ -111,11 +171,102 @@ function hasEditorRange(s) {
|
|
|
111
171
|
const range = meta['editorRange'];
|
|
112
172
|
return !!(range && typeof range.from === 'number' && typeof range.to === 'number');
|
|
113
173
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
174
|
+
function parseSurgicalOp(obj) {
|
|
175
|
+
const op = obj['op'];
|
|
176
|
+
const blockIndex = obj['blockIndex'];
|
|
177
|
+
if (typeof blockIndex !== 'number')
|
|
178
|
+
return null;
|
|
179
|
+
switch (op) {
|
|
180
|
+
case 'replace_block':
|
|
181
|
+
case 'insert_block_before': {
|
|
182
|
+
const content = obj['content'];
|
|
183
|
+
if (typeof content !== 'string')
|
|
184
|
+
return null;
|
|
185
|
+
return { op, blockIndex, content };
|
|
186
|
+
}
|
|
187
|
+
case 'delete_block':
|
|
188
|
+
return { op, blockIndex };
|
|
189
|
+
case 'update_block_mark': {
|
|
190
|
+
const mark = obj['mark'];
|
|
191
|
+
const range = obj['range'];
|
|
192
|
+
const apply = obj['apply'];
|
|
193
|
+
const attrs = obj['attrs'];
|
|
194
|
+
if (typeof mark !== 'string')
|
|
195
|
+
return null;
|
|
196
|
+
if (!range || typeof range.from !== 'number' || typeof range.to !== 'number')
|
|
197
|
+
return null;
|
|
198
|
+
if (typeof apply !== 'boolean')
|
|
199
|
+
return null;
|
|
200
|
+
return {
|
|
201
|
+
op,
|
|
202
|
+
blockIndex,
|
|
203
|
+
mark,
|
|
204
|
+
range: { from: range.from, to: range.to },
|
|
205
|
+
apply,
|
|
206
|
+
...(attrs && typeof attrs === 'object' ? { attrs: attrs } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
default:
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function readSurgicalMeta(s) {
|
|
214
|
+
const meta = (s.meta ?? {});
|
|
215
|
+
const raw = meta['surgical'];
|
|
216
|
+
if (!raw || typeof raw !== 'object')
|
|
217
|
+
return null;
|
|
218
|
+
const obj = raw;
|
|
219
|
+
// Batch form: { ops: [SurgicalOp, ...] }
|
|
220
|
+
if (Array.isArray(obj['ops'])) {
|
|
221
|
+
const parsed = [];
|
|
222
|
+
for (const entry of obj['ops']) {
|
|
223
|
+
if (!entry || typeof entry !== 'object')
|
|
224
|
+
continue;
|
|
225
|
+
const op = parseSurgicalOp(entry);
|
|
226
|
+
if (op)
|
|
227
|
+
parsed.push(op);
|
|
228
|
+
}
|
|
229
|
+
if (parsed.length === 0)
|
|
230
|
+
return null;
|
|
231
|
+
return { ops: parsed };
|
|
232
|
+
}
|
|
233
|
+
return parseSurgicalOp(obj);
|
|
234
|
+
}
|
|
235
|
+
function planOp(editor, op) {
|
|
236
|
+
switch (op.op) {
|
|
237
|
+
case 'replace_block': return planReplaceBlock(editor, op.blockIndex, op.content);
|
|
238
|
+
case 'insert_block_before': return planInsertBlockBefore(editor, op.blockIndex, op.content);
|
|
239
|
+
case 'delete_block': return planDeleteBlock(editor, op.blockIndex);
|
|
240
|
+
case 'update_block_mark': return planUpdateBlockMark(editor, op.blockIndex, op.mark, op.range, op.apply, op.attrs);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Translate a surgical meta into a single TransactionModifier the diff
|
|
245
|
+
* extension can wrap with a snapshot. For batches, modifiers are
|
|
246
|
+
* computed against the original (pre-transaction) doc and then applied
|
|
247
|
+
* in DESC `blockIndex` order — each subsequent modifier touches earlier
|
|
248
|
+
* positions, so the prior modifiers' edits (at higher positions) don't
|
|
249
|
+
* shift the absolute positions the later modifiers were planned with.
|
|
250
|
+
*
|
|
251
|
+
* Returns null when the batch has no plannable ops (all out-of-range /
|
|
252
|
+
* unparseable). Drops individual non-plannable ops from a batch but
|
|
253
|
+
* still runs whatever did plan, so a single bad op doesn't kill the
|
|
254
|
+
* whole batch.
|
|
255
|
+
*/
|
|
256
|
+
function planSurgicalModifier(editor, surgical) {
|
|
257
|
+
if ('ops' in surgical) {
|
|
258
|
+
const sorted = [...surgical.ops].sort((a, b) => b.blockIndex - a.blockIndex);
|
|
259
|
+
const modifiers = [];
|
|
260
|
+
for (const op of sorted) {
|
|
261
|
+
const mod = planOp(editor, op);
|
|
262
|
+
if (mod)
|
|
263
|
+
modifiers.push(mod);
|
|
264
|
+
}
|
|
265
|
+
if (modifiers.length === 0)
|
|
266
|
+
return null;
|
|
267
|
+
return (tr) => { for (const mod of modifiers)
|
|
268
|
+
mod(tr); };
|
|
269
|
+
}
|
|
270
|
+
return planOp(editor, surgical);
|
|
120
271
|
}
|
|
121
272
|
//# sourceMappingURL=useAiInlineDiff.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAiInlineDiff.js","sourceRoot":"","sources":["../../src/react/useAiInlineDiff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAGzC,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAG9B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;
|
|
1
|
+
{"version":3,"file":"useAiInlineDiff.js","sourceRoot":"","sources":["../../src/react/useAiInlineDiff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surgical block-op planners for AI-driven precise edits.
|
|
3
|
+
*
|
|
4
|
+
* Each planner takes the editor + a logical block index + a payload and
|
|
5
|
+
* returns a `TransactionModifier` — a function the caller (typically
|
|
6
|
+
* `useAiInlineDiff`) feeds into
|
|
7
|
+
* `editor.commands.applySurgicalAiInlineDiff(id, modifier)`. The diff
|
|
8
|
+
* extension wraps the modifier in a snapshot-then-apply step so the
|
|
9
|
+
* inline-diff overlay renders against the precise changed range.
|
|
10
|
+
*
|
|
11
|
+
* "Block index" refers to a 0-based position across the doc's top-level
|
|
12
|
+
* children — what the AI agent sees as a numbered structural summary.
|
|
13
|
+
* Planners translate that to absolute ProseMirror positions internally.
|
|
14
|
+
*
|
|
15
|
+
* Planners return `null` when the request can't be satisfied (out-of-
|
|
16
|
+
* range index, unparseable HTML, unknown mark). Callers should treat
|
|
17
|
+
* `null` as "abort the surgical op" and surface a clear error to the
|
|
18
|
+
* agent so it can retry with a different plan.
|
|
19
|
+
*/
|
|
20
|
+
import type { Editor } from '@tiptap/core';
|
|
21
|
+
import type { Transaction } from '@tiptap/pm/state';
|
|
22
|
+
import type { Node as ProseMirrorNode } from '@tiptap/pm/model';
|
|
23
|
+
export type TransactionModifier = (tr: Transaction) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Replace the top-level block at `blockIndex` with the parsed content.
|
|
26
|
+
* `content` is HTML for `RichTextField` (Tiptap) editors and markdown
|
|
27
|
+
* source for `MarkdownField` (markdown-extension) editors —
|
|
28
|
+
* auto-detected by `parseContentToSlice`. Multiple top-level nodes are
|
|
29
|
+
* allowed and will all land where the original block was.
|
|
30
|
+
*/
|
|
31
|
+
export declare function planReplaceBlock(editor: Editor, blockIndex: number, content: string): TransactionModifier | null;
|
|
32
|
+
/**
|
|
33
|
+
* Insert one or more top-level nodes before the block at `blockIndex`.
|
|
34
|
+
* `content` is HTML on `RichTextField` editors and markdown source on
|
|
35
|
+
* `MarkdownField` editors — auto-detected by `parseContentToSlice`.
|
|
36
|
+
* `blockIndex === doc.childCount` appends at the end.
|
|
37
|
+
*/
|
|
38
|
+
export declare function planInsertBlockBefore(editor: Editor, blockIndex: number, content: string): TransactionModifier | null;
|
|
39
|
+
/**
|
|
40
|
+
* Delete the top-level block at `blockIndex`. Doc must retain at least
|
|
41
|
+
* one child after the delete (most schemas require this) — refuses to
|
|
42
|
+
* delete the last remaining block.
|
|
43
|
+
*/
|
|
44
|
+
export declare function planDeleteBlock(editor: Editor, blockIndex: number): TransactionModifier | null;
|
|
45
|
+
export interface BlockMarkRange {
|
|
46
|
+
/** 0-based text offset from the start of the block's content. */
|
|
47
|
+
from: number;
|
|
48
|
+
/** Exclusive end offset. */
|
|
49
|
+
to: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Apply or remove an inline mark on a range *within* the block at
|
|
53
|
+
* `blockIndex`. `range.from` / `range.to` are text offsets relative to
|
|
54
|
+
* the start of the block's content (so `0` is the first character of
|
|
55
|
+
* the block, not the start of the doc).
|
|
56
|
+
*
|
|
57
|
+
* `apply = true` sets the mark (with optional `attrs`); `apply = false`
|
|
58
|
+
* removes it. Unknown marks (not in the editor's schema) return `null`
|
|
59
|
+
* so the caller can surface a clean error to the agent.
|
|
60
|
+
*/
|
|
61
|
+
export declare function planUpdateBlockMark(editor: Editor, blockIndex: number, mark: string, range: BlockMarkRange, apply: boolean, attrs?: Record<string, unknown>): TransactionModifier | null;
|
|
62
|
+
/**
|
|
63
|
+
* Summarize a doc's top-level structure as a numbered list the AI can
|
|
64
|
+
* cite by index when proposing surgical ops. Each entry includes the
|
|
65
|
+
* block index, node type, and a truncated text preview — enough for the
|
|
66
|
+
* model to identify which block it wants to modify without sending the
|
|
67
|
+
* whole HTML/markdown back through token-priced channels.
|
|
68
|
+
*
|
|
69
|
+
* Returns one line per top-level child:
|
|
70
|
+
* `[0] heading: Welcome to the docs`
|
|
71
|
+
* `[1] paragraph: Lorem ipsum dolor sit amet…`
|
|
72
|
+
* `[2] bulletList: 3 items`
|
|
73
|
+
*/
|
|
74
|
+
export declare function summarizeBlockStructure(doc: ProseMirrorNode, maxChars?: number): string;
|
|
75
|
+
//# sourceMappingURL=surgicalOps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"surgicalOps.d.ts","sourceRoot":"","sources":["../src/surgicalOps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,KAAK,EAAkB,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAG/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAA;AA6C3D;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAM,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAK,MAAM,GACjB,mBAAmB,GAAG,IAAI,CAQ5B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAM,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAK,MAAM,GACjB,mBAAmB,GAAG,IAAI,CAQ5B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAM,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,mBAAmB,GAAG,IAAI,CAO5B;AAED,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAA;IACZ,4BAA4B;IAC5B,EAAE,EAAI,MAAM,CAAA;CACb;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAM,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAQ,MAAM,EAClB,KAAK,EAAO,cAAc,EAC1B,KAAK,EAAO,OAAO,EACnB,KAAK,CAAC,EAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,mBAAmB,GAAG,IAAI,CAwB5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,eAAe,EAAE,QAAQ,SAAK,GAAG,MAAM,CAWnF"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surgical block-op planners for AI-driven precise edits.
|
|
3
|
+
*
|
|
4
|
+
* Each planner takes the editor + a logical block index + a payload and
|
|
5
|
+
* returns a `TransactionModifier` — a function the caller (typically
|
|
6
|
+
* `useAiInlineDiff`) feeds into
|
|
7
|
+
* `editor.commands.applySurgicalAiInlineDiff(id, modifier)`. The diff
|
|
8
|
+
* extension wraps the modifier in a snapshot-then-apply step so the
|
|
9
|
+
* inline-diff overlay renders against the precise changed range.
|
|
10
|
+
*
|
|
11
|
+
* "Block index" refers to a 0-based position across the doc's top-level
|
|
12
|
+
* children — what the AI agent sees as a numbered structural summary.
|
|
13
|
+
* Planners translate that to absolute ProseMirror positions internally.
|
|
14
|
+
*
|
|
15
|
+
* Planners return `null` when the request can't be satisfied (out-of-
|
|
16
|
+
* range index, unparseable HTML, unknown mark). Callers should treat
|
|
17
|
+
* `null` as "abort the surgical op" and surface a clear error to the
|
|
18
|
+
* agent so it can retry with a different plan.
|
|
19
|
+
*/
|
|
20
|
+
import { DOMParser as PMDOMParser } from '@tiptap/pm/model';
|
|
21
|
+
/** Resolve the start position of the top-level child at `blockIndex`. */
|
|
22
|
+
function blockStartPos(doc, blockIndex) {
|
|
23
|
+
if (!Number.isInteger(blockIndex) || blockIndex < 0 || blockIndex >= doc.childCount)
|
|
24
|
+
return null;
|
|
25
|
+
let pos = 0;
|
|
26
|
+
for (let i = 0; i < blockIndex; i++)
|
|
27
|
+
pos += doc.child(i).nodeSize;
|
|
28
|
+
return pos;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse content into a doc-replaceable Slice against the editor's
|
|
32
|
+
* schema. Auto-detects markdown editors by sniffing for the
|
|
33
|
+
* `tiptap-markdown` extension's `storage.markdown.parser`: if present,
|
|
34
|
+
* the editor is a `MarkdownEditor` and AI-supplied `content` is
|
|
35
|
+
* markdown source — run it through the markdown parser to produce
|
|
36
|
+
* HTML first. Otherwise content is HTML (the `RichTextField` /
|
|
37
|
+
* `TiptapEditor` path).
|
|
38
|
+
*
|
|
39
|
+
* Mirrors the same auto-detect strategy `MarkdownEditor.tsx` uses for
|
|
40
|
+
* its `parseSuggestion` whole-field callback (see `useAiInlineDiff`),
|
|
41
|
+
* so surgical ops on markdown fields stay consistent with the
|
|
42
|
+
* existing whole-field replacement path.
|
|
43
|
+
*
|
|
44
|
+
* Returns `null` when DOM isn't available (SSR — shouldn't happen
|
|
45
|
+
* here, but keeps the planner safe) or when the markdown parser
|
|
46
|
+
* throws / returns a non-string (malformed content).
|
|
47
|
+
*/
|
|
48
|
+
function parseContentToSlice(editor, content) {
|
|
49
|
+
if (typeof document === 'undefined')
|
|
50
|
+
return null;
|
|
51
|
+
let html = content;
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
const mdParser = editor.storage?.markdown?.parser;
|
|
54
|
+
if (mdParser && typeof mdParser.parse === 'function') {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = mdParser.parse(content);
|
|
57
|
+
if (typeof parsed !== 'string')
|
|
58
|
+
return null;
|
|
59
|
+
html = parsed;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const container = document.createElement('div');
|
|
66
|
+
container.innerHTML = html;
|
|
67
|
+
return PMDOMParser.fromSchema(editor.schema).parseSlice(container);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Replace the top-level block at `blockIndex` with the parsed content.
|
|
71
|
+
* `content` is HTML for `RichTextField` (Tiptap) editors and markdown
|
|
72
|
+
* source for `MarkdownField` (markdown-extension) editors —
|
|
73
|
+
* auto-detected by `parseContentToSlice`. Multiple top-level nodes are
|
|
74
|
+
* allowed and will all land where the original block was.
|
|
75
|
+
*/
|
|
76
|
+
export function planReplaceBlock(editor, blockIndex, content) {
|
|
77
|
+
const doc = editor.state.doc;
|
|
78
|
+
const start = blockStartPos(doc, blockIndex);
|
|
79
|
+
if (start === null)
|
|
80
|
+
return null;
|
|
81
|
+
const slice = parseContentToSlice(editor, content);
|
|
82
|
+
if (!slice)
|
|
83
|
+
return null;
|
|
84
|
+
const end = start + doc.child(blockIndex).nodeSize;
|
|
85
|
+
return (tr) => { tr.replace(start, end, slice); };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Insert one or more top-level nodes before the block at `blockIndex`.
|
|
89
|
+
* `content` is HTML on `RichTextField` editors and markdown source on
|
|
90
|
+
* `MarkdownField` editors — auto-detected by `parseContentToSlice`.
|
|
91
|
+
* `blockIndex === doc.childCount` appends at the end.
|
|
92
|
+
*/
|
|
93
|
+
export function planInsertBlockBefore(editor, blockIndex, content) {
|
|
94
|
+
const doc = editor.state.doc;
|
|
95
|
+
if (!Number.isInteger(blockIndex) || blockIndex < 0 || blockIndex > doc.childCount)
|
|
96
|
+
return null;
|
|
97
|
+
const slice = parseContentToSlice(editor, content);
|
|
98
|
+
if (!slice)
|
|
99
|
+
return null;
|
|
100
|
+
let pos = 0;
|
|
101
|
+
for (let i = 0; i < blockIndex; i++)
|
|
102
|
+
pos += doc.child(i).nodeSize;
|
|
103
|
+
return (tr) => { tr.replace(pos, pos, slice); };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Delete the top-level block at `blockIndex`. Doc must retain at least
|
|
107
|
+
* one child after the delete (most schemas require this) — refuses to
|
|
108
|
+
* delete the last remaining block.
|
|
109
|
+
*/
|
|
110
|
+
export function planDeleteBlock(editor, blockIndex) {
|
|
111
|
+
const doc = editor.state.doc;
|
|
112
|
+
const start = blockStartPos(doc, blockIndex);
|
|
113
|
+
if (start === null)
|
|
114
|
+
return null;
|
|
115
|
+
if (doc.childCount <= 1)
|
|
116
|
+
return null;
|
|
117
|
+
const end = start + doc.child(blockIndex).nodeSize;
|
|
118
|
+
return (tr) => { tr.delete(start, end); };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Apply or remove an inline mark on a range *within* the block at
|
|
122
|
+
* `blockIndex`. `range.from` / `range.to` are text offsets relative to
|
|
123
|
+
* the start of the block's content (so `0` is the first character of
|
|
124
|
+
* the block, not the start of the doc).
|
|
125
|
+
*
|
|
126
|
+
* `apply = true` sets the mark (with optional `attrs`); `apply = false`
|
|
127
|
+
* removes it. Unknown marks (not in the editor's schema) return `null`
|
|
128
|
+
* so the caller can surface a clean error to the agent.
|
|
129
|
+
*/
|
|
130
|
+
export function planUpdateBlockMark(editor, blockIndex, mark, range, apply, attrs) {
|
|
131
|
+
const doc = editor.state.doc;
|
|
132
|
+
const start = blockStartPos(doc, blockIndex);
|
|
133
|
+
if (start === null)
|
|
134
|
+
return null;
|
|
135
|
+
const markType = editor.schema.marks[mark];
|
|
136
|
+
if (!markType)
|
|
137
|
+
return null;
|
|
138
|
+
const block = doc.child(blockIndex);
|
|
139
|
+
const blockInner = start + 1; // step inside the block's opening token
|
|
140
|
+
const contentMax = block.content.size;
|
|
141
|
+
if (!Number.isInteger(range.from) || !Number.isInteger(range.to))
|
|
142
|
+
return null;
|
|
143
|
+
const clampedFrom = Math.max(0, Math.min(range.from, contentMax));
|
|
144
|
+
const clampedTo = Math.max(clampedFrom, Math.min(range.to, contentMax));
|
|
145
|
+
if (clampedTo === clampedFrom)
|
|
146
|
+
return null;
|
|
147
|
+
const from = blockInner + clampedFrom;
|
|
148
|
+
const to = blockInner + clampedTo;
|
|
149
|
+
if (apply) {
|
|
150
|
+
const m = markType.create(attrs ?? null);
|
|
151
|
+
return (tr) => { tr.addMark(from, to, m); };
|
|
152
|
+
}
|
|
153
|
+
return (tr) => { tr.removeMark(from, to, markType); };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Summarize a doc's top-level structure as a numbered list the AI can
|
|
157
|
+
* cite by index when proposing surgical ops. Each entry includes the
|
|
158
|
+
* block index, node type, and a truncated text preview — enough for the
|
|
159
|
+
* model to identify which block it wants to modify without sending the
|
|
160
|
+
* whole HTML/markdown back through token-priced channels.
|
|
161
|
+
*
|
|
162
|
+
* Returns one line per top-level child:
|
|
163
|
+
* `[0] heading: Welcome to the docs`
|
|
164
|
+
* `[1] paragraph: Lorem ipsum dolor sit amet…`
|
|
165
|
+
* `[2] bulletList: 3 items`
|
|
166
|
+
*/
|
|
167
|
+
export function summarizeBlockStructure(doc, maxChars = 80) {
|
|
168
|
+
const lines = [];
|
|
169
|
+
for (let i = 0; i < doc.childCount; i++) {
|
|
170
|
+
const node = doc.child(i);
|
|
171
|
+
const text = node.textContent.trim().replace(/\s+/g, ' ');
|
|
172
|
+
const preview = text.length === 0
|
|
173
|
+
? describeStructuralNode(node)
|
|
174
|
+
: text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
|
|
175
|
+
lines.push(`[${i}] ${node.type.name}: ${preview}`);
|
|
176
|
+
}
|
|
177
|
+
return lines.join('\n');
|
|
178
|
+
}
|
|
179
|
+
function describeStructuralNode(node) {
|
|
180
|
+
const kids = node.childCount;
|
|
181
|
+
if (kids === 0)
|
|
182
|
+
return '(empty)';
|
|
183
|
+
if (kids === 1)
|
|
184
|
+
return `1 ${node.firstChild?.type.name ?? 'child'}`;
|
|
185
|
+
return `${kids} children`;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=surgicalOps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"surgicalOps.js","sourceRoot":"","sources":["../src/surgicalOps.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -52,6 +52,20 @@ declare module '@tiptap/core' {
|
|
|
52
52
|
* the queue entry.
|
|
53
53
|
*/
|
|
54
54
|
startAiInlineDiff: (id: string, newDocSlice: Slice) => ReturnType
|
|
55
|
+
/**
|
|
56
|
+
* Start the inline-diff review session for a surgical edit.
|
|
57
|
+
* Snapshots the current doc as the baseline, then runs
|
|
58
|
+
* `applyFn(tr)` to mutate the transaction with a precise change
|
|
59
|
+
* (e.g. replace one block, insert before a position, set a mark
|
|
60
|
+
* on a range). The plugin folds the resulting steps into the
|
|
61
|
+
* changeset, so decorations land exactly on the modified ranges
|
|
62
|
+
* — no whole-doc replacement.
|
|
63
|
+
*
|
|
64
|
+
* Use this for `replace_block` / `insert_block_before` /
|
|
65
|
+
* `delete_block` / `update_block_mark` AI ops. Returns false (no
|
|
66
|
+
* dispatch) when `applyFn` produced no doc change.
|
|
67
|
+
*/
|
|
68
|
+
applySurgicalAiInlineDiff: (id: string, applyFn: (tr: Transaction) => void) => ReturnType
|
|
55
69
|
/** Clear diff state. Current doc IS the accepted state. */
|
|
56
70
|
acceptAiInlineDiff: () => ReturnType
|
|
57
71
|
/** Revert doc to the captured baseline and clear diff state. */
|
|
@@ -141,6 +155,15 @@ export const AiInlineDiffExtension = Extension.create<AiInlineDiffExtensionOptio
|
|
|
141
155
|
if (dispatch) dispatch(tr)
|
|
142
156
|
return true
|
|
143
157
|
},
|
|
158
|
+
applySurgicalAiInlineDiff: (id, applyFn) => ({ tr, state, dispatch }) => {
|
|
159
|
+
const baseline = state.doc
|
|
160
|
+
applyFn(tr)
|
|
161
|
+
if (!tr.docChanged) return false
|
|
162
|
+
const meta: StartMeta = { type: 'start', id, baseline }
|
|
163
|
+
tr.setMeta(aiInlineDiffPluginKey, meta)
|
|
164
|
+
if (dispatch) dispatch(tr)
|
|
165
|
+
return true
|
|
166
|
+
},
|
|
144
167
|
acceptAiInlineDiff: () => ({ tr, dispatch }) => {
|
|
145
168
|
const meta: ClearMeta = { type: 'clear' }
|
|
146
169
|
tr.setMeta(aiInlineDiffPluginKey, meta)
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,21 @@ export {
|
|
|
38
38
|
type AiSuggestionExtensionOptions,
|
|
39
39
|
} from './extensions/AiSuggestionExtension.js'
|
|
40
40
|
export { useAiSuggestionBridge } from './react/useAiSuggestionBridge.js'
|
|
41
|
+
export {
|
|
42
|
+
AiInlineDiffExtension,
|
|
43
|
+
aiInlineDiffPluginKey,
|
|
44
|
+
getAiInlineDiffState,
|
|
45
|
+
type AiInlineDiffExtensionOptions,
|
|
46
|
+
} from './extensions/AiInlineDiffExtension.js'
|
|
47
|
+
export {
|
|
48
|
+
planReplaceBlock,
|
|
49
|
+
planInsertBlockBefore,
|
|
50
|
+
planDeleteBlock,
|
|
51
|
+
planUpdateBlockMark,
|
|
52
|
+
summarizeBlockStructure,
|
|
53
|
+
type BlockMarkRange,
|
|
54
|
+
type TransactionModifier,
|
|
55
|
+
} from './surgicalOps.js'
|
|
41
56
|
export {
|
|
42
57
|
renderRichTextToHtml,
|
|
43
58
|
isRichTextValue,
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { useEffect, useRef } from 'react'
|
|
34
34
|
import type { Editor } from '@tiptap/core'
|
|
35
35
|
import type { Slice } from '@tiptap/pm/model'
|
|
36
|
+
import { useEditorState } from '@tiptap/react'
|
|
36
37
|
import {
|
|
37
38
|
registerPendingSuggestionApplier,
|
|
38
39
|
usePendingSuggestionsForField,
|
|
@@ -40,6 +41,14 @@ import {
|
|
|
40
41
|
type PendingSuggestionApplier,
|
|
41
42
|
} from '@pilotiq/pilotiq/react'
|
|
42
43
|
import { aiInlineDiffPluginKey } from '../extensions/AiInlineDiffExtension.js'
|
|
44
|
+
import {
|
|
45
|
+
planReplaceBlock,
|
|
46
|
+
planInsertBlockBefore,
|
|
47
|
+
planDeleteBlock,
|
|
48
|
+
planUpdateBlockMark,
|
|
49
|
+
type BlockMarkRange,
|
|
50
|
+
type TransactionModifier,
|
|
51
|
+
} from '../surgicalOps.js'
|
|
43
52
|
|
|
44
53
|
export interface UseAiInlineDiffOptions {
|
|
45
54
|
/**
|
|
@@ -64,18 +73,11 @@ export interface UseAiInlineDiffOptions {
|
|
|
64
73
|
* `rejectAiInlineDiff` to revert the doc).
|
|
65
74
|
*/
|
|
66
75
|
export function useIsAiInlineDiffActive(editor: Editor | null): boolean {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!editor) return
|
|
73
|
-
const handler = () => force()
|
|
74
|
-
editor.on('transaction', handler)
|
|
75
|
-
return () => { editor.off('transaction', handler) }
|
|
76
|
-
}, [editor, force])
|
|
77
|
-
if (!editor) return false
|
|
78
|
-
return aiInlineDiffPluginKey.getState(editor.state) !== null
|
|
76
|
+
const active = useEditorState({
|
|
77
|
+
editor,
|
|
78
|
+
selector: ({ editor: ed }) => !!ed && aiInlineDiffPluginKey.getState(ed.state) !== null,
|
|
79
|
+
})
|
|
80
|
+
return active ?? false
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export function useAiInlineDiff(
|
|
@@ -93,17 +95,58 @@ export function useAiInlineDiff(
|
|
|
93
95
|
// suggestions.
|
|
94
96
|
const startedRef = useRef<Set<string>>(new Set())
|
|
95
97
|
|
|
96
|
-
// Context → editor: start the diff for each new whole-field
|
|
98
|
+
// Context → editor: start the diff for each new whole-field /
|
|
99
|
+
// surgical-block suggestion. `meta.surgical` (if present) routes to a
|
|
100
|
+
// precise PM transaction; otherwise we treat the suggested value as a
|
|
101
|
+
// whole-field replacement. `meta.editorRange` (chip path) is filtered
|
|
102
|
+
// out — handled by AiSuggestionExtension elsewhere.
|
|
97
103
|
useEffect(() => {
|
|
98
104
|
if (!editor) return
|
|
99
|
-
const
|
|
100
|
-
for (const s of
|
|
105
|
+
const diffable = list.filter(s => !hasEditorRange(s))
|
|
106
|
+
for (const s of diffable) {
|
|
101
107
|
if (startedRef.current.has(s.id)) continue
|
|
108
|
+
const diffActive = aiInlineDiffPluginKey.getState(editor.state) !== null
|
|
109
|
+
const surgical = readSurgicalMeta(s)
|
|
110
|
+
|
|
111
|
+
// Cross-tool-call surgical stacking. When a diff is already active
|
|
112
|
+
// and a fresh surgical suggestion arrives (typically the model
|
|
113
|
+
// emitted a second `update_form_state` tool call instead of
|
|
114
|
+
// batching ops in one), fold the new modifier into the active
|
|
115
|
+
// diff. We dispatch a plain transaction with no extension meta;
|
|
116
|
+
// the plugin's existing "no explicit meta + tr.docChanged" branch
|
|
117
|
+
// adds the steps to the running changeset, so decorations update
|
|
118
|
+
// to cover both ops' ranges and the banner shows the combined
|
|
119
|
+
// count. Accept commits the union, Reject reverts to the same
|
|
120
|
+
// baseline captured when the FIRST suggestion started the diff —
|
|
121
|
+
// semantically "reject all pending suggested changes", which
|
|
122
|
+
// matches the banner copy.
|
|
123
|
+
//
|
|
124
|
+
// Whole-field suggestions still bail when a diff is active —
|
|
125
|
+
// dropping a fresh slice on top of an active review is too
|
|
126
|
+
// disruptive (it'd swap the entire doc mid-review).
|
|
127
|
+
if (diffActive) {
|
|
128
|
+
if (!surgical) continue
|
|
129
|
+
const modifier = planSurgicalModifier(editor, surgical)
|
|
130
|
+
if (!modifier) continue
|
|
131
|
+
editor.commands.command(({ tr, dispatch }) => {
|
|
132
|
+
modifier(tr)
|
|
133
|
+
if (!tr.docChanged) return false
|
|
134
|
+
if (dispatch) dispatch(tr)
|
|
135
|
+
return true
|
|
136
|
+
})
|
|
137
|
+
startedRef.current.add(s.id)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (surgical) {
|
|
142
|
+
const modifier = planSurgicalModifier(editor, surgical)
|
|
143
|
+
if (!modifier) continue
|
|
144
|
+
editor.commands.applySurgicalAiInlineDiff(s.id, modifier)
|
|
145
|
+
startedRef.current.add(s.id)
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
102
149
|
if (typeof s.suggestedValue !== 'string') continue
|
|
103
|
-
// Bail when a different diff is already showing — one at a time.
|
|
104
|
-
// Producer should serialize calls; if not, the second suggestion
|
|
105
|
-
// sits in the queue until the first is approved/rejected.
|
|
106
|
-
if (aiInlineDiffPluginKey.getState(editor.state) !== null) continue
|
|
107
150
|
const slice = parseRef.current(editor, s.suggestedValue)
|
|
108
151
|
if (!slice) continue
|
|
109
152
|
editor.commands.startAiInlineDiff(s.id, slice)
|
|
@@ -118,14 +161,36 @@ export function useAiInlineDiff(
|
|
|
118
161
|
}
|
|
119
162
|
}, [editor, list])
|
|
120
163
|
|
|
121
|
-
// Cross-tree applier —
|
|
122
|
-
//
|
|
123
|
-
// accept
|
|
164
|
+
// Cross-tree applier — two paths:
|
|
165
|
+
//
|
|
166
|
+
// 1. Review-mode accept. Banner / chat-sidebar pill calls
|
|
167
|
+
// `pendingSuggestions.approve(id)` for a suggestion we've already
|
|
168
|
+
// started a diff for. Clear the diff state — current doc IS the
|
|
169
|
+
// accepted state.
|
|
170
|
+
// 2. Auto-mode direct apply. AI tool binding bypassed the queue and
|
|
171
|
+
// called the registry's applier with a synthetic suggestion
|
|
172
|
+
// carrying `meta.surgical` (the suggestion was never pushed to
|
|
173
|
+
// the context, so it's not in `startedRef`). Plan the modifier
|
|
174
|
+
// and dispatch it as a plain transaction — no diff overlay, no
|
|
175
|
+
// Accept / Reject step. Mirrors `set_value` auto-mode, which
|
|
176
|
+
// writes directly via the same registry.
|
|
124
177
|
useEffect(() => {
|
|
125
178
|
if (!editor) return
|
|
126
179
|
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
127
|
-
if (
|
|
128
|
-
|
|
180
|
+
if (startedRef.current.has(suggestion.id)) {
|
|
181
|
+
editor.commands.acceptAiInlineDiff()
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
const surgical = readSurgicalMeta(suggestion)
|
|
185
|
+
if (!surgical) return
|
|
186
|
+
const modifier = planSurgicalModifier(editor, surgical)
|
|
187
|
+
if (!modifier) return
|
|
188
|
+
editor.commands.command(({ tr, dispatch }) => {
|
|
189
|
+
modifier(tr)
|
|
190
|
+
if (!tr.docChanged) return false
|
|
191
|
+
if (dispatch) dispatch(tr)
|
|
192
|
+
return true
|
|
193
|
+
})
|
|
129
194
|
}
|
|
130
195
|
return registerPendingSuggestionApplier(undefined, fieldName, applier)
|
|
131
196
|
}, [editor, fieldName])
|
|
@@ -137,10 +202,115 @@ function hasEditorRange(s: PendingSuggestion): boolean {
|
|
|
137
202
|
return !!(range && typeof range.from === 'number' && typeof range.to === 'number')
|
|
138
203
|
}
|
|
139
204
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Surgical op carried in `PendingSuggestion.meta.surgical`. The pilotiq-
|
|
207
|
+
* pro `update_form_state` client handler stamps this when the AI agent
|
|
208
|
+
* picks a block-level op instead of `set_value`.
|
|
209
|
+
*
|
|
210
|
+
* `content` is HTML for replace/insert ops, ignored otherwise. `mark` +
|
|
211
|
+
* `range` apply only to the mark op. Discriminated union; readers should
|
|
212
|
+
* narrow on `op`.
|
|
213
|
+
*/
|
|
214
|
+
type SurgicalOp =
|
|
215
|
+
| { op: 'replace_block'; blockIndex: number; content: string }
|
|
216
|
+
| { op: 'insert_block_before'; blockIndex: number; content: string }
|
|
217
|
+
| { op: 'delete_block'; blockIndex: number }
|
|
218
|
+
| { op: 'update_block_mark'; blockIndex: number; mark: string; range: BlockMarkRange; apply: boolean; attrs?: Record<string, unknown> }
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Either a single op (when the AI emitted only one surgical change) or
|
|
222
|
+
* an `{ ops: [...] }` batch (when the AI emitted multiple surgical ops
|
|
223
|
+
* in one `update_form_state` tool call). We apply a batch as a single
|
|
224
|
+
* combined diff so the user sees one Accept / Reject for the whole set.
|
|
225
|
+
*/
|
|
226
|
+
type SurgicalMeta = SurgicalOp | { ops: SurgicalOp[] }
|
|
227
|
+
|
|
228
|
+
function parseSurgicalOp(obj: Record<string, unknown>): SurgicalOp | null {
|
|
229
|
+
const op = obj['op']
|
|
230
|
+
const blockIndex = obj['blockIndex']
|
|
231
|
+
if (typeof blockIndex !== 'number') return null
|
|
232
|
+
switch (op) {
|
|
233
|
+
case 'replace_block':
|
|
234
|
+
case 'insert_block_before': {
|
|
235
|
+
const content = obj['content']
|
|
236
|
+
if (typeof content !== 'string') return null
|
|
237
|
+
return { op, blockIndex, content }
|
|
238
|
+
}
|
|
239
|
+
case 'delete_block':
|
|
240
|
+
return { op, blockIndex }
|
|
241
|
+
case 'update_block_mark': {
|
|
242
|
+
const mark = obj['mark']
|
|
243
|
+
const range = obj['range'] as { from?: unknown; to?: unknown } | undefined
|
|
244
|
+
const apply = obj['apply']
|
|
245
|
+
const attrs = obj['attrs']
|
|
246
|
+
if (typeof mark !== 'string') return null
|
|
247
|
+
if (!range || typeof range.from !== 'number' || typeof range.to !== 'number') return null
|
|
248
|
+
if (typeof apply !== 'boolean') return null
|
|
249
|
+
return {
|
|
250
|
+
op,
|
|
251
|
+
blockIndex,
|
|
252
|
+
mark,
|
|
253
|
+
range: { from: range.from, to: range.to },
|
|
254
|
+
apply,
|
|
255
|
+
...(attrs && typeof attrs === 'object' ? { attrs: attrs as Record<string, unknown> } : {}),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
default:
|
|
259
|
+
return null
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function readSurgicalMeta(s: PendingSuggestion): SurgicalMeta | null {
|
|
264
|
+
const meta = (s.meta ?? {}) as Record<string, unknown>
|
|
265
|
+
const raw = meta['surgical']
|
|
266
|
+
if (!raw || typeof raw !== 'object') return null
|
|
267
|
+
const obj = raw as Record<string, unknown>
|
|
268
|
+
// Batch form: { ops: [SurgicalOp, ...] }
|
|
269
|
+
if (Array.isArray(obj['ops'])) {
|
|
270
|
+
const parsed: SurgicalOp[] = []
|
|
271
|
+
for (const entry of obj['ops'] as unknown[]) {
|
|
272
|
+
if (!entry || typeof entry !== 'object') continue
|
|
273
|
+
const op = parseSurgicalOp(entry as Record<string, unknown>)
|
|
274
|
+
if (op) parsed.push(op)
|
|
275
|
+
}
|
|
276
|
+
if (parsed.length === 0) return null
|
|
277
|
+
return { ops: parsed }
|
|
278
|
+
}
|
|
279
|
+
return parseSurgicalOp(obj)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function planOp(editor: Editor, op: SurgicalOp): TransactionModifier | null {
|
|
283
|
+
switch (op.op) {
|
|
284
|
+
case 'replace_block': return planReplaceBlock(editor, op.blockIndex, op.content)
|
|
285
|
+
case 'insert_block_before': return planInsertBlockBefore(editor, op.blockIndex, op.content)
|
|
286
|
+
case 'delete_block': return planDeleteBlock(editor, op.blockIndex)
|
|
287
|
+
case 'update_block_mark': return planUpdateBlockMark(editor, op.blockIndex, op.mark, op.range, op.apply, op.attrs)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Translate a surgical meta into a single TransactionModifier the diff
|
|
293
|
+
* extension can wrap with a snapshot. For batches, modifiers are
|
|
294
|
+
* computed against the original (pre-transaction) doc and then applied
|
|
295
|
+
* in DESC `blockIndex` order — each subsequent modifier touches earlier
|
|
296
|
+
* positions, so the prior modifiers' edits (at higher positions) don't
|
|
297
|
+
* shift the absolute positions the later modifiers were planned with.
|
|
298
|
+
*
|
|
299
|
+
* Returns null when the batch has no plannable ops (all out-of-range /
|
|
300
|
+
* unparseable). Drops individual non-plannable ops from a batch but
|
|
301
|
+
* still runs whatever did plan, so a single bad op doesn't kill the
|
|
302
|
+
* whole batch.
|
|
303
|
+
*/
|
|
304
|
+
function planSurgicalModifier(editor: Editor, surgical: SurgicalMeta): TransactionModifier | null {
|
|
305
|
+
if ('ops' in surgical) {
|
|
306
|
+
const sorted = [...surgical.ops].sort((a, b) => b.blockIndex - a.blockIndex)
|
|
307
|
+
const modifiers: TransactionModifier[] = []
|
|
308
|
+
for (const op of sorted) {
|
|
309
|
+
const mod = planOp(editor, op)
|
|
310
|
+
if (mod) modifiers.push(mod)
|
|
311
|
+
}
|
|
312
|
+
if (modifiers.length === 0) return null
|
|
313
|
+
return (tr) => { for (const mod of modifiers) mod(tr) }
|
|
314
|
+
}
|
|
315
|
+
return planOp(editor, surgical)
|
|
146
316
|
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surgical block-op planners for AI-driven precise edits.
|
|
3
|
+
*
|
|
4
|
+
* Each planner takes the editor + a logical block index + a payload and
|
|
5
|
+
* returns a `TransactionModifier` — a function the caller (typically
|
|
6
|
+
* `useAiInlineDiff`) feeds into
|
|
7
|
+
* `editor.commands.applySurgicalAiInlineDiff(id, modifier)`. The diff
|
|
8
|
+
* extension wraps the modifier in a snapshot-then-apply step so the
|
|
9
|
+
* inline-diff overlay renders against the precise changed range.
|
|
10
|
+
*
|
|
11
|
+
* "Block index" refers to a 0-based position across the doc's top-level
|
|
12
|
+
* children — what the AI agent sees as a numbered structural summary.
|
|
13
|
+
* Planners translate that to absolute ProseMirror positions internally.
|
|
14
|
+
*
|
|
15
|
+
* Planners return `null` when the request can't be satisfied (out-of-
|
|
16
|
+
* range index, unparseable HTML, unknown mark). Callers should treat
|
|
17
|
+
* `null` as "abort the surgical op" and surface a clear error to the
|
|
18
|
+
* agent so it can retry with a different plan.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Editor } from '@tiptap/core'
|
|
22
|
+
import type { Transaction } from '@tiptap/pm/state'
|
|
23
|
+
import type { Mark, MarkType, Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|
24
|
+
import { DOMParser as PMDOMParser } from '@tiptap/pm/model'
|
|
25
|
+
|
|
26
|
+
export type TransactionModifier = (tr: Transaction) => void
|
|
27
|
+
|
|
28
|
+
/** Resolve the start position of the top-level child at `blockIndex`. */
|
|
29
|
+
function blockStartPos(doc: ProseMirrorNode, blockIndex: number): number | null {
|
|
30
|
+
if (!Number.isInteger(blockIndex) || blockIndex < 0 || blockIndex >= doc.childCount) return null
|
|
31
|
+
let pos = 0
|
|
32
|
+
for (let i = 0; i < blockIndex; i++) pos += doc.child(i).nodeSize
|
|
33
|
+
return pos
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse content into a doc-replaceable Slice against the editor's
|
|
38
|
+
* schema. Auto-detects markdown editors by sniffing for the
|
|
39
|
+
* `tiptap-markdown` extension's `storage.markdown.parser`: if present,
|
|
40
|
+
* the editor is a `MarkdownEditor` and AI-supplied `content` is
|
|
41
|
+
* markdown source — run it through the markdown parser to produce
|
|
42
|
+
* HTML first. Otherwise content is HTML (the `RichTextField` /
|
|
43
|
+
* `TiptapEditor` path).
|
|
44
|
+
*
|
|
45
|
+
* Mirrors the same auto-detect strategy `MarkdownEditor.tsx` uses for
|
|
46
|
+
* its `parseSuggestion` whole-field callback (see `useAiInlineDiff`),
|
|
47
|
+
* so surgical ops on markdown fields stay consistent with the
|
|
48
|
+
* existing whole-field replacement path.
|
|
49
|
+
*
|
|
50
|
+
* Returns `null` when DOM isn't available (SSR — shouldn't happen
|
|
51
|
+
* here, but keeps the planner safe) or when the markdown parser
|
|
52
|
+
* throws / returns a non-string (malformed content).
|
|
53
|
+
*/
|
|
54
|
+
function parseContentToSlice(editor: Editor, content: string): ReturnType<typeof PMDOMParser.prototype.parseSlice> | null {
|
|
55
|
+
if (typeof document === 'undefined') return null
|
|
56
|
+
let html = content
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
const mdParser = (editor.storage as any)?.markdown?.parser
|
|
59
|
+
if (mdParser && typeof mdParser.parse === 'function') {
|
|
60
|
+
try {
|
|
61
|
+
const parsed: unknown = mdParser.parse(content)
|
|
62
|
+
if (typeof parsed !== 'string') return null
|
|
63
|
+
html = parsed
|
|
64
|
+
} catch { return null }
|
|
65
|
+
}
|
|
66
|
+
const container = document.createElement('div')
|
|
67
|
+
container.innerHTML = html
|
|
68
|
+
return PMDOMParser.fromSchema(editor.schema).parseSlice(container)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replace the top-level block at `blockIndex` with the parsed content.
|
|
73
|
+
* `content` is HTML for `RichTextField` (Tiptap) editors and markdown
|
|
74
|
+
* source for `MarkdownField` (markdown-extension) editors —
|
|
75
|
+
* auto-detected by `parseContentToSlice`. Multiple top-level nodes are
|
|
76
|
+
* allowed and will all land where the original block was.
|
|
77
|
+
*/
|
|
78
|
+
export function planReplaceBlock(
|
|
79
|
+
editor: Editor,
|
|
80
|
+
blockIndex: number,
|
|
81
|
+
content: string,
|
|
82
|
+
): TransactionModifier | null {
|
|
83
|
+
const doc = editor.state.doc
|
|
84
|
+
const start = blockStartPos(doc, blockIndex)
|
|
85
|
+
if (start === null) return null
|
|
86
|
+
const slice = parseContentToSlice(editor, content)
|
|
87
|
+
if (!slice) return null
|
|
88
|
+
const end = start + doc.child(blockIndex).nodeSize
|
|
89
|
+
return (tr) => { tr.replace(start, end, slice) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Insert one or more top-level nodes before the block at `blockIndex`.
|
|
94
|
+
* `content` is HTML on `RichTextField` editors and markdown source on
|
|
95
|
+
* `MarkdownField` editors — auto-detected by `parseContentToSlice`.
|
|
96
|
+
* `blockIndex === doc.childCount` appends at the end.
|
|
97
|
+
*/
|
|
98
|
+
export function planInsertBlockBefore(
|
|
99
|
+
editor: Editor,
|
|
100
|
+
blockIndex: number,
|
|
101
|
+
content: string,
|
|
102
|
+
): TransactionModifier | null {
|
|
103
|
+
const doc = editor.state.doc
|
|
104
|
+
if (!Number.isInteger(blockIndex) || blockIndex < 0 || blockIndex > doc.childCount) return null
|
|
105
|
+
const slice = parseContentToSlice(editor, content)
|
|
106
|
+
if (!slice) return null
|
|
107
|
+
let pos = 0
|
|
108
|
+
for (let i = 0; i < blockIndex; i++) pos += doc.child(i).nodeSize
|
|
109
|
+
return (tr) => { tr.replace(pos, pos, slice) }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Delete the top-level block at `blockIndex`. Doc must retain at least
|
|
114
|
+
* one child after the delete (most schemas require this) — refuses to
|
|
115
|
+
* delete the last remaining block.
|
|
116
|
+
*/
|
|
117
|
+
export function planDeleteBlock(
|
|
118
|
+
editor: Editor,
|
|
119
|
+
blockIndex: number,
|
|
120
|
+
): TransactionModifier | null {
|
|
121
|
+
const doc = editor.state.doc
|
|
122
|
+
const start = blockStartPos(doc, blockIndex)
|
|
123
|
+
if (start === null) return null
|
|
124
|
+
if (doc.childCount <= 1) return null
|
|
125
|
+
const end = start + doc.child(blockIndex).nodeSize
|
|
126
|
+
return (tr) => { tr.delete(start, end) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface BlockMarkRange {
|
|
130
|
+
/** 0-based text offset from the start of the block's content. */
|
|
131
|
+
from: number
|
|
132
|
+
/** Exclusive end offset. */
|
|
133
|
+
to: number
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Apply or remove an inline mark on a range *within* the block at
|
|
138
|
+
* `blockIndex`. `range.from` / `range.to` are text offsets relative to
|
|
139
|
+
* the start of the block's content (so `0` is the first character of
|
|
140
|
+
* the block, not the start of the doc).
|
|
141
|
+
*
|
|
142
|
+
* `apply = true` sets the mark (with optional `attrs`); `apply = false`
|
|
143
|
+
* removes it. Unknown marks (not in the editor's schema) return `null`
|
|
144
|
+
* so the caller can surface a clean error to the agent.
|
|
145
|
+
*/
|
|
146
|
+
export function planUpdateBlockMark(
|
|
147
|
+
editor: Editor,
|
|
148
|
+
blockIndex: number,
|
|
149
|
+
mark: string,
|
|
150
|
+
range: BlockMarkRange,
|
|
151
|
+
apply: boolean,
|
|
152
|
+
attrs?: Record<string, unknown>,
|
|
153
|
+
): TransactionModifier | null {
|
|
154
|
+
const doc = editor.state.doc
|
|
155
|
+
const start = blockStartPos(doc, blockIndex)
|
|
156
|
+
if (start === null) return null
|
|
157
|
+
const markType: MarkType | undefined = editor.schema.marks[mark]
|
|
158
|
+
if (!markType) return null
|
|
159
|
+
|
|
160
|
+
const block = doc.child(blockIndex)
|
|
161
|
+
const blockInner = start + 1 // step inside the block's opening token
|
|
162
|
+
const contentMax = block.content.size
|
|
163
|
+
|
|
164
|
+
if (!Number.isInteger(range.from) || !Number.isInteger(range.to)) return null
|
|
165
|
+
const clampedFrom = Math.max(0, Math.min(range.from, contentMax))
|
|
166
|
+
const clampedTo = Math.max(clampedFrom, Math.min(range.to, contentMax))
|
|
167
|
+
if (clampedTo === clampedFrom) return null
|
|
168
|
+
|
|
169
|
+
const from = blockInner + clampedFrom
|
|
170
|
+
const to = blockInner + clampedTo
|
|
171
|
+
|
|
172
|
+
if (apply) {
|
|
173
|
+
const m: Mark = markType.create(attrs ?? null)
|
|
174
|
+
return (tr) => { tr.addMark(from, to, m) }
|
|
175
|
+
}
|
|
176
|
+
return (tr) => { tr.removeMark(from, to, markType) }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Summarize a doc's top-level structure as a numbered list the AI can
|
|
181
|
+
* cite by index when proposing surgical ops. Each entry includes the
|
|
182
|
+
* block index, node type, and a truncated text preview — enough for the
|
|
183
|
+
* model to identify which block it wants to modify without sending the
|
|
184
|
+
* whole HTML/markdown back through token-priced channels.
|
|
185
|
+
*
|
|
186
|
+
* Returns one line per top-level child:
|
|
187
|
+
* `[0] heading: Welcome to the docs`
|
|
188
|
+
* `[1] paragraph: Lorem ipsum dolor sit amet…`
|
|
189
|
+
* `[2] bulletList: 3 items`
|
|
190
|
+
*/
|
|
191
|
+
export function summarizeBlockStructure(doc: ProseMirrorNode, maxChars = 80): string {
|
|
192
|
+
const lines: string[] = []
|
|
193
|
+
for (let i = 0; i < doc.childCount; i++) {
|
|
194
|
+
const node = doc.child(i)
|
|
195
|
+
const text = node.textContent.trim().replace(/\s+/g, ' ')
|
|
196
|
+
const preview = text.length === 0
|
|
197
|
+
? describeStructuralNode(node)
|
|
198
|
+
: text.length > maxChars ? `${text.slice(0, maxChars)}…` : text
|
|
199
|
+
lines.push(`[${i}] ${node.type.name}: ${preview}`)
|
|
200
|
+
}
|
|
201
|
+
return lines.join('\n')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function describeStructuralNode(node: ProseMirrorNode): string {
|
|
205
|
+
const kids = node.childCount
|
|
206
|
+
if (kids === 0) return '(empty)'
|
|
207
|
+
if (kids === 1) return `1 ${node.firstChild?.type.name ?? 'child'}`
|
|
208
|
+
return `${kids} children`
|
|
209
|
+
}
|