@atlaskit/editor-plugin-collab-edit 13.3.1 → 13.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @atlaskit/editor-plugin-collab-edit
2
2
 
3
+ ## 13.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 13.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`95e5e100318c4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/95e5e100318c4) -
14
+ Review moment (Post Stream Review) for BE streaming — record remote agent edits into
15
+ aiContentPositions and open/append the Review moment (hybrid completion signal + idle debounce).
16
+ - New `@atlaskit/editor-common/collab/agent-remote-edit-review` subpath export for the neutral
17
+ agent-edit review contract (AGENT_REMOTE_EDIT_REVIEW_DATA, AgentRemoteEditReviewSegment,
18
+ AgentRemoteEditReviewData).
19
+ - Fix chimera delete diffs in agent-review-segments: per-step backward StepMap mapping replaces
20
+ the previous group-then-invert approach that produced blended original text when multiple
21
+ discrete operations (delete + insert) interacted in the same region.
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies
26
+
3
27
  ## 13.3.1
4
28
 
5
29
  ### Patch Changes
@@ -7,12 +7,14 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.registerAllCustomSteps = exports.handleTelePointer = exports.handlePresence = exports.handleInit = exports.handleConnection = exports.getSendableSelection = exports.applyRemoteSteps = exports.applyRemoteData = void 0;
8
8
  var allAdfSchemaSteps = _interopRequireWildcard(require("@atlaskit/adf-schema/steps"));
9
9
  var allAtlaskitCustomSteps = _interopRequireWildcard(require("@atlaskit/custom-steps"));
10
+ var _collabAgentRemoteEditReview = require("@atlaskit/editor-common/collab-agent-remote-edit-review");
10
11
  var _state = require("@atlaskit/editor-prosemirror/state");
11
12
  var _transform = require("@atlaskit/editor-prosemirror/transform");
12
13
  var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
13
14
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
14
15
  var _expVal = require("@atlaskit/tmp-editor-statsig/expVal");
15
16
  var _analytics = require("./analytics");
17
+ var _agentReviewSegments = require("./main/agent-review-segments");
16
18
  var _agentShimmerDecorations = require("./main/agent-shimmer-decorations");
17
19
  var _agentShimmerRanges = require("./main/agent-shimmer-ranges");
18
20
  var _utils = require("./utils");
@@ -112,6 +114,21 @@ var applyRemoteSteps = exports.applyRemoteSteps = function applyRemoteSteps(json
112
114
  }
113
115
  }
114
116
 
