@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.
@@ -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,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,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,8CA+GhC,CAAA"}
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":"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"}
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
@@ -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;AAS7C,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,CAatE;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,sBAAsB,GAC9B,IAAI,CA+CN"}
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
- // Re-render on every editor transaction so the hook tracks state
43
- // changes (start / accept / reject). useEditorState would be the
44
- // idiomatic way; we read directly here to keep the dep surface tiny.
45
- const [, force] = useReducerForceUpdate();
46
- useEffect(() => {
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 suggestion.
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 wholeField = list.filter(s => !hasEditorRange(s));
70
- for (const s of wholeField) {
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
- if (typeof s.suggestedValue !== 'string')
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
- // Bail when a different diff is already showing — one at a time.
76
- // Producer should serialize calls; if not, the second suggestion
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 — when the banner / chat-sidebar pill calls
96
- // `pendingSuggestions.approve(id)` for one of our tracked suggestions,
97
- // accept the diff. Editor is the source of truth for the new doc.
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 (!startedRef.current.has(suggestion.id))
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.acceptAiInlineDiff();
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
- // useReducer + dispatch is the smallest-API force-update primitive React
115
- // ships. Hoisted into a helper so the call site stays one line.
116
- import { useReducer } from 'react';
117
- function useReducerForceUpdate() {
118
- const [n, inc] = useReducer((x) => (x + 1) | 0, 0);
119
- return [n, () => inc()];
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":"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"}
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,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAG9B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAC9E,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,GAGpB,MAAM,mBAAmB,CAAA;AAkB1B;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAqB;IAC3D,MAAM,MAAM,GAAG,cAAc,CAAC;QAC5B,MAAM;QACN,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,qBAAqB,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,IAAI;KACxF,CAAC,CAAA;IACF,OAAO,MAAM,IAAI,KAAK,CAAA;AACxB,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,MAAqB,EACrB,SAAiB,EACjB,OAA+B;IAE/B,MAAM,EAAE,IAAI,EAAE,GAAG,6BAA6B,CAAC,SAAS,CAAC,CAAA;IAEzD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAA;IAChD,SAAS,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,OAAO,GAAG,OAAO,CAAC,eAAe,CAAA,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAA;IAE1F,kEAAkE;IAClE,mEAAmE;IACnE,eAAe;IACf,MAAM,UAAU,GAAG,MAAM,CAAc,IAAI,GAAG,EAAE,CAAC,CAAA;IAEjD,8DAA8D;IAC9D,sEAAsE;IACtE,sEAAsE;IACtE,sEAAsE;IACtE,oDAAoD;IACpD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAA;QACrD,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBAAE,SAAQ;YAC1C,MAAM,UAAU,GAAG,qBAAqB,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,CAAA;YACxE,MAAM,QAAQ,GAAK,gBAAgB,CAAC,CAAC,CAAC,CAAA;YAEtC,mEAAmE;YACnE,+DAA+D;YAC/D,4DAA4D;YAC5D,8DAA8D;YAC9D,gEAAgE;YAChE,kEAAkE;YAClE,iEAAiE;YACjE,8DAA8D;YAC9D,8DAA8D;YAC9D,iEAAiE;YACjE,6DAA6D;YAC7D,2BAA2B;YAC3B,EAAE;YACF,6DAA6D;YAC7D,2DAA2D;YAC3D,oDAAoD;YACpD,IAAI,UAAU,EAAE,CAAC;gBACf,IAAI,CAAC,QAAQ;oBAAE,SAAQ;gBACvB,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;gBACvD,IAAI,CAAC,QAAQ;oBAAE,SAAQ;gBACvB,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;oBAC3C,QAAQ,CAAC,EAAE,CAAC,CAAA;oBACZ,IAAI,CAAC,EAAE,CAAC,UAAU;wBAAE,OAAO,KAAK,CAAA;oBAChC,IAAI,QAAQ;wBAAE,QAAQ,CAAC,EAAE,CAAC,CAAA;oBAC1B,OAAO,IAAI,CAAA;gBACb,CAAC,CAAC,CAAA;gBACF,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;gBAC5B,SAAQ;YACV,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;gBACvD,IAAI,CAAC,QAAQ;oBAAE,SAAQ;gBACvB,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;gBACzD,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;gBAC5B,SAAQ;YACV,CAAC;YAED,IAAI,OAAO,CAAC,CAAC,cAAc,KAAK,QAAQ;gBAAE,SAAQ;YAClD,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC,CAAA;YACxD,IAAI,CAAC,KAAK;gBAAE,SAAQ;YACpB,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAA;YAC9C,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QAC9B,CAAC;QACD,kEAAkE;QAClE,oEAAoE;QACpE,gEAAgE;QAChE,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAC/C,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAChD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACxD,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IAElB,kCAAkC;IAClC,EAAE;IACF,4DAA4D;IAC5D,uEAAuE;IACvE,qEAAqE;IACrE,uBAAuB;IACvB,sEAAsE;IACtE,iEAAiE;IACjE,oEAAoE;IACpE,oEAAoE;IACpE,oEAAoE;IACpE,kEAAkE;IAClE,8CAA8C;IAC9C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,MAAM,OAAO,GAA6B,CAAC,UAAU,EAAE,EAAE;YACvD,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC1C,MAAM,CAAC,QAAQ,CAAC,kBAAkB,EAAE,CAAA;gBACpC,OAAM;YACR,CAAC;YACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAA;YAC7C,IAAI,CAAC,QAAQ;gBAAE,OAAM;YACrB,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;YACvD,IAAI,CAAC,QAAQ;gBAAE,OAAM;YACrB,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;gBAC3C,QAAQ,CAAC,EAAE,CAAC,CAAA;gBACZ,IAAI,CAAC,EAAE,CAAC,UAAU;oBAAE,OAAO,KAAK,CAAA;gBAChC,IAAI,QAAQ;oBAAE,QAAQ,CAAC,EAAE,CAAC,CAAA;gBAC1B,OAAO,IAAI,CAAA;YACb,CAAC,CAAC,CAAA;QACJ,CAAC,CAAA;QACD,OAAO,gCAAgC,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;IACxE,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;AACzB,CAAC;AAED,SAAS,cAAc,CAAC,CAAoB;IAC1C,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAA4B,CAAA;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAiD,CAAA;IACjF,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAA;AACpF,CAAC;AAyBD,SAAS,eAAe,CAAC,GAA4B;IACnD,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;IACpB,MAAM,UAAU,GAAG,GAAG,CAAC,YAAY,CAAC,CAAA;IACpC,IAAI,OAAO,UAAU,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC/C,QAAQ,EAAE,EAAE,CAAC;QACX,KAAK,eAAe,CAAC;QACrB,KAAK,qBAAqB,CAAC,CAAC,CAAC;YAC3B,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAA;YAC9B,IAAI,OAAO,OAAO,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAA;YAC5C,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,CAAA;QACpC,CAAC;QACD,KAAK,cAAc;YACjB,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,CAAA;QAC3B,KAAK,mBAAmB,CAAC,CAAC,CAAC;YACzB,MAAM,IAAI,GAAI,GAAG,CAAC,MAAM,CAAC,CAAA;YACzB,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAiD,CAAA;YAC1E,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAA;YAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAA;YAC1B,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAA;YACzC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAA;YACzF,IAAI,OAAO,KAAK,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAA;YAC3C,OAAO;gBACL,EAAE;gBACF,UAAU;gBACV,IAAI;gBACJ,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE;gBACzC,KAAK;gBACL,GAAG,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAgC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3F,CAAA;QACH,CAAC;QACD;YACE,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,CAAoB;IAC5C,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAA4B,CAAA;IACtD,MAAM,GAAG,GAAI,IAAI,CAAC,UAAU,CAAC,CAAA;IAC7B,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAChD,MAAM,GAAG,GAAG,GAA8B,CAAA;IAC1C,yCAAyC;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAiB,EAAE,CAAA;QAC/B,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAc,EAAE,CAAC;YAC5C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,SAAQ;YACjD,MAAM,EAAE,GAAG,eAAe,CAAC,KAAgC,CAAC,CAAA;YAC5D,IAAI,EAAE;gBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACzB,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QACpC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAA;IACxB,CAAC;IACD,OAAO,eAAe,CAAC,GAAG,CAAC,CAAA;AAC7B,CAAC;AAED,SAAS,MAAM,CAAC,MAAc,EAAE,EAAc;IAC5C,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QACd,KAAK,eAAe,CAAC,CAAO,OAAO,gBAAgB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;QACtF,KAAK,qBAAqB,CAAC,CAAC,OAAO,qBAAqB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;QAC3F,KAAK,cAAc,CAAC,CAAQ,OAAO,eAAe,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC,CAAA;QACzE,KAAK,mBAAmB,CAAC,CAAG,OAAO,mBAAmB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAA;IACtH,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,oBAAoB,CAAC,MAAc,EAAE,QAAsB;IAClE,IAAI,KAAK,IAAI,QAAQ,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,CAAA;QAC5E,MAAM,SAAS,GAA0B,EAAE,CAAA;QAC3C,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;YAC9B,IAAI,GAAG;gBAAE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC9B,CAAC;QACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QACvC,OAAO,CAAC,EAAE,EAAE,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,SAAS;YAAE,GAAG,CAAC,EAAE,CAAC,CAAA,CAAC,CAAC,CAAA;IACzD,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;AACjC,CAAC"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilotiq/tiptap",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "Tiptap rich-text editor adapter for @pilotiq/pilotiq — slash menu, draggable blocks, custom-block API",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- // Re-render on every editor transaction so the hook tracks state
68
- // changes (start / accept / reject). useEditorState would be the
69
- // idiomatic way; we read directly here to keep the dep surface tiny.
70
- const [, force] = useReducerForceUpdate()
71
- useEffect(() => {
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 suggestion.
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 wholeField = list.filter(s => !hasEditorRange(s))
100
- for (const s of wholeField) {
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 — when the banner / chat-sidebar pill calls
122
- // `pendingSuggestions.approve(id)` for one of our tracked suggestions,
123
- // accept the diff. Editor is the source of truth for the new doc.
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 (!startedRef.current.has(suggestion.id)) return
128
- editor.commands.acceptAiInlineDiff()
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
- // useReducer + dispatch is the smallest-API force-update primitive React
141
- // ships. Hoisted into a helper so the call site stays one line.
142
- import { useReducer } from 'react'
143
- function useReducerForceUpdate(): [number, () => void] {
144
- const [n, inc] = useReducer((x: number) => (x + 1) | 0, 0)
145
- return [n, () => inc()]
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
+ }