@atlaskit/editor-plugin-collab-edit 13.1.3 → 13.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # @atlaskit/editor-plugin-collab-edit
2
2
 
3
+ ## 13.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`51c33ef5349b6`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/51c33ef5349b6) -
8
+ Enable compatibility with React 19.2.0
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies
13
+
14
+ ## 13.2.0
15
+
16
+ ### Minor Changes
17
+
18
+ - [`782360c1111dc`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/782360c1111dc) -
19
+ Agent-edit shimmer review follow-ups (behind the default-OFF `platform_editor_agent_be_streaming`
20
+ experiment):
21
+ - Add a second shimmer phase: after the skeleton loader clears, the changed range shows a purple
22
+ "just edited" highlight (same colours as the editor AI "improve writing" in-editor highlight)
23
+ that eases in and out over its lifetime. The Rovo telepointer stays through both phases.
24
+ - The two phases are sized by independent experiment params: `shimmerDurationMs` (skeleton,
25
+ renamed from `durationMs`) and `highlightDurationMs` (highlight). `0` on either skips just that
26
+ phase, `0` on both shows nothing.
27
+ - Gate the agent-edit shimmer styles behind the experiment in both the emotion and compiled
28
+ `EditorContentContainer` style entries, using the no-exposure check on the hot render path.
29
+ - Add an `agentEditReceived` collab-provider analytics event, fired once per received transaction
30
+ that contains agent-authored steps, with non-PII attributes (agent ids, count, kinds,
31
+ agent/total step counts).
32
+ - Add an `agentEditShimmerNotShown` COLLAB operational analytics event for agent edits that apply
33
+ without the shimmer (`rebasedConcurrentEdit`, `nothingToShow`, `captureThrew`,
34
+ `tornDownMidAnimation`); neutral action + `reason`, non-PII attributes.
35
+ - Hoist the top-level block/position helpers in `agent-shimmer-ranges` to reusable module scope,
36
+ drop the redundant upper-bound clamp in `agent-shimmer-decorations`, and remove the empty
37
+ `@example` JSDoc tags added in the original PR.
38
+
39
+ `@atlaskit/editor-common` gains a new subpath export,
40
+ `@atlaskit/editor-common/analytics/types/agent-edit-shimmer-events`, exposing the
41
+ `agentEditShimmerNotShown` operational event types. Usage:
42
+
43
+ ```ts
44
+ import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
45
+ import type { AgentEditShimmerNotShownReason } from '@atlaskit/editor-common/analytics/types/agent-edit-shimmer-events';
46
+
47
+ const reason: AgentEditShimmerNotShownReason = 'captureThrew';
48
+ editorAnalyticsApi?.fireAnalyticsEvent({
49
+ action: ACTION.AGENT_EDIT_SHIMMER_NOT_SHOWN,
50
+ actionSubject: ACTION_SUBJECT.COLLAB,
51
+ eventType: EVENT_TYPE.OPERATIONAL,
52
+ attributes: { reason },
53
+ });
54
+ ```
55
+
56
+ ### Patch Changes
57
+
58
+ - Updated dependencies
59
+
3
60
  ## 13.1.3
4
61
 
5
62
  ### Patch Changes
@@ -12,6 +12,7 @@ var _transform = require("@atlaskit/editor-prosemirror/transform");
12
12
  var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
13
13
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
14
14
  var _expVal = require("@atlaskit/tmp-editor-statsig/expVal");
15
+ var _analytics = require("./analytics");
15
16
  var _agentShimmerDecorations = require("./main/agent-shimmer-decorations");
16
17
  var _agentShimmerRanges = require("./main/agent-shimmer-ranges");
17
18
  var _utils = require("./utils");
@@ -40,7 +41,7 @@ var handleInit = exports.handleInit = function handleInit(initData, view, option
40
41
  tr.setMeta('isRemote', true);
41
42
  view.dispatch(tr);
42
43
  } else if (json) {
43
- applyRemoteSteps(json, view);
44
+ applyRemoteSteps(json, view, undefined, options, editorAnalyticsApi);
44
45
  }
45
46
  };