117
+ // [CCI-17994] Post Stream Review ("Review moment") for BE streaming. Record what
118
+ // the agent just wrote (original + new slice per contiguous change) onto this same
119
+ // transaction. `editor-plugin-ai` consumes this neutral meta to populate
120
+ // `aiContentPositions` and — only on the requester's client — open the Review
121
+ // moment once the edit settles. Gated on `platform_editor_agent_be_streaming`
122
+ // alongside shimmer; `getAgentEditSegments` also self-gates by returning null
123
+ // unless the batch contains agent-authored steps (and never throws), so this is
124
+ // a no-op for ordinary collaborator edits and can't affect step application.
125
+ if ((0, _expValEquals.expValEquals)('platform_editor_agent_be_streaming', 'isEnabled', true)) {
126
+ var reviewData = (0, _agentReviewSegments.getAgentEditSegments)(json, steps, tr, view);
127
+ if (reviewData) {
128
+ tr.setMeta(_collabAgentRemoteEditReview.AGENT_REMOTE_EDIT_REVIEW_DATA, reviewData);
129
+ }
130
+ }
131
+
115
132
  /*
116
133
  * Persist marks across transactions. Fixes an issue where
117
134
  * marks are lost if remote transactions are dispatched
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getAgentEditSegments = void 0;
7
+ var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
8
+ var _agentShimmerRanges = require("./agent-shimmer-ranges");
9
+ // [CCI-17994] Post Stream Review ("Review moment") recording for BE streaming.
10
+ //
11
+ // Sibling to `getAgentShimmerRanges`: same idea of deriving what the agent wrote
12
+ // from a received remote-step batch, but for a DIFFERENT consumer. The shimmer
13
+ // only needs the NEW extent of added content (to draw a skeleton), so it drops
14
+ // pure deletions and expands to whole blocks. The Review moment instead needs, per
15
+ // contiguous change, BOTH the original slice (pre-edit, for undo / "compare with
16
+ // original") and the new slice (for redo), AND it must keep deletions (a removed
17
+ // paragraph is a reviewable change). So this derives the actual changed extents
18
+ // and reconstructs the pre-edit slice from `tr.before`.
19
+ //
20
+ // The output is a neutral `AgentRemoteEditReviewData` (contract in editor-common);
21
+ // `editor-plugin-ai`'s session-context plugin turns each segment into a coarse
22
+ // `aiContentPositions` entry, reusing the entire FE Review moment pipeline.
23
+
24
+ var clampToDoc = function clampToDoc(doc, pos) {
25
+ return Math.min(Math.max(pos, 0), doc.content.size);
26
+ };
27
+
28
+ // Map a position in doc_{i+1} forward through the remaining steps to final-doc
29
+ // coords. Mirrors the shimmer's `mapToFinalDoc` — a later step's own inserted
30
+ // content starts at its `from`; only subsequent steps shift it.
31
+ var mapToFinalDoc = function mapToFinalDoc(steps, pos, stepIndex, bias) {
32
+ var p = pos;
33
+ for (var j = stepIndex + 1; j < steps.length; j++) {
34
+ p = steps[j].getMap().map(p, bias);
35
+ }
36
+ return p;
37
+ };
38
+
39
+ /**
40
+ * Derive per-change Review moment segments from an agent-authored remote-step
41
+ * batch. Returns `null` when there is nothing to record (no agent steps, or a
42
+ * rebase invalidated our index math, or derivation threw) so the caller can no-op
43
+ * safely — this must never throw into the shared remote-step handler.
44
+ *
45
+ * @param json the raw received step JSON (carries `agentType` / `agentId` / `userId`)
46
+ * @param steps the parsed PM steps (index-aligned with `json`)
47
+ * @param tr the transaction that applied `steps` (so `tr.before`/`tr.doc`/`tr.mapping` are available)
48
+ * @param view the editor view (for the collab rebase guard)
49
+ */
50
+ var getAgentEditSegments = exports.getAgentEditSegments = function getAgentEditSegments(json, steps, tr, view) {
51
+ // `agentType` present ⇒ agent-authored (per the NCS↔Editor steps contract). Read
52
+ // from the first agent step; it is identical across a batch.
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ var agentStep = json.find(function (step) {
55
+ return typeof (step === null || step === void 0 ? void 0 : step.agentType) === 'string';
56
+ });
57
+ var agentType = agentStep === null || agentStep === void 0 ? void 0 : agentStep.agentType;
58
+ if (agentType === undefined) {
59
+ return null;
60
+ }
61
+ // The user the agent acted on behalf of (the requester). Optional/additive on the
62
+ // step contract, so it may be absent.
63
+ var actorUserId = typeof (agentStep === null || agentStep === void 0 ? void 0 : agentStep.userId) === 'string' ? agentStep.userId : undefined;
64
+ // Which agent instance wrote the batch. Additive on the step contract like
65
+ // `userId`, so treat it as optional. Carried for contract clarity / future
66
+ // per-agent attribution; nothing consumes it yet.
67
+ var agentId = typeof (agentStep === null || agentStep === void 0 ? void 0 : agentStep.agentId) === 'string' ? agentStep.agentId : undefined;
68
+ // Hybrid end-of-edit seam: if the BE/NCS flags a batch as the terminal one, carry
69
+ // it so the AI plugin can open review immediately instead of waiting for the
70
+ // debounce. Additive marker; absent for streaming (non-final) batches.
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ var complete = json.some(function (step) {
73
+ return (step === null || step === void 0 ? void 0 : step.agentEditComplete) === true;
74
+ });
75
+
76
+ // Same rebase guard as the shimmer: index-based range math is only valid if any
77
+ // rebased-over local steps shifted no positions. Degrade to no recording otherwise.
78
+ if (Number(tr.getMeta('rebased')) > 0) {
79
+ var _getCollabState$uncon, _getCollabState;
80
+ var unconfirmed = (_getCollabState$uncon = (_getCollabState = (0, _prosemirrorCollab.getCollabState)(view.state)) === null || _getCollabState === void 0 ? void 0 : _getCollabState.unconfirmed) !== null && _getCollabState$uncon !== void 0 ? _getCollabState$uncon : [];
81
+ if (unconfirmed.some(function (entry) {
82
+ return !(0, _agentShimmerRanges.isPositionNeutralStep)(entry.step);
83
+ })) {
84
+ return null;
85
+ }
86
+ }
87
+ try {
88
+ // Compute one segment per step-level StepMap range. Each agent step's changed
89
+ // extent is taken from its StepMap (the canonical, step-type-agnostic source).
90
+ // Unlike the shimmer we KEEP zero-width new extents — a pure deletion has
91
+ // `newEnd === newStart` but is a reviewable `remove`.
92
+ //
93
+ // We derive the originalSlice/newSlice PER RANGE (per step) rather than
94
+ // grouping ranges first and then inverting over a merged range. Grouping
95
+ // before slicing corrupts the invert-mapping: when multiple discrete
96
+ // operations (e.g. two deleteNode + one insertNodeAfter) are merged into one
97
+ // range, mapping that merged range back through the inverted mapping produces
98
+ // a chimera — a blend of old and new text that never existed in the document.
99
+ //
100
+ // The downstream PSR pipeline (mergeOverlappingSegments +
101
+ // calculateTopLevelNodeSegments) already handles overlapping/adjacent coarse
102
+ // entries correctly, so grouping here is unnecessary.
103
+
104
+ var segments = [];
105
+ json.forEach(function (rawStep, index) {
106
+ if (typeof (rawStep === null || rawStep === void 0 ? void 0 : rawStep.agentType) !== 'string') {
107
+ return;
108
+ }
109
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
+ var pmStep = steps[index];
111
+ if (typeof (pmStep === null || pmStep === void 0 ? void 0 : pmStep.getMap) !== 'function') {
112
+ return;
113
+ }
114
+ pmStep.getMap().forEach(function (oldStart, oldEnd, newStart, newEnd) {
115
+ try {
116
+ // NEW-doc coordinates: map this step's newStart/newEnd forward
117
+ // through all subsequent steps to get final-doc positions.
118
+ var finalFrom = mapToFinalDoc(steps, newStart, index, -1);
119
+ var finalTo = mapToFinalDoc(steps, newEnd, index, 1);
120
+ var newFrom = clampToDoc(tr.doc, Math.min(finalFrom, finalTo));
121
+ var newTo = clampToDoc(tr.doc, Math.max(finalFrom, finalTo));
122
+
123
+ // ORIGINAL-doc coordinates: map this step's oldStart/oldEnd
124
+ // BACKWARD through all preceding steps to get pre-batch
125
+ // (`tr.before`) positions. Each preceding step's StepMap tells
126
+ // us how positions shifted; we invert that shift by mapping
127
+ // through the inverted StepMap.
128
+ //
129
+ // This is the key difference from using `tr.mapping.invert()`
130
+ // which inverts ALL steps at once — that produces chimera text
131
+ // when multiple discrete operations (delete + delete + insert)
132
+ // interact in the same region. Per-step backward mapping keeps
133
+ // each operation's original range isolated.
134
+ var origStart = oldStart;
135
+ var origEnd = oldEnd;
136
+ for (var j = index - 1; j >= 0; j--) {
137
+ var prevMap = steps[j].getMap().invert();
138
+ origStart = prevMap.map(origStart, -1);
139
+ origEnd = prevMap.map(origEnd, 1);
140
+ }
141
+ var origFrom = clampToDoc(tr.before, Math.min(origStart, origEnd));
142
+ var origTo = clampToDoc(tr.before, Math.max(origStart, origEnd));
143
+ var originalSlice = tr.before.slice(origFrom, origTo);
144
+ var newSlice = tr.doc.slice(newFrom, newTo);
145
+ var originalEmpty = originalSlice.content.size === 0;
146
+ var newEmpty = newSlice.content.size === 0;
147
+ // A truly empty-to-empty region is not a change — skip it.
148
+ if (originalEmpty && newEmpty) {
149
+ return;
150
+ }
151
+ var kind = originalEmpty ? 'add' : newEmpty ? 'remove' : 'update';
152
+ segments.push({
153
+ startPos: newFrom,
154
+ endPos: newTo,
155
+ originalSlice: originalSlice,
156
+ newSlice: newSlice,
157
+ kind: kind
158
+ });
159
+ } catch (_unused) {
160
+ // One bad range must not drop the others.
161
+ }
162
+ });
163
+ });
164
+ if (!segments.length) {
165
+ return null;
166
+ }
167
+ return {
168
+ actorUserId: actorUserId,
169
+ agentId: agentId,
170
+ agentType: agentType,
171
+ complete: complete,
172
+ segments: segments
173
+ };
174
+ } catch (_unused2) {
175
+ // Never throw into the shared remote-step handler; degrade to no recording.
176
+ return null;
177
+ }
178
+ };
@@ -4,12 +4,14 @@ import * as allAdfSchemaSteps from '@atlaskit/adf-schema/steps';
4
4
  // Ignored via go/ees005
