@atlaskit/editor-plugin-collab-edit 13.0.30 → 13.1.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.
@@ -0,0 +1,115 @@
1
+ import { Decoration } from '@atlaskit/editor-prosemirror/view';
2
+
3
+ // Kept out of `plugin-state.ts` so all the agent-shimmer plumbing lives together and is easy to
4
+ // remove if the approach changes. `plugin-state` just reduces the active ranges and asks this module
5
+ // to build the decorations.
6
+
7
+ /** Default time the skeleton shimmer stays on the agent-authored content (ms). */
8
+ export const AGENT_SHIMMER_DEFAULT_DURATION_MS = 3000;
9
+
10
+ // Skeleton-loader bar over the agent-authored range, plus a Rovo AI telepointer at the end.
11
+ export const AGENT_SHIMMER_CLASS = 'collab-agent-shimmer';
12
+ export const ROVO_AGENT_TELEPOINTER_CLASS = 'ai-in-editor-telepointer';
13
+ export const ROVO_AGENT_TELEPOINTER_LABEL_CLASS = 'ai-in-editor-telepointer-label';
14
+ export const ADD_AGENT_SHIMMER_META = 'addAgentShimmer'; // register the shimmer decorations
15
+ export const REMOVE_AGENT_SHIMMER_META = 'removeAgentShimmer'; // remove them once the shimmer ends
16
+
17
+ // A range an agent step wrote; the skeleton + telepointer decorations are drawn over `from`..`to`
18
+ // and kept until removal (so positions can be re-mapped). Pure data only. `telepointerLabel` is the
19
+ // label for the trailing agent telepointer (the agent's type); when absent, no telepointer is shown.
20
+
21
+ // Rovo AI in-editor telepointer/cursor shown at the end of an agent-authored range (same DOM/style
22
+ // pattern as editor-plugin-ai's in-editor direct-streaming telepointer).
23
+ const createRovoAgentTelepointer = label => {
24
+ const element = document.createElement('span');
25
+ element.setAttribute('data-testid', 'ai-in-editor-telepointer-widget');
26
+ element.className = ROVO_AGENT_TELEPOINTER_CLASS;
27
+ const labelElement = document.createElement('span');
28
+ labelElement.setAttribute('data-testid', 'ai-in-editor-telepointer-widget-label');
29
+ labelElement.className = ROVO_AGENT_TELEPOINTER_LABEL_CLASS;
30
+ labelElement.append(label);
31
+ element.appendChild(labelElement);
32
+ return element;
33
+ };
34
+
35
+ /**
36
+ * Pure reducer for the active shimmer ranges from a transaction's changes. Maps existing ranges
37
+ * forward as the doc changes, replaces them wholesale when a new agent batch lands (a new batch
38
+ * supersedes any still-in-flight shimmer), and drops a range when its removal timer fires. Returns a
39
+ * fresh array (never mutates in place) plus whether anything changed.
40
+ */
41
+ export const reduceAgentShimmers = (current, tr, added, removedShimmerId) => {
42
+ let next = current;
43
+ let changed = false;
44
+
45
+ // Ranges added in THIS transaction are already in post-change coords, so map the pre-existing
46
+ // ones BEFORE replacing with any new batch.
47
+ if (tr.docChanged && next.length) {
48
+ next = next.map(shimmer => ({
49
+ ...shimmer,
50
+ from: tr.mapping.map(shimmer.from, -1),
51
+ to: tr.mapping.map(shimmer.to, 1)
52
+ }));
53
+ changed = true;
54
+ }
55
+ if (added !== null && added !== void 0 && added.length) {
56
+ next = added.map(shimmer => ({
57
+ ...shimmer
58
+ }));
59
+ changed = true;
60
+ }
61
+ if (removedShimmerId) {
62
+ next = next.filter(shimmer => shimmer.shimmerId !== removedShimmerId);
63
+ changed = true;
64
+ }
65
+ return {
66
+ changed,
67
+ next
68
+ };
69
+ };
70
+
71
+ /**
72
+ * Builds the inline skeleton-bar + trailing telepointer decorations for the active shimmer ranges.
73
+ * `getValidPos` clamps a raw position to a valid decoration position (owned by `plugin-state`).
74
+ * One bad range is isolated via `onError` so it can't kill the others.
75
+ */
76
+ export const buildAgentShimmerDecorations = (tr, shimmers, getValidPos, onError) => {
77
+ const decorations = [];
78
+ const docEnd = tr.doc.nodeSize - 2;
79
+ shimmers.forEach(({
80
+ shimmerId,
81
+ from,
82
+ to,
83
+ telepointerLabel
84
+ }) => {
85
+ try {
86
+ const validFrom = getValidPos(tr, Math.max(from, 1));
87
+ const validTo = getValidPos(tr, Math.min(to, docEnd));
88
+ if (validTo <= validFrom) {
89
+ return;
90
+ }
91
+ // Skeleton-loader bar over the whole agent-authored range...
92
+ decorations.push(Decoration.inline(validFrom, validTo, {
93
+ class: AGENT_SHIMMER_CLASS
94
+ }, {
95
+ isAgentShimmer: true,
96
+ shimmerId
97
+ }));
98
+ // ...and, when enabled, a Rovo AI telepointer/cursor (labelled with the agent's type) at the
99
+ // end of the range.
100
+ if (telepointerLabel) {
101
+ decorations.push(Decoration.widget(validTo, createRovoAgentTelepointer(telepointerLabel), {
102
+ isAgentShimmer: true,
103
+ shimmerId,
104
+ class: ROVO_AGENT_TELEPOINTER_CLASS,
105
+ key: `agent-telepointer-${shimmerId}`,
106
+ side: 1
107
+ }));
108
+ }
109
+ } catch (err) {
110
+ // One bad range must not kill the others.
111
+ onError(err);
112
+ }
113
+ });
114
+ return decorations;
115
+ };
@@ -0,0 +1,151 @@
1
+ import { getCollabState } from '@atlaskit/prosemirror-collab';
2
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
3
+ // When an agent step lands we cover the top-level block(s) it wrote with a skeleton-loader shimmer
4
+ // (plus a Rovo agent telepointer at the end of the range), then remove it on a timer to reveal the
5
+ // content. Gated behind the `platform_editor_agent_be_streaming` experiment; `durationMs` sets how
6
+ // long the shimmer stays and a 0 duration disables it.
7
+ let agentShimmerIdCounter = 0;
8
+
9
+ // A step is position-neutral when its StepMap changes no range's length — i.e. it shifts no
10
+ // positions. Attribute-only steps (e.g. `localId` assignment) produce an empty StepMap, and
11
+ // same-size replacements preserve lengths, so both are position-neutral. Used to decide whether a
12
+ // rebase over local unconfirmed steps could have invalidated our index-based range math.
13
+ export const isPositionNeutralStep = step => {
14
+ let neutral = true;
15
+ step.getMap().forEach((oldStart, oldEnd, newStart, newEnd) => {
16
+ if (oldEnd - oldStart !== newEnd - newStart) {
17
+ neutral = false;
18
+ }
19
+ });
20
+ return neutral;
21
+ };
22
+
23
+ /**
24
+ * Derive the shimmer ranges for the agent-authored steps in a received batch, in final-doc
25
+ * coordinates. `agentType` present ⇒ agent-authored (per the NCS↔Editor steps contract). Each
26
+ * emitted range is expanded to the whole top-level block(s) the agent touched, which the plugin
27
+ * covers with the skeleton shimmer. Ranges with no new content (pure deletions) are dropped.
28
+ *
29
+ * Steps whose new content is in the same or directly-adjacent top-level block are coalesced into one
30
+ * range, so an edit that arrives as several steps in a region shimmers as a single unit. Edits
31
+ * separated by an untouched block stay independent.
32
+ *
33
+ * Correctness: the range math assumes `tr` is a linear 1:1 apply of `steps`. Under the native collab
34
+ * plugin, `receiveTransaction` rebases incoming steps over unconfirmed local steps when they exist;
35
+ * that only invalidates our positions if a local step shifted positions, so we skip solely when a
36
+ * rebased local step changed sizes (a rare, safe degrade). Any unexpected error also degrades to no
37
+ * shimmer, so this never throws into the shared remote-step handler.
38
+ */
39
+ export const getAgentShimmerRanges = (json, steps, tr, view, durationMs, telepointerEnabled) => {
40
+ var _json$find;
41
+ if (!expValEquals('platform_editor_agent_be_streaming', 'isEnabled', true)) {
42
+ return [];
43
+ }
44
+ // Nothing to reveal if the shimmer is disabled.
45
+ if (durationMs <= 0) {
46
+ return [];
47
+ }
48
+ // Telepointer label = the agent's type upper-cased (e.g. `mcp` → "MCP"), falling back to a generic
49
+ // "Agent"; `undefined` when the telepointer is disabled, so the plugin skips it. `agentType` is the
50
+ // same on every step of an agent batch, so read it from the first agent-authored step.
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ const agentType = (_json$find = json.find(step => typeof (step === null || step === void 0 ? void 0 : step.agentType) === 'string')) === null || _json$find === void 0 ? void 0 : _json$find.agentType;
53
+ const telepointerLabel = telepointerEnabled ? (agentType === null || agentType === void 0 ? void 0 : agentType.toUpperCase()) || 'Agent' : undefined;
54
+ // When the batch was rebased over local unconfirmed steps, our index-based range math is only
55
+ // valid if those local steps shifted no positions. Attribute-only steps (e.g. `localId`) and
56
+ // same-size replacements are position-neutral, so the shimmer stays correct. Skip only when a
57
+ // local step actually changed sizes (a genuine concurrent content edit).
58
+ if (Number(tr.getMeta('rebased')) > 0) {
59
+ var _getCollabState$uncon, _getCollabState;
60
+ const unconfirmed = (_getCollabState$uncon = (_getCollabState = getCollabState(view.state)) === null || _getCollabState === void 0 ? void 0 : _getCollabState.unconfirmed) !== null && _getCollabState$uncon !== void 0 ? _getCollabState$uncon : [];
61
+ if (unconfirmed.some(entry => !isPositionNeutralStep(entry.step))) {
62
+ return [];
63
+ }
64
+ }
65
+ try {
66
+ // Map an inserted range in doc_{i+1} forward through the remaining steps to final-doc coords.
67
+ // (A later step's own inserted content starts at its `from`; only subsequent steps shift it.)
68
+ const mapToFinalDoc = (pos, stepIndex, bias) => {
69
+ let p = pos;
70
+ for (let j = stepIndex + 1; j < steps.length; j++) {
71
+ p = steps[j].getMap().map(p, bias);
72
+ }
73
+ return p;
74
+ };
75
+
76
+ // Derive each agent step's changed ranges from its StepMap — the canonical, step-type-agnostic
77
+ // source of what a step wrote. We only need the NEW extent (the content now in the document), in
78
+ // final-doc coords, since the highlight decorates content that is already present.
79
+
80
+ const infos = [];
81
+ json.forEach((rawStep, index) => {
82
+ if (typeof (rawStep === null || rawStep === void 0 ? void 0 : rawStep.agentType) !== 'string') {
83
+ return;
84
+ }
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ const pmStep = steps[index];
87
+ if (typeof (pmStep === null || pmStep === void 0 ? void 0 : pmStep.getMap) !== 'function') {
88
+ return;
89
+ }
90
+ pmStep.getMap().forEach((_oldStart, _oldEnd, newStart, newEnd) => {
91
+ if (newEnd <= newStart) {
92
+ return; // pure deletion / attribute-only — no new content to highlight
93
+ }
94
+ infos.push({
95
+ from: mapToFinalDoc(newStart, index, -1),
96
+ to: mapToFinalDoc(newEnd, index, 1)
97
+ });
98
+ });
99
+ });
100
+ if (!infos.length) {
101
+ return [];
102
+ }
103
+
104
+ // Group the changed ranges by the TOP-LEVEL block they landed in, then highlight each touched
105
+ // block IN FULL. NCS emits a single agent edit as many small replace fragments and often keeps a
106
+ // common prefix/suffix untouched, so the changed sub-ranges cover only part of a block (e.g. half
107
+ // a rewritten heading). Expanding to the whole block's content makes the entire heading/paragraph
108
+ // shimmer as one unit rather than leaving the unchanged half undecorated. Directly-adjacent
109
+ // touched blocks (index n and n+1) merge into one group so a multi-block rewrite reveals together;
110
+ // a genuinely untouched block in between (index gap > 1) splits the run, so far-apart edits stay
111
+ // independent.
112
+ const docSize = tr.doc.content.size;
113
+ const clampPos = pos => Math.min(Math.max(pos, 1), docSize);
114
+ const blockIndexAt = pos => tr.doc.resolve(clampPos(pos)).index(0);
115
+ // Start/end of the content of the top-level block containing `pos`, so the whole block is covered.
116
+ const blockContentStart = pos => {
117
+ const $pos = tr.doc.resolve(clampPos(pos));
118
+ return $pos.depth >= 1 ? $pos.start(1) : pos;
119
+ };
120
+ const blockContentEnd = pos => {
121
+ const $pos = tr.doc.resolve(clampPos(pos));
122
+ return $pos.depth >= 1 ? $pos.end(1) : pos;
123
+ };
124
+ const sorted = [...infos].sort((a, b) => a.from - b.from || a.to - b.to);
125
+ const groups = [];
126
+ sorted.forEach(info => {
127
+ const block = blockIndexAt(info.from);
128
+ const current = groups[groups.length - 1];
129
+ if (current && block <= current.maxBlock + 1) {
130
+ current.to = Math.max(current.to, info.to);
131
+ current.maxBlock = Math.max(current.maxBlock, block);
132
+ } else {
133
+ groups.push({
134
+ from: info.from,
135
+ to: info.to,
136
+ maxBlock: block
137
+ });
138
+ }
139
+ });
140
+ return groups.map(group => ({
141
+ shimmerId: `agent-shimmer-${agentShimmerIdCounter++}`,
142
+ from: blockContentStart(group.from),
143
+ to: blockContentEnd(group.to),
144
+ telepointerLabel
145
+ }));
146
+ } catch {
147
+ // Never let shimmer range derivation throw into the shared remote-step handler; degrade to no
148
+ // shimmer.
149
+ return [];
150
+ }
151
+ };
@@ -5,6 +5,8 @@ import { Selection } from '@atlaskit/editor-prosemirror/state';
5
5
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
6
6
  import { Participants } from '../participants';