46
47
  var handleConnection = exports.handleConnection = function handleConnection(connectionData, view) {
@@ -51,15 +52,15 @@ var handlePresence = exports.handlePresence = function handlePresence(presenceDa
51
52
  var tr = view.state.tr;
52
53
  view.dispatch(tr.setMeta('presence', presenceData));
53
54
  };
54
- var applyRemoteData = exports.applyRemoteData = function applyRemoteData(remoteData, view, options) {
55
+ var applyRemoteData = exports.applyRemoteData = function applyRemoteData(remoteData, view, options, editorAnalyticsApi) {
55
56
  var json = remoteData.json,
56
57
  _remoteData$userIds = remoteData.userIds,
57
58
  userIds = _remoteData$userIds === void 0 ? [] : _remoteData$userIds;
58
59
  if (json) {
59
- applyRemoteSteps(json, view, userIds, options);
60
+ applyRemoteSteps(json, view, userIds, options, editorAnalyticsApi);
60
61
  }
61
62
  };
62
- var applyRemoteSteps = exports.applyRemoteSteps = function applyRemoteSteps(json, view, userIds, options) {
63
+ var applyRemoteSteps = exports.applyRemoteSteps = function applyRemoteSteps(json, view, userIds, options, editorAnalyticsApi) {
63
64
  if (!json || !json.length) {
64
65
  return;
65
66
  }
@@ -84,16 +85,28 @@ var applyRemoteSteps = exports.applyRemoteSteps = function applyRemoteSteps(json
84
85
  tr.setMeta('isRemote', true);
85
86
 
86
87
  // Agent edit shimmer: mark the ranges agent steps just wrote so the plugin reveals them with the
87
- // gloss-sweep purple-highlight sequence. Gated as a whole so no experiment reads run
88
- // off-experiment; off-path leaves `agentShimmers` empty and everything below is a no-op.
89
- var durationMs = 0;
88
+ // skeleton loader, then (optionally) a purple "just edited" highlight. Gated as a whole so no
89
+ // experiment reads run off-experiment; off-path leaves `agentShimmers` empty and everything below
90
+ // is a no-op.
91
+ var shimmerDurationMs = 0;
92
+ var highlightDurationMs = 0;
90
93
  var agentShimmers = [];
91
94
  if ((0, _expValEquals.expValEquals)('platform_editor_agent_be_streaming', 'isEnabled', true)) {
92
- durationMs = (0, _expVal.expVal)('platform_editor_agent_be_streaming', 'durationMs', _agentShimmerDecorations.AGENT_SHIMMER_DEFAULT_DURATION_MS);
95
+ // The skeleton and purple-highlight phases toggle independently. `shimmerDurationMs` is the
96
+ // skeleton lifetime; `0` skips the skeleton (an edit can still get the purple highlight).
97
+ shimmerDurationMs = (0, _expVal.expVal)('platform_editor_agent_be_streaming', 'shimmerDurationMs', _agentShimmerDecorations.AGENT_SHIMMER_DEFAULT_DURATION_MS);
98
+ // `highlightDurationMs` is the purple "just edited" highlight lifetime; `0` skips the highlight
99
+ // (the skeleton still shows). `0` on both shows nothing.
100
+ highlightDurationMs = (0, _expVal.expVal)('platform_editor_agent_be_streaming', 'highlightDurationMs', _agentShimmerDecorations.AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS);
93
101
  // Telepointer shown by default; `telepointerDisabled` hides it (inverted because `expVal`
94
102
  // only permits `false` as a boolean default).
95
103
  var telepointerEnabled = !(0, _expVal.expVal)('platform_editor_agent_be_streaming', 'telepointerDisabled', false);
96
- agentShimmers = (0, _agentShimmerRanges.getAgentShimmerRanges)(json, steps, tr, view, durationMs, telepointerEnabled);
104
+ agentShimmers = (0, _agentShimmerRanges.getAgentShimmerRanges)(json, steps, tr, view, shimmerDurationMs, highlightDurationMs, telepointerEnabled,
105
+ // An agent edit that applied without the shimmer fires the operational event on the same
106
+ // transaction, so success stays event-free and only degrades are reported.
107
+ function (reason, agentType, error) {
108
+ return editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent((0, _analytics.getAgentEditShimmerNotShownPayload)(reason, agentType, error))(tr);
109
+ });
97
110
  if (agentShimmers.length) {
98
111
  tr.setMeta(_agentShimmerDecorations.ADD_AGENT_SHIMMER_META, agentShimmers);
99
112
  }
@@ -109,22 +122,45 @@ var applyRemoteSteps = exports.applyRemoteSteps = function applyRemoteSteps(json
109
122
  }
110
123
  view.dispatch(tr);
111
124
 
112
- // Remove each shimmer after `durationMs`. `dispatchMeta` is guarded so a timer firing after
125
+ // Schedule each shimmer's phase changes. `dispatchMeta` is guarded so a timer firing after
113
126
  // teardown is a harmless no-op. Only reached when the gated block above produced ranges, so this
114
127
  // whole path is off-experiment-safe.
115
128
  if (agentShimmers.length) {
129
+ var tornDownReported = false;
116
130
  var dispatchMeta = function dispatchMeta(metaKey, value) {
117
131
  try {
118
132
  view.dispatch(view.state.tr.setMeta(metaKey, value));
119
133
  } catch (_unused) {
120
- // View torn down before the timer fired — nothing to clean up.
134
+ // View torn down before the timer fired — nothing to clean up. Report once (not once
135
+ // per shimmer) so we can see how often shimmers are interrupted by teardown.
136
+ if (!tornDownReported) {
137
+ tornDownReported = true;
138
+ editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.fireAnalyticsEvent((0, _analytics.getAgentEditShimmerNotShownPayload)('tornDownMidAnimation'));
139
+ }
121
140
  }
122
141
  };
123
142
  agentShimmers.forEach(function (_ref) {
124
143
  var shimmerId = _ref.shimmerId;
125
- setTimeout(function () {
126
- dispatchMeta(_agentShimmerDecorations.REMOVE_AGENT_SHIMMER_META, shimmerId);
127
- }, durationMs);
144
+ if (shimmerDurationMs > 0) {
145
+ // Skeleton first. When it clears, show the purple highlight (if enabled) then remove;
146
+ // otherwise remove straight away.
147
+ setTimeout(function () {
148
+ if (highlightDurationMs > 0) {
149
+ dispatchMeta(_agentShimmerDecorations.HIGHLIGHT_AGENT_SHIMMER_META, shimmerId);
150
+ setTimeout(function () {
151
+ dispatchMeta(_agentShimmerDecorations.REMOVE_AGENT_SHIMMER_META, shimmerId);
152
+ }, highlightDurationMs);
153
+ } else {
154
+ dispatchMeta(_agentShimmerDecorations.REMOVE_AGENT_SHIMMER_META, shimmerId);
155
+ }
156
+ }, shimmerDurationMs);
157
+ } else {
158
+ // Skeleton disabled: the range already starts in the highlight phase, so just time its
159
+ // removal. (Reached only when `highlightDurationMs > 0`, else no ranges were produced.)
160
+ setTimeout(function () {
161
+ dispatchMeta(_agentShimmerDecorations.REMOVE_AGENT_SHIMMER_META, shimmerId);
162
+ }, highlightDurationMs);
163
+ }
128
164
  });
129
165
  }
130
166
  }
@@ -1,12 +1,16 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
6
- exports.addSynchronyErrorAnalytics = exports.addSynchronyEntityAnalytics = void 0;
7
+ exports.getAgentEditShimmerNotShownPayload = exports.addSynchronyErrorAnalytics = exports.addSynchronyEntityAnalytics = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
7
9
  var _analytics = require("@atlaskit/editor-common/analytics");
8
10
  var _coreUtils = require("@atlaskit/editor-common/core-utils");
9
11
  var _utils = require("@atlaskit/editor-common/utils");
12
+ 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; }
13
+ 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) { (0, _defineProperty2.default)(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; }
10
14
  var addSynchronyErrorAnalytics = exports.addSynchronyErrorAnalytics = function addSynchronyErrorAnalytics(state, tr, featureFlags, editorAnalyticsApi) {
11
15
  return function (error) {
12
16
  var browserExtensions = (0, _utils.sniffUserBrowserExtensions)({
@@ -32,6 +36,27 @@ var addSynchronyErrorAnalytics = exports.addSynchronyErrorAnalytics = function a
32
36
  return tr;
33
37
  };
34
38
  };
39
+
40
+ /**
41
+ * Builds the `agentEditShimmerNotShown` operational event fired when an agent-authored edit applied
42
+ * instantly without the skeleton shimmer/telepointer. Success is the ABSENCE of this event; a
43
+ * neutral action + `reason` keeps expected degrades out of error dashboards. Attributes are non-PII:
44
+ * only the agent kind and a sanitised error name/message (never the raw Error or any doc content).
45
+ */
46
+ var getAgentEditShimmerNotShownPayload = exports.getAgentEditShimmerNotShownPayload = function getAgentEditShimmerNotShownPayload(reason, agentType, error) {
47
+ return {
48
+ action: _analytics.ACTION.AGENT_EDIT_SHIMMER_NOT_SHOWN,
49
+ actionSubject: _analytics.ACTION_SUBJECT.COLLAB,
50
+ eventType: _analytics.EVENT_TYPE.OPERATIONAL,
51
+ attributes: _objectSpread(_objectSpread({
52
+ reason: reason
53
+ }, agentType ? {
54
+ agentType: agentType
55
+ } : {}), error ? {
56
+ error: "".concat(error.name, ": ").concat(error.message)
57
+ } : {})
58
+ };
59
+ };
35
60
  var addSynchronyEntityAnalytics = exports.addSynchronyEntityAnalytics = function addSynchronyEntityAnalytics(state, tr) {
36
61
  return function (type, editorAnalyticsApi) {
37
62
  editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent({
@@ -55,7 +55,7 @@ var subscribe = exports.subscribe = effect(function (view, provider, options, fe
55
55
  return (0, _actions.handleConnection)(data, view);
56
56
  },
57
57
  dataHandler: function dataHandler(data) {
58
- return (0, _actions.applyRemoteData)(data, view, options);
58
+ return (0, _actions.applyRemoteData)(data, view, options, editorAnalyticsApi);
59
59
  },
60
60
  presenceHandler: function presenceHandler(data) {
61
61
  return (0, _actions.handlePresence)(data, view);
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.reduceAgentShimmers = exports.buildAgentShimmerDecorations = exports.ROVO_AGENT_TELEPOINTER_LABEL_CLASS = exports.ROVO_AGENT_TELEPOINTER_CLASS = exports.REMOVE_AGENT_SHIMMER_META = exports.AGENT_SHIMMER_DEFAULT_DURATION_MS = exports.AGENT_SHIMMER_CLASS = exports.ADD_AGENT_SHIMMER_META = void 0;
7
+ exports.reduceAgentShimmers = exports.buildAgentShimmerDecorations = exports.ROVO_AGENT_TELEPOINTER_LABEL_CLASS = exports.ROVO_AGENT_TELEPOINTER_CLASS = exports.REMOVE_AGENT_SHIMMER_META = exports.HIGHLIGHT_AGENT_SHIMMER_META = exports.AGENT_SHIMMER_DEFAULT_DURATION_MS = exports.AGENT_SHIMMER_CLASS = exports.AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS = exports.AGENT_EDIT_HIGHLIGHT_CLASS = exports.ADD_AGENT_SHIMMER_META = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _view = require("@atlaskit/editor-prosemirror/view");
10
10
  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; }
@@ -15,17 +15,31 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
15
15
 
16
16
  /** Default time the skeleton shimmer stays on the agent-authored content (ms). */
17
17
  var AGENT_SHIMMER_DEFAULT_DURATION_MS = exports.AGENT_SHIMMER_DEFAULT_DURATION_MS = 3000;
18
+ /**
19
+ * Default time the purple "just edited" highlight stays after the skeleton clears (ms); `0` disables
20
+ * the highlight phase so the shimmer just reveals the content.
21
+ */
22
+ var AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS = exports.AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS = 2000;
18
23
 
19
24
  // Skeleton-loader bar over the agent-authored range, plus a Rovo AI telepointer at the end.
20
25
  var AGENT_SHIMMER_CLASS = exports.AGENT_SHIMMER_CLASS = 'collab-agent-shimmer';
26
+ // Purple "just edited" highlight shown over the range after the skeleton clears — same style as the
27
+ // editor AI "improve writing" in-editor highlight (subtle purple background + dashed purple underline).
28
+ var AGENT_EDIT_HIGHLIGHT_CLASS = exports.AGENT_EDIT_HIGHLIGHT_CLASS = 'collab-agent-edit-highlight';
21
29
  var ROVO_AGENT_TELEPOINTER_CLASS = exports.ROVO_AGENT_TELEPOINTER_CLASS = 'ai-in-editor-telepointer';
22
30
  var ROVO_AGENT_TELEPOINTER_LABEL_CLASS = exports.ROVO_AGENT_TELEPOINTER_LABEL_CLASS = 'ai-in-editor-telepointer-label';
23
31
  var ADD_AGENT_SHIMMER_META = exports.ADD_AGENT_SHIMMER_META = 'addAgentShimmer'; // register the shimmer decorations
32
+ var HIGHLIGHT_AGENT_SHIMMER_META = exports.HIGHLIGHT_AGENT_SHIMMER_META = 'highlightAgentShimmer'; // skeleton → purple highlight phase
24
33
  var REMOVE_AGENT_SHIMMER_META = exports.REMOVE_AGENT_SHIMMER_META = 'removeAgentShimmer'; // remove them once the shimmer ends
25
34
 
26
- // A range an agent step wrote; the skeleton + telepointer decorations are drawn over `from`..`to`
27
- // and kept until removal (so positions can be re-mapped). Pure data only. `telepointerLabel` is the
28
- // label for the trailing agent telepointer (the agent's type); when absent, no telepointer is shown.
35
+ // A shimmer runs in two phases: the `skeleton` loader, then (optionally) the purple `highlight` over
36
+ // the revealed content, before removal.
37
+
38
+ // A range an agent step wrote; the shimmer decorations are drawn over `from`..`to` and kept until
39
+ // removal (so positions can be re-mapped). Pure data only. `phase` selects skeleton vs purple
40
+ // highlight. `telepointerLabel` is the label for the trailing agent telepointer (shown through both
41
+ // phases); when absent, no telepointer is shown. `highlightDurationMs` is the purple-highlight
42
+ // lifetime, used to size its ease in/out animation so it matches the removal timer.
29
43
 
30
44
  // Rovo AI in-editor telepointer/cursor shown at the end of an agent-authored range (same DOM/style
31
45
  // pattern as editor-plugin-ai's in-editor direct-streaming telepointer).
@@ -47,7 +61,7 @@ var createRovoAgentTelepointer = function createRovoAgentTelepointer(label) {
47
61
  * supersedes any still-in-flight shimmer), and drops a range when its removal timer fires. Returns a
48
62
  * fresh array (never mutates in place) plus whether anything changed.
49
63
  */
50
- var reduceAgentShimmers = exports.reduceAgentShimmers = function reduceAgentShimmers(current, tr, added, removedShimmerId) {
64
+ var reduceAgentShimmers = exports.reduceAgentShimmers = function reduceAgentShimmers(current, tr, added, removedShimmerId, highlightShimmerId) {
51
65
  var next = current;
52
66
  var changed = false;
53
67
 
@@ -68,6 +82,19 @@ var reduceAgentShimmers = exports.reduceAgentShimmers = function reduceAgentShim
68
82
  });
69
83
  changed = true;
70
84
  }
85
+ // Transition a shimmer from the skeleton phase to the purple highlight phase (skeleton timer fired).
86
+ // Guard on an actual match so a timer firing after the shimmer was already removed (superseded by a
87
+ // new batch, or cleared) doesn't rebuild the array and trigger a pointless decoration rebuild.
88
+ if (highlightShimmerId && next.some(function (shimmer) {
89
+ return shimmer.shimmerId === highlightShimmerId;
90
+ })) {
91
+ next = next.map(function (shimmer) {
92
+ return shimmer.shimmerId === highlightShimmerId ? _objectSpread(_objectSpread({}, shimmer), {}, {
93
+ phase: 'highlight'
94
+ }) : shimmer;
95
+ });
96
+ changed = true;
97
+ }
71
98
  if (removedShimmerId) {
72
99
  next = next.filter(function (shimmer) {
73
100
  return shimmer.shimmerId !== removedShimmerId;
@@ -81,33 +108,45 @@ var reduceAgentShimmers = exports.reduceAgentShimmers = function reduceAgentShim
81
108
  };
82
109
 
83
110
  /**
84
- * Builds the inline skeleton-bar + trailing telepointer decorations for the active shimmer ranges.
85
- * `getValidPos` clamps a raw position to a valid decoration position (owned by `plugin-state`).
86
- * One bad range is isolated via `onError` so it can't kill the others.
111
+ * Builds the inline decorations (skeleton bar or purple highlight, per phase) plus the trailing
112
+ * telepointer for the active shimmer ranges. `getValidPos` clamps a raw position to a valid
113
+ * decoration position (owned by `plugin-state`). One bad range is isolated via `onError` so it can't
114
+ * kill the others.
87
115
  */
88
116
  var buildAgentShimmerDecorations = exports.buildAgentShimmerDecorations = function buildAgentShimmerDecorations(tr, shimmers, getValidPos, onError) {
89
117
  var decorations = [];
90
- var docEnd = tr.doc.nodeSize - 2;
91
118
  shimmers.forEach(function (_ref) {
92
119
  var shimmerId = _ref.shimmerId,
93
120
  from = _ref.from,
94
121
  to = _ref.to,
95
- telepointerLabel = _ref.telepointerLabel;
122
+ telepointerLabel = _ref.telepointerLabel,
123
+ phase = _ref.phase,
124
+ highlightDurationMs = _ref.highlightDurationMs;
96
125
  try {
126
+ // `getValidPos` already clamps to the last valid position, so only the lower bound needs
127
+ // guarding here (a raw `from < 1` would throw in `doc.resolve`).
97
128
  var validFrom = getValidPos(tr, Math.max(from, 1));
98
- var validTo = getValidPos(tr, Math.min(to, docEnd));
129
+ var validTo = getValidPos(tr, to);
99
130
  if (validTo <= validFrom) {
100
131
  return;
101
132
  }
102
- // Skeleton-loader bar over the whole agent-authored range...
103
- decorations.push(_view.Decoration.inline(validFrom, validTo, {
104
- class: AGENT_SHIMMER_CLASS
105
- }, {
133
+ // Inline decoration over the whole range: the grey skeleton loader, or (once revealed) the
134
+ // purple "just edited" highlight. The highlight eases in and out over its lifetime via a CSS
135
+ // animation whose duration is set inline so it matches the removal timer.
136
+ var isHighlight = phase === 'highlight';
137
+ var inlineAttrs = {
138
+ class: isHighlight ? AGENT_EDIT_HIGHLIGHT_CLASS : AGENT_SHIMMER_CLASS
139
+ };
140
+ if (isHighlight && highlightDurationMs > 0) {
141
+ inlineAttrs.style = "animation-duration: ".concat(highlightDurationMs, "ms");
142
+ }
143
+ decorations.push(_view.Decoration.inline(validFrom, validTo, inlineAttrs, {
106
144
  isAgentShimmer: true,
107
145
  shimmerId: shimmerId
108
146
  }));
109
- // ...and, when enabled, a Rovo AI telepointer/cursor (labelled with the agent's type) at the
110
- // end of the range.
147
+ // Rovo AI telepointer/cursor (labelled with the agent's type) at the end of the range, shown
148
+ // through BOTH the skeleton and purple-highlight phases so the agent's cursor stays put until
149
+ // the edit is fully revealed.
111
150
  if (telepointerLabel) {
112
151
  decorations.push(_view.Decoration.widget(validTo, createRovoAgentTelepointer(telepointerLabel), {
113
152
  isAgentShimmer: true,
@@ -8,8 +8,9 @@ var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
8
8
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
9
9
  // When an agent step lands we cover the top-level block(s) it wrote with a skeleton-loader shimmer
10
10
  // (plus a Rovo agent telepointer at the end of the range), then remove it on a timer to reveal the
11
- // content. Gated behind the `platform_editor_agent_be_streaming` experiment; `durationMs` sets how
12
- // long the shimmer stays and a 0 duration disables it.
11
+ // content. Gated behind the `platform_editor_agent_be_streaming` experiment. `shimmerDurationMs` and
12
+ // `highlightDurationMs` size the skeleton and purple-highlight phases and toggle independently: `0` on
13
+ // either skips that phase, `0` on both shows nothing.
13
14
  var agentShimmerIdCounter = 0;
14
15
 
15
16
  // A step is position-neutral when its StepMap changes no range's length — i.e. it shifts no
@@ -26,6 +27,26 @@ var isPositionNeutralStep = exports.isPositionNeutralStep = function isPositionN
26
27
  return neutral;
27
28
  };
28
29
 
30
+ // Top-level block/position helpers over a doc. Kept at module scope (rather than re-created as
31
+ // closures on every call) so they are defined once, reusable, and unit-testable. Each clamps into
32
+ // valid document coordinates before resolving.
33
+ var clampToDoc = function clampToDoc(doc, pos) {
34
+ return Math.min(Math.max(pos, 1), doc.content.size);
35
+ };
36
+ var topLevelBlockIndexAt = function topLevelBlockIndexAt(doc, pos) {
37
+ return doc.resolve(clampToDoc(doc, pos)).index(0);
38
+ };
39
+ // Start of the content of the top-level block containing `pos`, so the whole block is covered.
40
+ var topLevelBlockContentStart = function topLevelBlockContentStart(doc, pos) {
41
+ var $pos = doc.resolve(clampToDoc(doc, pos));
42
+ return $pos.depth >= 1 ? $pos.start(1) : pos;
43
+ };
44
+ // End of the content of the top-level block containing `pos`, so the whole block is covered.
45
+ var topLevelBlockContentEnd = function topLevelBlockContentEnd(doc, pos) {
46
+ var $pos = doc.resolve(clampToDoc(doc, pos));
47
+ return $pos.depth >= 1 ? $pos.end(1) : pos;
48
+ };
49
+
29
50
  /**
30
51
  * Derive the shimmer ranges for the agent-authored steps in a received batch, in final-doc
31
52
  * coordinates. `agentType` present ⇒ agent-authored (per the NCS↔Editor steps contract). Each
@@ -42,15 +63,20 @@ var isPositionNeutralStep = exports.isPositionNeutralStep = function isPositionN
42
63
  * rebased local step changed sizes (a rare, safe degrade). Any unexpected error also degrades to no
43
64
  * shimmer, so this never throws into the shared remote-step handler.
44
65
  */
45
- var getAgentShimmerRanges = exports.getAgentShimmerRanges = function getAgentShimmerRanges(json, steps, tr, view, durationMs, telepointerEnabled) {
66
+ var getAgentShimmerRanges = exports.getAgentShimmerRanges = function getAgentShimmerRanges(json, steps, tr, view, shimmerDurationMs, highlightDurationMs, telepointerEnabled, onNotShown) {
46
67
  var _json$find;
47
68
  if (!(0, _expValEquals.expValEquals)('platform_editor_agent_be_streaming', 'isEnabled', true)) {
48
69
  return [];
49
70
  }
50
- // Nothing to reveal if the shimmer is disabled.
51
- if (durationMs <= 0) {
71
+ // The two phases toggle independently: `shimmerDurationMs` sizes the skeleton, `highlightDurationMs`
72
+ // sizes the purple highlight, and `0` on either skips just that phase. Nothing to reveal only when
73
+ // both are off.
74
+ if (shimmerDurationMs <= 0 && highlightDurationMs <= 0) {
52
75
  return [];
53
76
  }
77
+ // Start in the skeleton phase when it's enabled, otherwise straight into the purple highlight phase
78
+ // (skeleton toggled off, highlight on).
79
+ var initialPhase = shimmerDurationMs > 0 ? 'skeleton' : 'highlight';
54
80
  // Telepointer label = the agent's type upper-cased (e.g. `mcp` → "MCP"), falling back to a generic
55
81
  // "Agent"; `undefined` when the telepointer is disabled, so the plugin skips it. `agentType` is the
56
82
  // same on every step of an agent batch, so read it from the first agent-authored step.
@@ -58,7 +84,11 @@ var getAgentShimmerRanges = exports.getAgentShimmerRanges = function getAgentShi
58
84
  var agentType = (_json$find = json.find(function (step) {
59
85
  return typeof (step === null || step === void 0 ? void 0 : step.agentType) === 'string';
60
86
  })) === null || _json$find === void 0 ? void 0 : _json$find.agentType;
61
- var telepointerLabel = telepointerEnabled ? (agentType === null || agentType === void 0 ? void 0 : agentType.toUpperCase()) || 'Agent' : undefined;
87
+ // No agent-authored steps in this batch nothing to shimmer, and not a "not shown" case.
88
+ if (agentType === undefined) {
89
+ return [];
90
+ }
91
+ var telepointerLabel = telepointerEnabled ? agentType.toUpperCase() || 'Agent' : undefined;
62
92
  // When the batch was rebased over local unconfirmed steps, our index-based range math is only
63
93
  // valid if those local steps shifted no positions. Attribute-only steps (e.g. `localId`) and
64
94
  // same-size replacements are position-neutral, so the shimmer stays correct. Skip only when a
@@ -69,6 +99,7 @@ var getAgentShimmerRanges = exports.getAgentShimmerRanges = function getAgentShi
69
99
  if (unconfirmed.some(function (entry) {
70
100
  return !isPositionNeutralStep(entry.step);
71
101
  })) {
102
+ onNotShown === null || onNotShown === void 0 || onNotShown('rebasedConcurrentEdit', agentType);
72
103
  return [];
73
104
  }
74
105
  }
@@ -108,6 +139,7 @@ var getAgentShimmerRanges = exports.getAgentShimmerRanges = function getAgentShi
108
139
  });
109
140
  });
110
141
  if (!infos.length) {
142
+ onNotShown === null || onNotShown === void 0 || onNotShown('nothingToShow', agentType);
111
143
  return [];
112
144
  }
113
145
 
@@ -119,28 +151,12 @@ var getAgentShimmerRanges = exports.getAgentShimmerRanges = function getAgentShi
119
151
  // touched blocks (index n and n+1) merge into one group so a multi-block rewrite reveals together;
120
152
  // a genuinely untouched block in between (index gap > 1) splits the run, so far-apart edits stay
121
153
  // independent.
122
- var docSize = tr.doc.content.size;
123
- var clampPos = function clampPos(pos) {
124
- return Math.min(Math.max(pos, 1), docSize);
125
- };
126
- var blockIndexAt = function blockIndexAt(pos) {
127
- return tr.doc.resolve(clampPos(pos)).index(0);
128
- };
129
- // Start/end of the content of the top-level block containing `pos`, so the whole block is covered.
130
- var blockContentStart = function blockContentStart(pos) {
131
- var $pos = tr.doc.resolve(clampPos(pos));
132
- return $pos.depth >= 1 ? $pos.start(1) : pos;
133
- };
134
- var blockContentEnd = function blockContentEnd(pos) {
135
- var $pos = tr.doc.resolve(clampPos(pos));
136
- return $pos.depth >= 1 ? $pos.end(1) : pos;
137
- };
138
154
  var sorted = [].concat(infos).sort(function (a, b) {
139
155
  return a.from - b.from || a.to - b.to;
140
156
  });
141
157
  var groups = [];
142
158
  sorted.forEach(function (info) {
143
- var block = blockIndexAt(info.from);
159
+ var block = topLevelBlockIndexAt(tr.doc, info.from);
144
160
  var current = groups[groups.length - 1];
145
161
  if (current && block <= current.maxBlock + 1) {
146
162
  current.to = Math.max(current.to, info.to);
@@ -156,14 +172,17 @@ var getAgentShimmerRanges = exports.getAgentShimmerRanges = function getAgentShi
156
172
  return groups.map(function (group) {
157
173
  return {
158
174
  shimmerId: "agent-shimmer-".concat(agentShimmerIdCounter++),
159
- from: blockContentStart(group.from),
160
- to: blockContentEnd(group.to),
161
- telepointerLabel: telepointerLabel
175
+ from: topLevelBlockContentStart(tr.doc, group.from),
176
+ to: topLevelBlockContentEnd(tr.doc, group.to),
177
+ telepointerLabel: telepointerLabel,
178
+ phase: initialPhase,
179
+ highlightDurationMs: highlightDurationMs
162
180
  };
163
181
  });
164
- } catch (_unused) {
182
+ } catch (err) {
165
183
  // Never let shimmer range derivation throw into the shared remote-step handler; degrade to no
166
184
  // shimmer.
185
+ onNotShown === null || onNotShown === void 0 || onNotShown('captureThrew', agentType, err);
167
186
  return [];
168
187
  }
169
188
  };
@@ -97,6 +97,7 @@ var PluginState = exports.PluginState = /*#__PURE__*/function () {
97
97
  var nudgeTelepointerData = tr.getMeta('nudgeTelepointer');
98
98
  var agentShimmerData = tr.getMeta(_agentShimmerDecorations.ADD_AGENT_SHIMMER_META);
99
99
  var removeAgentShimmerId = tr.getMeta(_agentShimmerDecorations.REMOVE_AGENT_SHIMMER_META);
100
+ var highlightAgentShimmerId = tr.getMeta(_agentShimmerDecorations.HIGHLIGHT_AGENT_SHIMMER_META);
100
101
  var sessionIdData = tr.getMeta('sessionId');
101
102
  var collabInitialised = tr.getMeta('collabInitialised');
102
103
  if (typeof collabInitialised !== 'boolean') {
@@ -239,7 +240,7 @@ var PluginState = exports.PluginState = /*#__PURE__*/function () {
239
240
  // here can never corrupt telepointer decorations or the shared decoration set. Never mutates
240
241
  // shimmer entries in place: each change produces a fresh array of fresh objects.
241
242
  try {
242
- var _reduceAgentShimmers = (0, _agentShimmerDecorations.reduceAgentShimmers)(this.agentShimmers, tr, agentShimmerData, removeAgentShimmerId),
243
+ var _reduceAgentShimmers = (0, _agentShimmerDecorations.reduceAgentShimmers)(this.agentShimmers, tr, agentShimmerData, removeAgentShimmerId, highlightAgentShimmerId),
243
244
  changed = _reduceAgentShimmers.changed,
244
245
  next = _reduceAgentShimmers.next;
245
246
  if (changed) {