5
5
  // eslint-disable-next-line import/no-namespace
6
6
  import * as allAtlaskitCustomSteps from '@atlaskit/custom-steps';
7
+ import { AGENT_REMOTE_EDIT_REVIEW_DATA } from '@atlaskit/editor-common/collab-agent-remote-edit-review';
7
8
  import { AllSelection, NodeSelection } from '@atlaskit/editor-prosemirror/state';
8
9
  import { Step } from '@atlaskit/editor-prosemirror/transform';
9
10
  import { receiveTransaction } from '@atlaskit/prosemirror-collab';
10
11
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
11
12
  import { expVal } from '@atlaskit/tmp-editor-statsig/expVal';
12
13
  import { getAgentEditShimmerNotShownPayload } from './analytics';
14
+ import { getAgentEditSegments } from './main/agent-review-segments';
13
15
  import { ADD_AGENT_SHIMMER_META, AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS, AGENT_SHIMMER_DEFAULT_DURATION_MS, HIGHLIGHT_AGENT_SHIMMER_META, REMOVE_AGENT_SHIMMER_META } from './main/agent-shimmer-decorations';
14
16
  import { getAgentShimmerRanges } from './main/agent-shimmer-ranges';
15
17
  import { replaceDocument } from './utils';
@@ -114,6 +116,21 @@ export const applyRemoteSteps = (json, view, userIds, options, editorAnalyticsAp
114
116
  }
115
117
  }
116
118
 