7
7
  import { createTelepointers, findPointers, getPositionOfTelepointer, isReplaceStep, hasExistingNudge } from '../utils';
8
+ import { ADD_AGENT_SHIMMER_META, buildAgentShimmerDecorations, reduceAgentShimmers, REMOVE_AGENT_SHIMMER_META } from './agent-shimmer-decorations';
9
+
8
10
  /**
9
11
  * Returns position where it's possible to place a decoration.
10
12
  */
@@ -29,7 +31,9 @@ export class PluginState {
29
31
  get sessionId() {
30
32
  return this.sid;
31
33
  }
32
- constructor(decorations, participants, sessionId, collabInitalised = false, onError, nudgeAnimations = new Map()) {
34
+ constructor(decorations, participants, sessionId, collabInitalised = false, onError, nudgeAnimations = new Map(), agentShimmers = []) {
35
+ // Active agent-edit shimmer ranges (pure data). Replaced with a fresh array of fresh
36
+ // objects whenever it changes — never mutated in place.
33
37
  // eslint-disable-next-line no-console
34
38
  _defineProperty(this, "onError", error => console.error(error));
35
39
  this.decorationSet = decorations;
@@ -38,6 +42,7 @@ export class PluginState {
38
42
  this.isReady = collabInitalised;
39
43
  this.onError = onError || this.onError;
40
44
  this.nudgeAnimations = nudgeAnimations;
45
+ this.agentShimmers = agentShimmers;
41
46
  }
42
47
  getFullName(sessionId) {
43
48
  const participant = this.participants.get(sessionId);
@@ -62,6 +67,8 @@ export class PluginState {
62
67
  const presenceData = tr.getMeta('presence');
63
68
  const telepointerData = tr.getMeta('telepointer');
64
69
  const nudgeTelepointerData = tr.getMeta('nudgeTelepointer');
70
+ const agentShimmerData = tr.getMeta(ADD_AGENT_SHIMMER_META);
71
+ const removeAgentShimmerId = tr.getMeta(REMOVE_AGENT_SHIMMER_META);
65
72
  const sessionIdData = tr.getMeta('sessionId');
66
73
  let collabInitialised = tr.getMeta('collabInitialised');
67
74
  if (typeof collabInitialised !== 'boolean') {
@@ -159,6 +166,11 @@ export class PluginState {
159
166
  // Ignored via go/ees005
160
167
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
168
  this.decorationSet.find().forEach(deco => {
169
+ var _deco$spec;
170
+ // Never let telepointer dim/side logic touch agent-shimmer decorations.
171
+ if ((_deco$spec = deco.spec) !== null && _deco$spec !== void 0 && _deco$spec.isAgentShimmer) {
172
+ return;
173
+ }
162
174
  if (deco.type.toDOM) {
163
175
  const hasTelepointerDimClass = deco.type.toDOM.classList.contains(TELEPOINTER_DIM_CLASS);
164
176
  const browser = getBrowserInfo();
@@ -187,8 +199,12 @@ export class PluginState {
187
199
  // Ignored via go/ees005
188
200
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
201
  this.decorationSet.find().forEach(deco => {
190
- var _deco$spec, _deco$spec$pointer, _deco$spec2;
191
- if (deco.type.toDOM && participants.get(nudgeSessionId) && ((_deco$spec = deco.spec) === null || _deco$spec === void 0 ? void 0 : (_deco$spec$pointer = _deco$spec.pointer) === null || _deco$spec$pointer === void 0 ? void 0 : _deco$spec$pointer.sessionId) === nudgeSessionId && ((_deco$spec2 = deco.spec) === null || _deco$spec2 === void 0 ? void 0 : _deco$spec2.key) === `telepointer-${nudgeSessionId}`) {
202
+ var _deco$spec2, _deco$spec3, _deco$spec3$pointer, _deco$spec4;
203
+ // Never let telepointer nudge logic touch agent-shimmer decorations.
204
+ if ((_deco$spec2 = deco.spec) !== null && _deco$spec2 !== void 0 && _deco$spec2.isAgentShimmer) {
205
+ return;
206
+ }
207
+ if (deco.type.toDOM && participants.get(nudgeSessionId) && ((_deco$spec3 = deco.spec) === null || _deco$spec3 === void 0 ? void 0 : (_deco$spec3$pointer = _deco$spec3.pointer) === null || _deco$spec3$pointer === void 0 ? void 0 : _deco$spec3$pointer.sessionId) === nudgeSessionId && ((_deco$spec4 = deco.spec) === null || _deco$spec4 === void 0 ? void 0 : _deco$spec4.key) === `telepointer-${nudgeSessionId}`) {
192
208
  // Restart animation by removing and re-adding the class
193
209
  deco.type.toDOM.classList.remove(TELEPOINTER_PULSE_DURING_TR_CLASS);
194
210
  deco.type.toDOM.classList.remove(TELEPOINTER_PULSE_CLASS);
@@ -198,6 +214,38 @@ export class PluginState {
198
214
  }
199
215
  });
200
216
  }
217
+
218
+ // Register / keep-aligned / remove the agent-edit purple highlight
219
+ // decorations. Fully fault-isolated — it works on local copies and only merges into the shared
220
+ // `add`/`remove` (and commits `this.agentShimmers`) after the whole block succeeds, so a fault
221
+ // here can never corrupt telepointer decorations or the shared decoration set. Never mutates
222
+ // shimmer entries in place: each change produces a fresh array of fresh objects.
223
+ try {
224
+ const {
225
+ changed,
226
+ next
227
+ } = reduceAgentShimmers(this.agentShimmers, tr, agentShimmerData, removeAgentShimmerId);
228
+ if (changed) {
229
+ // Build into local arrays; only merge into the shared add/remove once the block succeeds.
230
+ const agentRemove = this.decorationSet.find(undefined, undefined, spec => spec.isAgentShimmer);
231
+ const agentAdd = buildAgentShimmerDecorations(tr, next, getValidPos, this.onError);
232
+
233
+ // Commit only after the whole block succeeded.
234
+ remove = remove.concat(agentRemove);
235
+ add = add.concat(agentAdd);
236
+ this.agentShimmers = next;
237
+ }
238
+ } catch (err) {
239
+ this.onError(err);
240
+ // Degrade to no shimmer without corrupting telepointer decorations: drop all agent
241
+ // decorations and clear agent state, leaving the telepointer `add`/`remove` untouched.
242
+ this.agentShimmers = [];
243
+ try {
244
+ remove = remove.concat(this.decorationSet.find(undefined, undefined, spec => spec.isAgentShimmer));
245
+ } catch {
246
+ // Teardown must not re-throw.
247
+ }
248
+ }
201
249
  if (remove.length) {
202
250
  this.decorationSet = this.decorationSet.remove(remove);
203
251
  }
@@ -218,7 +266,7 @@ export class PluginState {
218
266
  }
219
267
  }
220
268
  }
221
- const nextState = new PluginState(this.decorationSet, participants, sid, collabInitialised, this.onError, this.nudgeAnimations);
269
+ const nextState = new PluginState(this.decorationSet, participants, sid, collabInitialised, this.onError, this.nudgeAnimations, this.agentShimmers);
222
270
  return PluginState.eq(nextState, this) ? this : nextState;
223
271
  }
224
272
  static eq(a, b) {
@@ -8,7 +8,13 @@ import { AttrStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
8
8
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
9
9
  import { getParticipantColor } from '@atlaskit/editor-shared-styles';
10
10
  import { preserveNodeIdentity } from './preserve-node-identity';
11
- export const findPointers = (id, decorations) => decorations.find().reduce((arr, deco) => deco.spec.pointer.presenceId === id ? arr.concat(deco) : arr, []);
11
+ export const findPointers = (id, decorations) => decorations.find().reduce(
12
+ // `pointer` is absent on non-telepointer decorations (e.g. the agent-shimmer
13
+ // sweep decorations), so guard against it to avoid crashing this shared helper.
14
+ (arr, deco) => {
15
+ var _deco$spec$pointer;
16
+ return ((_deco$spec$pointer = deco.spec.pointer) === null || _deco$spec$pointer === void 0 ? void 0 : _deco$spec$pointer.presenceId) === id ? arr.concat(deco) : arr;
17
+ }, []);
12
18
  function style(options) {
13
19
  const color = options && options.color || "var(--ds-border, #0B120E24)";
14
20
  const borderWidth = "var(--ds-border-width-focused, 2px)";
@@ -7,6 +7,10 @@ import * as allAtlaskitCustomSteps from '@atlaskit/custom-steps';
7
7
  import { AllSelection, NodeSelection } from '@atlaskit/editor-prosemirror/state';
8
8
  import { Step } from '@atlaskit/editor-prosemirror/transform';
9
9
  import { receiveTransaction } from '@atlaskit/prosemirror-collab';
10
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
11
+ import { expVal } from '@atlaskit/tmp-editor-statsig/expVal';
12
+ import { ADD_AGENT_SHIMMER_META, AGENT_SHIMMER_DEFAULT_DURATION_MS, REMOVE_AGENT_SHIMMER_META } from './main/agent-shimmer-decorations';
13
+ import { getAgentShimmerRanges } from './main/agent-shimmer-ranges';
10
14
  import { replaceDocument } from './utils';
11
15
 
12
16
  /*
@@ -70,6 +74,22 @@ export var applyRemoteSteps = function applyRemoteSteps(json, view, userIds, opt
70
74
  tr.setMeta('addToHistory', false);
71
75
  tr.setMeta('isRemote', true);
72
76
 
77
+ // Agent edit shimmer: mark the ranges agent steps just wrote so the plugin reveals them with the
78
+ // gloss-sweep → purple-highlight sequence. Gated as a whole so no experiment reads run
79
+ // off-experiment; off-path leaves `agentShimmers` empty and everything below is a no-op.
80
+ var durationMs = 0;
81
+ var agentShimmers = [];
82
+ if (expValEquals('platform_editor_agent_be_streaming', 'isEnabled', true)) {
83
+ durationMs = expVal('platform_editor_agent_be_streaming', 'durationMs', AGENT_SHIMMER_DEFAULT_DURATION_MS);
84
+ // Telepointer shown by default; `telepointerDisabled` hides it (inverted because `expVal`
85
+ // only permits `false` as a boolean default).
86
+ var telepointerEnabled = !expVal('platform_editor_agent_be_streaming', 'telepointerDisabled', false);
87
+ agentShimmers = getAgentShimmerRanges(json, steps, tr, view, durationMs, telepointerEnabled);
88
+ if (agentShimmers.length) {
89
+ tr.setMeta(ADD_AGENT_SHIMMER_META, agentShimmers);
90
+ }
91
+ }
92
+
73
93
  /*
74
94
  * Persist marks across transactions. Fixes an issue where
75
95
  * marks are lost if remote transactions are dispatched
@@ -79,6 +99,25 @@ export var applyRemoteSteps = function applyRemoteSteps(json, view, userIds, opt
79
99
  tr.setStoredMarks(state.tr.storedMarks);
80
100
  }
81
101
  view.dispatch(tr);
102
+
103
+ // Remove each shimmer after `durationMs`. `dispatchMeta` is guarded so a timer firing after
104
+ // teardown is a harmless no-op. Only reached when the gated block above produced ranges, so this
105
+ // whole path is off-experiment-safe.
106
+ if (agentShimmers.length) {
107
+ var dispatchMeta = function dispatchMeta(metaKey, value) {
108
+ try {
109
+ view.dispatch(view.state.tr.setMeta(metaKey, value));
110
+ } catch (_unused) {
111
+ // View torn down before the timer fired — nothing to clean up.
112
+ }
113
+ };
114
+ agentShimmers.forEach(function (_ref) {
115
+ var shimmerId = _ref.shimmerId;
116
+ setTimeout(function () {
117
+ dispatchMeta(REMOVE_AGENT_SHIMMER_META, shimmerId);
118
+ }, durationMs);
119
+ });
120
+ }
82
121
  }
83
122
  };
84
123
  export var handleTelePointer = function handleTelePointer(telepointerData, view) {
@@ -0,0 +1,120 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ import { Decoration } from '@atlaskit/editor-prosemirror/view';
5
+
6
+ // Kept out of `plugin-state.ts` so all the agent-shimmer plumbing lives together and is easy to
7
+ // remove if the approach changes. `plugin-state` just reduces the active ranges and asks this module
8
+ // to build the decorations.
9
+
10
+ /** Default time the skeleton shimmer stays on the agent-authored content (ms). */
11
+ export var AGENT_SHIMMER_DEFAULT_DURATION_MS = 3000;
12
+
13
+ // Skeleton-loader bar over the agent-authored range, plus a Rovo AI telepointer at the end.
14
+ export var AGENT_SHIMMER_CLASS = 'collab-agent-shimmer';
15
+ export var ROVO_AGENT_TELEPOINTER_CLASS = 'ai-in-editor-telepointer';
16
+ export var ROVO_AGENT_TELEPOINTER_LABEL_CLASS = 'ai-in-editor-telepointer-label';
17
+ export var ADD_AGENT_SHIMMER_META = 'addAgentShimmer'; // register the shimmer decorations
18
+ export var REMOVE_AGENT_SHIMMER_META = 'removeAgentShimmer'; // remove them once the shimmer ends
19
+
20
+ // A range an agent step wrote; the skeleton + telepointer decorations are drawn over `from`..`to`
21
+ // and kept until removal (so positions can be re-mapped). Pure data only. `telepointerLabel` is the
22
+ // label for the trailing agent telepointer (the agent's type); when absent, no telepointer is shown.
23
+
24
+ // Rovo AI in-editor telepointer/cursor shown at the end of an agent-authored range (same DOM/style
25
+ // pattern as editor-plugin-ai's in-editor direct-streaming telepointer).
26
+ var createRovoAgentTelepointer = function createRovoAgentTelepointer(label) {
27
+ var element = document.createElement('span');
28
+ element.setAttribute('data-testid', 'ai-in-editor-telepointer-widget');
29
+ element.className = ROVO_AGENT_TELEPOINTER_CLASS;
30
+ var labelElement = document.createElement('span');
31
+ labelElement.setAttribute('data-testid', 'ai-in-editor-telepointer-widget-label');
32
+ labelElement.className = ROVO_AGENT_TELEPOINTER_LABEL_CLASS;
33
+ labelElement.append(label);
34
+ element.appendChild(labelElement);
35
+ return element;
36
+ };
37
+
38
+ /**
39
+ * Pure reducer for the active shimmer ranges from a transaction's changes. Maps existing ranges
40
+ * forward as the doc changes, replaces them wholesale when a new agent batch lands (a new batch
41
+ * supersedes any still-in-flight shimmer), and drops a range when its removal timer fires. Returns a
42
+ * fresh array (never mutates in place) plus whether anything changed.
43
+ */
44
+ export var reduceAgentShimmers = function reduceAgentShimmers(current, tr, added, removedShimmerId) {
45
+ var next = current;
46
+ var changed = false;
47
+
48
+ // Ranges added in THIS transaction are already in post-change coords, so map the pre-existing
49
+ // ones BEFORE replacing with any new batch.
50
+ if (tr.docChanged && next.length) {
51
+ next = next.map(function (shimmer) {
52
+ return _objectSpread(_objectSpread({}, shimmer), {}, {
53
+ from: tr.mapping.map(shimmer.from, -1),
54
+ to: tr.mapping.map(shimmer.to, 1)
55
+ });
56
+ });
57
+ changed = true;
58
+ }
59
+ if (added !== null && added !== void 0 && added.length) {
60
+ next = added.map(function (shimmer) {
61
+ return _objectSpread({}, shimmer);
62
+ });
63
+ changed = true;
64
+ }
65
+ if (removedShimmerId) {
66
+ next = next.filter(function (shimmer) {
67
+ return shimmer.shimmerId !== removedShimmerId;
68
+ });
69
+ changed = true;
70
+ }
71
+ return {
72
+ changed: changed,
73
+ next: next
74
+ };
75
+ };
76
+
77
+ /**
78
+ * Builds the inline skeleton-bar + trailing telepointer decorations for the active shimmer ranges.
79
+ * `getValidPos` clamps a raw position to a valid decoration position (owned by `plugin-state`).
80
+ * One bad range is isolated via `onError` so it can't kill the others.
81
+ */
82
+ export var buildAgentShimmerDecorations = function buildAgentShimmerDecorations(tr, shimmers, getValidPos, onError) {
83
+ var decorations = [];
84
+ var docEnd = tr.doc.nodeSize - 2;
85
+ shimmers.forEach(function (_ref) {
86
+ var shimmerId = _ref.shimmerId,
87
+ from = _ref.from,
88
+ to = _ref.to,
89
+ telepointerLabel = _ref.telepointerLabel;
90
+ try {
91
+ var validFrom = getValidPos(tr, Math.max(from, 1));
92
+ var validTo = getValidPos(tr, Math.min(to, docEnd));
93
+ if (validTo <= validFrom) {
94
+ return;
95
+ }
96
+ // Skeleton-loader bar over the whole agent-authored range...
97
+ decorations.push(Decoration.inline(validFrom, validTo, {
98
+ class: AGENT_SHIMMER_CLASS
99
+ }, {
100
+ isAgentShimmer: true,
101
+ shimmerId: shimmerId
102
+ }));
103
+ // ...and, when enabled, a Rovo AI telepointer/cursor (labelled with the agent's type) at the
104
+ // end of the range.
105
+ if (telepointerLabel) {
106
+ decorations.push(Decoration.widget(validTo, createRovoAgentTelepointer(telepointerLabel), {
107
+ isAgentShimmer: true,
108
+ shimmerId: shimmerId,
109
+ class: ROVO_AGENT_TELEPOINTER_CLASS,
110
+ key: "agent-telepointer-".concat(shimmerId),
111
+ side: 1
112
+ }));
113
+ }
114
+ } catch (err) {
115
+ // One bad range must not kill the others.
116
+ onError(err);
117
+ }
118
+ });
119
+ return decorations;
120
+ };