119
+ // [CCI-17994] Post Stream Review ("Review moment") for BE streaming. Record what
120
+ // the agent just wrote (original + new slice per contiguous change) onto this same
121
+ // transaction. `editor-plugin-ai` consumes this neutral meta to populate
122
+ // `aiContentPositions` and — only on the requester's client — open the Review
123
+ // moment once the edit settles. Gated on `platform_editor_agent_be_streaming`
124
+ // alongside shimmer; `getAgentEditSegments` also self-gates by returning null
125
+ // unless the batch contains agent-authored steps (and never throws), so this is
126
+ // a no-op for ordinary collaborator edits and can't affect step application.
127
+ if (expValEquals('platform_editor_agent_be_streaming', 'isEnabled', true)) {
128
+ const reviewData = getAgentEditSegments(json, steps, tr, view);
129
+ if (reviewData) {
130
+ tr.setMeta(AGENT_REMOTE_EDIT_REVIEW_DATA, reviewData);
131
+ }
132
+ }
133
+
117
134
  /*
118
135
  * Persist marks across transactions. Fixes an issue where
119
136
  * marks are lost if remote transactions are dispatched
@@ -0,0 +1,165 @@
1
+ import { getCollabState } from '@atlaskit/prosemirror-collab';
2
+ import { isPositionNeutralStep } from './agent-shimmer-ranges';
3
+
4
+ // [CCI-17994] Post Stream Review ("Review moment") recording for BE streaming.
5
+ //
6
+ // Sibling to `getAgentShimmerRanges`: same idea of deriving what the agent wrote
7
+ // from a received remote-step batch, but for a DIFFERENT consumer. The shimmer
8
+ // only needs the NEW extent of added content (to draw a skeleton), so it drops
9
+ // pure deletions and expands to whole blocks. The Review moment instead needs, per
10
+ // contiguous change, BOTH the original slice (pre-edit, for undo / "compare with
11
+ // original") and the new slice (for redo), AND it must keep deletions (a removed
12
+ // paragraph is a reviewable change). So this derives the actual changed extents
13
+ // and reconstructs the pre-edit slice from `tr.before`.
14
+ //
15
+ // The output is a neutral `AgentRemoteEditReviewData` (contract in editor-common);
16
+ // `editor-plugin-ai`'s session-context plugin turns each segment into a coarse
17
+ // `aiContentPositions` entry, reusing the entire FE Review moment pipeline.
18
+
19
+ const clampToDoc = (doc, pos) => Math.min(Math.max(pos, 0), doc.content.size);
20
+
21
+ // Map a position in doc_{i+1} forward through the remaining steps to final-doc
22
+ // coords. Mirrors the shimmer's `mapToFinalDoc` — a later step's own inserted
23
+ // content starts at its `from`; only subsequent steps shift it.
24
+ const mapToFinalDoc = (steps, pos, stepIndex, bias) => {
25
+ let p = pos;
26
+ for (let j = stepIndex + 1; j < steps.length; j++) {
27
+ p = steps[j].getMap().map(p, bias);
28
+ }
29
+ return p;
30
+ };
31
+
32
+ /**
33
+ * Derive per-change Review moment segments from an agent-authored remote-step
34
+ * batch. Returns `null` when there is nothing to record (no agent steps, or a
35
+ * rebase invalidated our index math, or derivation threw) so the caller can no-op
36
+ * safely — this must never throw into the shared remote-step handler.
37
+ *
38
+ * @param json the raw received step JSON (carries `agentType` / `agentId` / `userId`)
39
+ * @param steps the parsed PM steps (index-aligned with `json`)
40
+ * @param tr the transaction that applied `steps` (so `tr.before`/`tr.doc`/`tr.mapping` are available)
41
+ * @param view the editor view (for the collab rebase guard)
42
+ */
43
+ export const getAgentEditSegments = (json, steps, tr, view) => {
44
+ // `agentType` present ⇒ agent-authored (per the NCS↔Editor steps contract). Read
45
+ // from the first agent step; it is identical across a batch.
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ const agentStep = json.find(step => typeof (step === null || step === void 0 ? void 0 : step.agentType) === 'string');
48
+ const agentType = agentStep === null || agentStep === void 0 ? void 0 : agentStep.agentType;
49
+ if (agentType === undefined) {
50
+ return null;
51
+ }
52
+ // The user the agent acted on behalf of (the requester). Optional/additive on the
53
+ // step contract, so it may be absent.
54
+ const actorUserId = typeof (agentStep === null || agentStep === void 0 ? void 0 : agentStep.userId) === 'string' ? agentStep.userId : undefined;
55
+ // Which agent instance wrote the batch. Additive on the step contract like
56
+ // `userId`, so treat it as optional. Carried for contract clarity / future
57
+ // per-agent attribution; nothing consumes it yet.
58
+ const agentId = typeof (agentStep === null || agentStep === void 0 ? void 0 : agentStep.agentId) === 'string' ? agentStep.agentId : undefined;
59
+ // Hybrid end-of-edit seam: if the BE/NCS flags a batch as the terminal one, carry
60
+ // it so the AI plugin can open review immediately instead of waiting for the
61
+ // debounce. Additive marker; absent for streaming (non-final) batches.
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ const complete = json.some(step => (step === null || step === void 0 ? void 0 : step.agentEditComplete) === true);
64
+
65
+ // Same rebase guard as the shimmer: index-based range math is only valid if any
66
+ // rebased-over local steps shifted no positions. Degrade to no recording otherwise.
67
+ if (Number(tr.getMeta('rebased')) > 0) {
68
+ var _getCollabState$uncon, _getCollabState;
69
+ const unconfirmed = (_getCollabState$uncon = (_getCollabState = getCollabState(view.state)) === null || _getCollabState === void 0 ? void 0 : _getCollabState.unconfirmed) !== null && _getCollabState$uncon !== void 0 ? _getCollabState$uncon : [];
70
+ if (unconfirmed.some(entry => !isPositionNeutralStep(entry.step))) {
71
+ return null;
72
+ }
73
+ }
74
+ try {
75
+ // Compute one segment per step-level StepMap range. Each agent step's changed
76
+ // extent is taken from its StepMap (the canonical, step-type-agnostic source).
77
+ // Unlike the shimmer we KEEP zero-width new extents — a pure deletion has
78
+ // `newEnd === newStart` but is a reviewable `remove`.
79
+ //
80
+ // We derive the originalSlice/newSlice PER RANGE (per step) rather than
81
+ // grouping ranges first and then inverting over a merged range. Grouping
82
+ // before slicing corrupts the invert-mapping: when multiple discrete
83
+ // operations (e.g. two deleteNode + one insertNodeAfter) are merged into one
84
+ // range, mapping that merged range back through the inverted mapping produces
85
+ // a chimera — a blend of old and new text that never existed in the document.
86
+ //
87
+ // The downstream PSR pipeline (mergeOverlappingSegments +
88
+ // calculateTopLevelNodeSegments) already handles overlapping/adjacent coarse
89
+ // entries correctly, so grouping here is unnecessary.
90
+
91
+ const segments = [];
92
+ json.forEach((rawStep, index) => {
93
+ if (typeof (rawStep === null || rawStep === void 0 ? void 0 : rawStep.agentType) !== 'string') {
94
+ return;
95
+ }
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ const pmStep = steps[index];
98
+ if (typeof (pmStep === null || pmStep === void 0 ? void 0 : pmStep.getMap) !== 'function') {
99
+ return;
100
+ }
101
+ pmStep.getMap().forEach((oldStart, oldEnd, newStart, newEnd) => {
102
+ try {
103
+ // NEW-doc coordinates: map this step's newStart/newEnd forward
104
+ // through all subsequent steps to get final-doc positions.
105
+ const finalFrom = mapToFinalDoc(steps, newStart, index, -1);
106
+ const finalTo = mapToFinalDoc(steps, newEnd, index, 1);
107
+ const newFrom = clampToDoc(tr.doc, Math.min(finalFrom, finalTo));
108
+ const newTo = clampToDoc(tr.doc, Math.max(finalFrom, finalTo));
109
+
110
+ // ORIGINAL-doc coordinates: map this step's oldStart/oldEnd
111
+ // BACKWARD through all preceding steps to get pre-batch
112
+ // (`tr.before`) positions. Each preceding step's StepMap tells
113
+ // us how positions shifted; we invert that shift by mapping
114
+ // through the inverted StepMap.
115
+ //
116
+ // This is the key difference from using `tr.mapping.invert()`
117
+ // which inverts ALL steps at once — that produces chimera text
118
+ // when multiple discrete operations (delete + delete + insert)
119
+ // interact in the same region. Per-step backward mapping keeps
120
+ // each operation's original range isolated.
121
+ let origStart = oldStart;
122
+ let origEnd = oldEnd;
123
+ for (let j = index - 1; j >= 0; j--) {
124
+ const prevMap = steps[j].getMap().invert();
125
+ origStart = prevMap.map(origStart, -1);
126
+ origEnd = prevMap.map(origEnd, 1);
127
+ }
128
+ const origFrom = clampToDoc(tr.before, Math.min(origStart, origEnd));
129
+ const origTo = clampToDoc(tr.before, Math.max(origStart, origEnd));
130
+ const originalSlice = tr.before.slice(origFrom, origTo);
131
+ const newSlice = tr.doc.slice(newFrom, newTo);
132
+ const originalEmpty = originalSlice.content.size === 0;
133
+ const newEmpty = newSlice.content.size === 0;
134
+ // A truly empty-to-empty region is not a change — skip it.
135
+ if (originalEmpty && newEmpty) {
136
+ return;
137
+ }
138
+ const kind = originalEmpty ? 'add' : newEmpty ? 'remove' : 'update';
139
+ segments.push({
140
+ startPos: newFrom,
141
+ endPos: newTo,
142
+ originalSlice,
143
+ newSlice,
144
+ kind
145
+ });
146
+ } catch {
147
+ // One bad range must not drop the others.
148
+ }
149
+ });
150
+ });
151
+ if (!segments.length) {
152
+ return null;
153
+ }
154
+ return {
155
+ actorUserId,
156
+ agentId,
157
+ agentType,
158
+ complete,
159
+ segments
160
+ };
161
+ } catch {
162
+ // Never throw into the shared remote-step handler; degrade to no recording.
163
+ return null;
164
+ }
165
+ };
@@ -4,12 +4,14 @@ import * as allAdfSchemaSteps from '@atlaskit/adf-schema/steps';
4
4
  // Ignored via go/ees005
5
5
  // eslint-disable-next-line import/no-namespace
6
6
  import * as allAtlaskitCustomSteps from '@atlaskit/custom-steps';
7
+ import { AGENT_REMOTE_EDIT_REVIEW_DATA } from '@atlaskit/editor-common/collab-agent-remote-edit-review';
7
8
  import { AllSelection, NodeSelection } from '@atlaskit/editor-prosemirror/state';
8
9
  import { Step } from '@atlaskit/editor-prosemirror/transform';
9
10
  import { receiveTransaction } from '@atlaskit/prosemirror-collab';
10
11
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
11
12
  import { expVal } from '@atlaskit/tmp-editor-statsig/expVal';
12
13
  import { getAgentEditShimmerNotShownPayload } from './analytics';
14
+ import { getAgentEditSegments } from './main/agent-review-segments';
13
15
  import { ADD_AGENT_SHIMMER_META, AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS, AGENT_SHIMMER_DEFAULT_DURATION_MS, HIGHLIGHT_AGENT_SHIMMER_META, REMOVE_AGENT_SHIMMER_META } from './main/agent-shimmer-decorations';
14
16
  import { getAgentShimmerRanges } from './main/agent-shimmer-ranges';
15
17
  import { replaceDocument } from './utils';
@@ -103,6 +105,21 @@ export var applyRemoteSteps = function applyRemoteSteps(json, view, userIds, opt
103
105
  }
104
106
  }
105
107
 
108
+ // [CCI-17994] Post Stream Review ("Review moment") for BE streaming. Record what
109
+ // the agent just wrote (original + new slice per contiguous change) onto this same
110
+ // transaction. `editor-plugin-ai` consumes this neutral meta to populate
111
+ // `aiContentPositions` and — only on the requester's client — open the Review
112
+ // moment once the edit settles. Gated on `platform_editor_agent_be_streaming`
113
+ // alongside shimmer; `getAgentEditSegments` also self-gates by returning null
114
+ // unless the batch contains agent-authored steps (and never throws), so this is
115
+ // a no-op for ordinary collaborator edits and can't affect step application.
116
+ if (expValEquals('platform_editor_agent_be_streaming', 'isEnabled', true)) {
117
+ var reviewData = getAgentEditSegments(json, steps, tr, view);
118
+ if (reviewData) {
119
+ tr.setMeta(AGENT_REMOTE_EDIT_REVIEW_DATA, reviewData);
120
+ }
121
+ }
122
+
106
123
  /*
107
124
  * Persist marks across transactions. Fixes an issue where
108
125
  * marks are lost if remote transactions are dispatched
@@ -0,0 +1,173 @@
1
+ import { getCollabState } from '@atlaskit/prosemirror-collab';
2
+ import { isPositionNeutralStep } from './agent-shimmer-ranges';
3
+
4
+ // [CCI-17994] Post Stream Review ("Review moment") recording for BE streaming.
5
+ //
6
+ // Sibling to `getAgentShimmerRanges`: same idea of deriving what the agent wrote
7
+ // from a received remote-step batch, but for a DIFFERENT consumer. The shimmer
8
+ // only needs the NEW extent of added content (to draw a skeleton), so it drops
9
+ // pure deletions and expands to whole blocks. The Review moment instead needs, per
10
+ // contiguous change, BOTH the original slice (pre-edit, for undo / "compare with
11
+ // original") and the new slice (for redo), AND it must keep deletions (a removed
12
+ // paragraph is a reviewable change). So this derives the actual changed extents
13
+ // and reconstructs the pre-edit slice from `tr.before`.
14
+ //
15
+ // The output is a neutral `AgentRemoteEditReviewData` (contract in editor-common);
16
+ // `editor-plugin-ai`'s session-context plugin turns each segment into a coarse
17
+ // `aiContentPositions` entry, reusing the entire FE Review moment pipeline.
18
+
19
+ var clampToDoc = function clampToDoc(doc, pos) {
20
+ return Math.min(Math.max(pos, 0), doc.content.size);
21
+ };
22
+
23
+ // Map a position in doc_{i+1} forward through the remaining steps to final-doc
24
+ // coords. Mirrors the shimmer's `mapToFinalDoc` — a later step's own inserted
25
+ // content starts at its `from`; only subsequent steps shift it.
26
+ var mapToFinalDoc = function mapToFinalDoc(steps, pos, stepIndex, bias) {
27
+ var p = pos;
28
+ for (var j = stepIndex + 1; j < steps.length; j++) {
29
+ p = steps[j].getMap().map(p, bias);
30
+ }
31
+ return p;
32
+ };
33
+
34
+ /**
35
+ * Derive per-change Review moment segments from an agent-authored remote-step
36
+ * batch. Returns `null` when there is nothing to record (no agent steps, or a
37
+ * rebase invalidated our index math, or derivation threw) so the caller can no-op
38
+ * safely — this must never throw into the shared remote-step handler.
39
+ *
40
+ * @param json the raw received step JSON (carries `agentType` / `agentId` / `userId`)
41
+ * @param steps the parsed PM steps (index-aligned with `json`)
42
+ * @param tr the transaction that applied `steps` (so `tr.before`/`tr.doc`/`tr.mapping` are available)
43
+ * @param view the editor view (for the collab rebase guard)
44
+ */
45
+ export var getAgentEditSegments = function getAgentEditSegments(json, steps, tr, view) {
46
+ // `agentType` present ⇒ agent-authored (per the NCS↔Editor steps contract). Read
47
+ // from the first agent step; it is identical across a batch.
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ var agentStep = json.find(function (step) {
50
+ return typeof (step === null || step === void 0 ? void 0 : step.agentType) === 'string';
51
+ });
52
+ var agentType = agentStep === null || agentStep === void 0 ? void 0 : agentStep.agentType;
53
+ if (agentType === undefined) {
54
+ return null;
55
+ }
56
+ // The user the agent acted on behalf of (the requester). Optional/additive on the
57
+ // step contract, so it may be absent.
58
+ var actorUserId = typeof (agentStep === null || agentStep === void 0 ? void 0 : agentStep.userId) === 'string' ? agentStep.userId : undefined;
59
+ // Which agent instance wrote the batch. Additive on the step contract like
60
+ // `userId`, so treat it as optional. Carried for contract clarity / future
61
+ // per-agent attribution; nothing consumes it yet.
62
+ var agentId = typeof (agentStep === null || agentStep === void 0 ? void 0 : agentStep.agentId) === 'string' ? agentStep.agentId : undefined;
63
+ // Hybrid end-of-edit seam: if the BE/NCS flags a batch as the terminal one, carry
64
+ // it so the AI plugin can open review immediately instead of waiting for the
65
+ // debounce. Additive marker; absent for streaming (non-final) batches.
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ var complete = json.some(function (step) {
68
+ return (step === null || step === void 0 ? void 0 : step.agentEditComplete) === true;
69
+ });
70
+
71
+ // Same rebase guard as the shimmer: index-based range math is only valid if any
72
+ // rebased-over local steps shifted no positions. Degrade to no recording otherwise.
73
+ if (Number(tr.getMeta('rebased')) > 0) {
74
+ var _getCollabState$uncon, _getCollabState;
75
+ var unconfirmed = (_getCollabState$uncon = (_getCollabState = getCollabState(view.state)) === null || _getCollabState === void 0 ? void 0 : _getCollabState.unconfirmed) !== null && _getCollabState$uncon !== void 0 ? _getCollabState$uncon : [];
76
+ if (unconfirmed.some(function (entry) {
77
+ return !isPositionNeutralStep(entry.step);
78
+ })) {
79
+ return null;
80
+ }
81
+ }
82
+ try {
83
+ // Compute one segment per step-level StepMap range. Each agent step's changed
84
+ // extent is taken from its StepMap (the canonical, step-type-agnostic source).
85
+ // Unlike the shimmer we KEEP zero-width new extents — a pure deletion has
86
+ // `newEnd === newStart` but is a reviewable `remove`.
87
+ //
88
+ // We derive the originalSlice/newSlice PER RANGE (per step) rather than
89
+ // grouping ranges first and then inverting over a merged range. Grouping
90
+ // before slicing corrupts the invert-mapping: when multiple discrete
91
+ // operations (e.g. two deleteNode + one insertNodeAfter) are merged into one
92
+ // range, mapping that merged range back through the inverted mapping produces
93
+ // a chimera — a blend of old and new text that never existed in the document.
94
+ //
95
+ // The downstream PSR pipeline (mergeOverlappingSegments +
96
+ // calculateTopLevelNodeSegments) already handles overlapping/adjacent coarse
97
+ // entries correctly, so grouping here is unnecessary.
98
+
99
+ var segments = [];
100
+ json.forEach(function (rawStep, index) {
101
+ if (typeof (rawStep === null || rawStep === void 0 ? void 0 : rawStep.agentType) !== 'string') {
102
+ return;
103
+ }
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ var pmStep = steps[index];
106
+ if (typeof (pmStep === null || pmStep === void 0 ? void 0 : pmStep.getMap) !== 'function') {
107
+ return;
108
+ }
109
+ pmStep.getMap().forEach(function (oldStart, oldEnd, newStart, newEnd) {
110
+ try {
111
+ // NEW-doc coordinates: map this step's newStart/newEnd forward
112
+ // through all subsequent steps to get final-doc positions.
113
+ var finalFrom = mapToFinalDoc(steps, newStart, index, -1);
114
+ var finalTo = mapToFinalDoc(steps, newEnd, index, 1);
115
+ var newFrom = clampToDoc(tr.doc, Math.min(finalFrom, finalTo));
116
+ var newTo = clampToDoc(tr.doc, Math.max(finalFrom, finalTo));
117
+
118
+ // ORIGINAL-doc coordinates: map this step's oldStart/oldEnd
119
+ // BACKWARD through all preceding steps to get pre-batch
120
+ // (`tr.before`) positions. Each preceding step's StepMap tells
121
+ // us how positions shifted; we invert that shift by mapping
122
+ // through the inverted StepMap.
123
+ //
124
+ // This is the key difference from using `tr.mapping.invert()`
125
+ // which inverts ALL steps at once — that produces chimera text
126
+ // when multiple discrete operations (delete + delete + insert)
127
+ // interact in the same region. Per-step backward mapping keeps
128
+ // each operation's original range isolated.
129
+ var origStart = oldStart;
130
+ var origEnd = oldEnd;
131
+ for (var j = index - 1; j >= 0; j--) {
132
+ var prevMap = steps[j].getMap().invert();
133
+ origStart = prevMap.map(origStart, -1);
134
+ origEnd = prevMap.map(origEnd, 1);
135
+ }
136
+ var origFrom = clampToDoc(tr.before, Math.min(origStart, origEnd));
137
+ var origTo = clampToDoc(tr.before, Math.max(origStart, origEnd));
138
+ var originalSlice = tr.before.slice(origFrom, origTo);
139
+ var newSlice = tr.doc.slice(newFrom, newTo);
140
+ var originalEmpty = originalSlice.content.size === 0;
141
+ var newEmpty = newSlice.content.size === 0;
142
+ // A truly empty-to-empty region is not a change — skip it.
143
+ if (originalEmpty && newEmpty) {
144
+ return;
145
+ }
146
+ var kind = originalEmpty ? 'add' : newEmpty ? 'remove' : 'update';
147
+ segments.push({
148
+ startPos: newFrom,
149
+ endPos: newTo,
150
+ originalSlice: originalSlice,
151
+ newSlice: newSlice,
152
+ kind: kind
153
+ });
154
+ } catch (_unused) {
155
+ // One bad range must not drop the others.
156
+ }
157
+ });
158
+ });
159
+ if (!segments.length) {
160
+ return null;
161
+ }
162
+ return {
163
+ actorUserId: actorUserId,
164
+ agentId: agentId,
165
+ agentType: agentType,
166
+ complete: complete,
167
+ segments: segments
168
+ };
169
+ } catch (_unused2) {
170
+ // Never throw into the shared remote-step handler; degrade to no recording.
171
+ return null;
172
+ }
173
+ };
@@ -0,0 +1,16 @@
1
+ import type { AgentRemoteEditReviewData } from '@atlaskit/editor-common/collab-agent-remote-edit-review';
2
+ import type { Transaction } from '@atlaskit/editor-prosemirror/state';
3
+ import type { Step } from '@atlaskit/editor-prosemirror/transform';
4
+ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
5
+ /**
6
+ * Derive per-change Review moment segments from an agent-authored remote-step
7
+ * batch. Returns `null` when there is nothing to record (no agent steps, or a
8
+ * rebase invalidated our index math, or derivation threw) so the caller can no-op
9
+ * safely — this must never throw into the shared remote-step handler.
10
+ *
11
+ * @param json the raw received step JSON (carries `agentType` / `agentId` / `userId`)
12
+ * @param steps the parsed PM steps (index-aligned with `json`)
13
+ * @param tr the transaction that applied `steps` (so `tr.before`/`tr.doc`/`tr.mapping` are available)
14
+ * @param view the editor view (for the collab rebase guard)
15
+ */
16
+ export declare const getAgentEditSegments: (json: any[], steps: Step[], tr: Transaction, view: EditorView) => AgentRemoteEditReviewData | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-collab-edit",
3
- "version": "13.3.1",
3
+ "version": "13.4.1",
4
4
  "description": "Collab Edit plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -19,7 +19,7 @@
19
19
  "sideEffects": false,
20
20
  "atlaskit:src": "src/index.ts",
21
21
  "dependencies": {
22
- "@atlaskit/adf-schema": "^56.4.0",
22
+ "@atlaskit/adf-schema": "^56.5.0",
23
23
  "@atlaskit/custom-steps": "^1.0.0",
24
24
  "@atlaskit/editor-json-transformer": "^9.2.0",
25
25
  "@atlaskit/editor-plugin-analytics": "^12.1.0",
@@ -31,13 +31,13 @@
31
31
  "@atlaskit/frontend-utilities": "^4.1.0",
32
32
  "@atlaskit/platform-feature-flags": "^2.1.0",
33
33
  "@atlaskit/prosemirror-collab": "^1.0.0",
34
- "@atlaskit/tmp-editor-statsig": "^136.0.0",
34
+ "@atlaskit/tmp-editor-statsig": "^137.0.0",
35
35
  "@atlaskit/tokens": "^16.3.0",
36
36
  "@babel/runtime": "^7.0.0",
37
37
  "memoize-one": "^6.0.0"
38
38
  },
39
39
  "peerDependencies": {
40
- "@atlaskit/editor-common": "^116.48.0",
40
+ "@atlaskit/editor-common": "^116.51.0",
41
41
  "react": "^18.2.0 || ^19.2.0",
42
42
  "react-dom": "^18.2.0 || ^19.2.0"
43
43
  },