@atlaskit/editor-plugin-collab-edit 13.1.2 → 13.2.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 +52 -0
- package/dist/cjs/pm-plugins/actions.js +50 -14
- package/dist/cjs/pm-plugins/analytics.js +26 -1
- package/dist/cjs/pm-plugins/events/handlers.js +1 -1
- package/dist/cjs/pm-plugins/main/agent-shimmer-decorations.js +56 -17
- package/dist/cjs/pm-plugins/main/agent-shimmer-ranges.js +46 -27
- package/dist/cjs/pm-plugins/main/plugin-state.js +2 -1
- package/dist/es2019/pm-plugins/actions.js +49 -15
- package/dist/es2019/pm-plugins/analytics.js +23 -0
- package/dist/es2019/pm-plugins/events/handlers.js +1 -1
- package/dist/es2019/pm-plugins/main/agent-shimmer-decorations.js +52 -16
- package/dist/es2019/pm-plugins/main/agent-shimmer-ranges.js +42 -23
- package/dist/es2019/pm-plugins/main/plugin-state.js +3 -2
- package/dist/esm/pm-plugins/actions.js +51 -15
- package/dist/esm/pm-plugins/analytics.js +24 -0
- package/dist/esm/pm-plugins/events/handlers.js +1 -1
- package/dist/esm/pm-plugins/main/agent-shimmer-decorations.js +55 -16
- package/dist/esm/pm-plugins/main/agent-shimmer-ranges.js +46 -27
- package/dist/esm/pm-plugins/main/plugin-state.js +3 -2
- package/dist/types/pm-plugins/actions.d.ts +2 -2
- package/dist/types/pm-plugins/analytics.d.ts +9 -1
- package/dist/types/pm-plugins/main/agent-shimmer-decorations.d.ts +15 -4
- package/dist/types/pm-plugins/main/agent-shimmer-ranges.d.ts +2 -1
- package/package.json +3 -3
|
@@ -9,7 +9,8 @@ import { Step } from '@atlaskit/editor-prosemirror/transform';
|
|
|
9
9
|
import { receiveTransaction } from '@atlaskit/prosemirror-collab';
|
|
10
10
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
11
11
|
import { expVal } from '@atlaskit/tmp-editor-statsig/expVal';
|
|
12
|
-
import {
|
|
12
|
+
import { getAgentEditShimmerNotShownPayload } from './analytics';
|
|
13
|
+
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';
|
|
13
14
|
import { getAgentShimmerRanges } from './main/agent-shimmer-ranges';
|
|
14
15
|
import { replaceDocument } from './utils';
|
|
15
16
|
|
|
@@ -35,7 +36,7 @@ export const handleInit = (initData, view, options, editorAnalyticsApi) => {
|
|
|
35
36
|
tr.setMeta('isRemote', true);
|
|
36
37
|
view.dispatch(tr);
|
|
37
38
|
} else if (json) {
|
|
38
|
-
applyRemoteSteps(json, view);
|
|
39
|
+
applyRemoteSteps(json, view, undefined, options, editorAnalyticsApi);
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
42
|
export const handleConnection = (connectionData, view) => {
|
|
@@ -54,16 +55,16 @@ export const handlePresence = (presenceData, view) => {
|
|
|
54
55
|
} = view;
|
|
55
56
|
view.dispatch(tr.setMeta('presence', presenceData));
|
|
56
57
|
};
|
|
57
|
-
export const applyRemoteData = (remoteData, view, options) => {
|
|
58
|
+
export const applyRemoteData = (remoteData, view, options, editorAnalyticsApi) => {
|
|
58
59
|
const {
|
|
59
60
|
json,
|
|
60
61
|
userIds = []
|
|
61
62
|
} = remoteData;
|
|
62
63
|
if (json) {
|
|
63
|
-
applyRemoteSteps(json, view, userIds, options);
|
|
64
|
+
applyRemoteSteps(json, view, userIds, options, editorAnalyticsApi);
|
|
64
65
|
}
|
|
65
66
|
};
|
|
66
|
-
export const applyRemoteSteps = (json, view, userIds, options) => {
|
|
67
|
+
export const applyRemoteSteps = (json, view, userIds, options, editorAnalyticsApi) => {
|
|
67
68
|
if (!json || !json.length) {
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
@@ -88,16 +89,26 @@ export const applyRemoteSteps = (json, view, userIds, options) => {
|
|
|
88
89
|
tr.setMeta('isRemote', true);
|
|
89
90
|
|
|
90
91
|
// Agent edit shimmer: mark the ranges agent steps just wrote so the plugin reveals them with the
|
|
91
|
-
//
|
|
92
|
-
// off-experiment; off-path leaves `agentShimmers` empty and everything below
|
|
93
|
-
|
|
92
|
+
// skeleton loader, then (optionally) a purple "just edited" highlight. Gated as a whole so no
|
|
93
|
+
// experiment reads run off-experiment; off-path leaves `agentShimmers` empty and everything below
|
|
94
|
+
// is a no-op.
|
|
95
|
+
let shimmerDurationMs = 0;
|
|
96
|
+
let highlightDurationMs = 0;
|
|
94
97
|
let agentShimmers = [];
|
|
95
98
|
if (expValEquals('platform_editor_agent_be_streaming', 'isEnabled', true)) {
|
|
96
|
-
|
|
99
|
+
// The skeleton and purple-highlight phases toggle independently. `shimmerDurationMs` is the
|
|
100
|
+
// skeleton lifetime; `0` skips the skeleton (an edit can still get the purple highlight).
|
|
101
|
+
shimmerDurationMs = expVal('platform_editor_agent_be_streaming', 'shimmerDurationMs', AGENT_SHIMMER_DEFAULT_DURATION_MS);
|
|
102
|
+
// `highlightDurationMs` is the purple "just edited" highlight lifetime; `0` skips the highlight
|
|
103
|
+
// (the skeleton still shows). `0` on both shows nothing.
|
|
104
|
+
highlightDurationMs = expVal('platform_editor_agent_be_streaming', 'highlightDurationMs', AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS);
|
|
97
105
|
// Telepointer shown by default; `telepointerDisabled` hides it (inverted because `expVal`
|
|
98
106
|
// only permits `false` as a boolean default).
|
|
99
107
|
const telepointerEnabled = !expVal('platform_editor_agent_be_streaming', 'telepointerDisabled', false);
|
|
100
|
-
agentShimmers = getAgentShimmerRanges(json, steps, tr, view,
|
|
108
|
+
agentShimmers = getAgentShimmerRanges(json, steps, tr, view, shimmerDurationMs, highlightDurationMs, telepointerEnabled,
|
|
109
|
+
// An agent edit that applied without the shimmer fires the operational event on the same
|
|
110
|
+
// transaction, so success stays event-free and only degrades are reported.
|
|
111
|
+
(reason, agentType, error) => editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(getAgentEditShimmerNotShownPayload(reason, agentType, error))(tr));
|
|
101
112
|
if (agentShimmers.length) {
|
|
102
113
|
tr.setMeta(ADD_AGENT_SHIMMER_META, agentShimmers);
|
|
103
114
|
}
|
|
@@ -113,23 +124,46 @@ export const applyRemoteSteps = (json, view, userIds, options) => {
|
|
|
113
124
|
}
|
|
114
125
|
view.dispatch(tr);
|
|
115
126
|
|
|
116
|
-
//
|
|
127
|
+
// Schedule each shimmer's phase changes. `dispatchMeta` is guarded so a timer firing after
|
|
117
128
|
// teardown is a harmless no-op. Only reached when the gated block above produced ranges, so this
|
|
118
129
|
// whole path is off-experiment-safe.
|
|
119
130
|
if (agentShimmers.length) {
|
|
131
|
+
let tornDownReported = false;
|
|
120
132
|
const dispatchMeta = (metaKey, value) => {
|
|
121
133
|
try {
|
|
122
134
|
view.dispatch(view.state.tr.setMeta(metaKey, value));
|
|
123
135
|
} catch {
|
|
124
|
-
// View torn down before the timer fired — nothing to clean up.
|
|
136
|
+
// View torn down before the timer fired — nothing to clean up. Report once (not once
|
|
137
|
+
// per shimmer) so we can see how often shimmers are interrupted by teardown.
|
|
138
|
+
if (!tornDownReported) {
|
|
139
|
+
tornDownReported = true;
|
|
140
|
+
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.fireAnalyticsEvent(getAgentEditShimmerNotShownPayload('tornDownMidAnimation'));
|
|
141
|
+
}
|
|
125
142
|
}
|
|
126
143
|
};
|
|
127
144
|
agentShimmers.forEach(({
|
|
128
145
|
shimmerId
|
|
129
146
|
}) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
if (shimmerDurationMs > 0) {
|
|
148
|
+
// Skeleton first. When it clears, show the purple highlight (if enabled) then remove;
|
|
149
|
+
// otherwise remove straight away.
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
if (highlightDurationMs > 0) {
|
|
152
|
+
dispatchMeta(HIGHLIGHT_AGENT_SHIMMER_META, shimmerId);
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
dispatchMeta(REMOVE_AGENT_SHIMMER_META, shimmerId);
|
|
155
|
+
}, highlightDurationMs);
|
|
156
|
+
} else {
|
|
157
|
+
dispatchMeta(REMOVE_AGENT_SHIMMER_META, shimmerId);
|
|
158
|
+
}
|
|
159
|
+
}, shimmerDurationMs);
|
|
160
|
+
} else {
|
|
161
|
+
// Skeleton disabled: the range already starts in the highlight phase, so just time its
|
|
162
|
+
// removal. (Reached only when `highlightDurationMs > 0`, else no ranges were produced.)
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
dispatchMeta(REMOVE_AGENT_SHIMMER_META, shimmerId);
|
|
165
|
+
}, highlightDurationMs);
|
|
166
|
+
}
|
|
133
167
|
});
|
|
134
168
|
}
|
|
135
169
|
}
|
|
@@ -26,6 +26,29 @@ export const addSynchronyErrorAnalytics = (state, tr, featureFlags, editorAnalyt
|
|
|
26
26
|
return tr;
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Builds the `agentEditShimmerNotShown` operational event fired when an agent-authored edit applied
|
|
32
|
+
* instantly without the skeleton shimmer/telepointer. Success is the ABSENCE of this event; a
|
|
33
|
+
* neutral action + `reason` keeps expected degrades out of error dashboards. Attributes are non-PII:
|
|
34
|
+
* only the agent kind and a sanitised error name/message (never the raw Error or any doc content).
|
|
35
|
+
*/
|
|
36
|
+
export const getAgentEditShimmerNotShownPayload = (reason, agentType, error) => ({
|
|
37
|
+
action: ACTION.AGENT_EDIT_SHIMMER_NOT_SHOWN,
|
|
38
|
+
actionSubject: ACTION_SUBJECT.COLLAB,
|
|
39
|
+
eventType: EVENT_TYPE.OPERATIONAL,
|
|
40
|
+
attributes: {
|
|
41
|
+
reason,
|
|
42
|
+
// Spread conditionally so absent context isn't emitted as `undefined` (the teardown path has no
|
|
43
|
+
// single agent, and non-throw reasons have no error).
|
|
44
|
+
...(agentType ? {
|
|
45
|
+
agentType
|
|
46
|
+
} : {}),
|
|
47
|
+
...(error ? {
|
|
48
|
+
error: `${error.name}: ${error.message}`
|
|
49
|
+
} : {})
|
|
50
|
+
}
|
|
51
|
+
});
|
|
29
52
|
export const addSynchronyEntityAnalytics = (state, tr) => {
|
|
30
53
|
return (type, editorAnalyticsApi) => {
|
|
31
54
|
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent({
|
|
@@ -44,7 +44,7 @@ export const subscribe = effect((view, provider, options, featureFlags, _provide
|
|
|
44
44
|
handleInit(data, view, options, editorAnalyticsApi);
|
|
45
45
|
},
|
|
46
46
|
connectedHandler: data => handleConnection(data, view),
|
|
47
|
-
dataHandler: data => applyRemoteData(data, view, options),
|
|
47
|
+
dataHandler: data => applyRemoteData(data, view, options, editorAnalyticsApi),
|
|
48
48
|
presenceHandler: data => handlePresence(data, view),
|
|
49
49
|
telepointerHandler: data => handleTelePointer(data, view),
|
|
50
50
|
localStepsHandler: data => {
|
|
@@ -6,17 +6,31 @@ import { Decoration } from '@atlaskit/editor-prosemirror/view';
|
|
|
6
6
|
|
|
7
7
|
/** Default time the skeleton shimmer stays on the agent-authored content (ms). */
|
|
8
8
|
export const AGENT_SHIMMER_DEFAULT_DURATION_MS = 3000;
|
|
9
|
+
/**
|
|
10
|
+
* Default time the purple "just edited" highlight stays after the skeleton clears (ms); `0` disables
|
|
11
|
+
* the highlight phase so the shimmer just reveals the content.
|
|
12
|
+
*/
|
|
13
|
+
export const AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS = 2000;
|
|
9
14
|
|
|
10
15
|
// Skeleton-loader bar over the agent-authored range, plus a Rovo AI telepointer at the end.
|
|
11
16
|
export const AGENT_SHIMMER_CLASS = 'collab-agent-shimmer';
|
|
17
|
+
// Purple "just edited" highlight shown over the range after the skeleton clears — same style as the
|
|
18
|
+
// editor AI "improve writing" in-editor highlight (subtle purple background + dashed purple underline).
|
|
19
|
+
export const AGENT_EDIT_HIGHLIGHT_CLASS = 'collab-agent-edit-highlight';
|
|
12
20
|
export const ROVO_AGENT_TELEPOINTER_CLASS = 'ai-in-editor-telepointer';
|
|
13
21
|
export const ROVO_AGENT_TELEPOINTER_LABEL_CLASS = 'ai-in-editor-telepointer-label';
|
|
14
22
|
export const ADD_AGENT_SHIMMER_META = 'addAgentShimmer'; // register the shimmer decorations
|
|
23
|
+
export const HIGHLIGHT_AGENT_SHIMMER_META = 'highlightAgentShimmer'; // skeleton → purple highlight phase
|
|
15
24
|
export const REMOVE_AGENT_SHIMMER_META = 'removeAgentShimmer'; // remove them once the shimmer ends
|
|
16
25
|
|
|
17
|
-
// A
|
|
18
|
-
//
|
|
19
|
-
|
|
26
|
+
// A shimmer runs in two phases: the `skeleton` loader, then (optionally) the purple `highlight` over
|
|
27
|
+
// the revealed content, before removal.
|
|
28
|
+
|
|
29
|
+
// A range an agent step wrote; the shimmer decorations are drawn over `from`..`to` and kept until
|
|
30
|
+
// removal (so positions can be re-mapped). Pure data only. `phase` selects skeleton vs purple
|
|
31
|
+
// highlight. `telepointerLabel` is the label for the trailing agent telepointer (shown through both
|
|
32
|
+
// phases); when absent, no telepointer is shown. `highlightDurationMs` is the purple-highlight
|
|
33
|
+
// lifetime, used to size its ease in/out animation so it matches the removal timer.
|
|
20
34
|
|
|
21
35
|
// Rovo AI in-editor telepointer/cursor shown at the end of an agent-authored range (same DOM/style
|
|
22
36
|
// pattern as editor-plugin-ai's in-editor direct-streaming telepointer).
|
|
@@ -38,7 +52,7 @@ const createRovoAgentTelepointer = label => {
|
|
|
38
52
|
* supersedes any still-in-flight shimmer), and drops a range when its removal timer fires. Returns a
|
|
39
53
|
* fresh array (never mutates in place) plus whether anything changed.
|
|
40
54
|
*/
|
|
41
|
-
export const reduceAgentShimmers = (current, tr, added, removedShimmerId) => {
|
|
55
|
+
export const reduceAgentShimmers = (current, tr, added, removedShimmerId, highlightShimmerId) => {
|
|
42
56
|
let next = current;
|
|
43
57
|
let changed = false;
|
|
44
58
|
|
|
@@ -58,6 +72,16 @@ export const reduceAgentShimmers = (current, tr, added, removedShimmerId) => {
|
|
|
58
72
|
}));
|
|
59
73
|
changed = true;
|
|
60
74
|
}
|
|
75
|
+
// Transition a shimmer from the skeleton phase to the purple highlight phase (skeleton timer fired).
|
|
76
|
+
// Guard on an actual match so a timer firing after the shimmer was already removed (superseded by a
|
|
77
|
+
// new batch, or cleared) doesn't rebuild the array and trigger a pointless decoration rebuild.
|
|
78
|
+
if (highlightShimmerId && next.some(shimmer => shimmer.shimmerId === highlightShimmerId)) {
|
|
79
|
+
next = next.map(shimmer => shimmer.shimmerId === highlightShimmerId ? {
|
|
80
|
+
...shimmer,
|
|
81
|
+
phase: 'highlight'
|
|
82
|
+
} : shimmer);
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
61
85
|
if (removedShimmerId) {
|
|
62
86
|
next = next.filter(shimmer => shimmer.shimmerId !== removedShimmerId);
|
|
63
87
|
changed = true;
|
|
@@ -69,34 +93,46 @@ export const reduceAgentShimmers = (current, tr, added, removedShimmerId) => {
|
|
|
69
93
|
};
|
|
70
94
|
|
|
71
95
|
/**
|
|
72
|
-
* Builds the inline skeleton
|
|
73
|
-
* `getValidPos` clamps a raw position to a valid
|
|
74
|
-
* One bad range is isolated via `onError` so it can't
|
|
96
|
+
* Builds the inline decorations (skeleton bar or purple highlight, per phase) plus the trailing
|
|
97
|
+
* telepointer for the active shimmer ranges. `getValidPos` clamps a raw position to a valid
|
|
98
|
+
* decoration position (owned by `plugin-state`). One bad range is isolated via `onError` so it can't
|
|
99
|
+
* kill the others.
|
|
75
100
|
*/
|
|
76
101
|
export const buildAgentShimmerDecorations = (tr, shimmers, getValidPos, onError) => {
|
|
77
102
|
const decorations = [];
|
|
78
|
-
const docEnd = tr.doc.nodeSize - 2;
|
|
79
103
|
shimmers.forEach(({
|
|
80
104
|
shimmerId,
|
|
81
105
|
from,
|
|
82
106
|
to,
|
|
83
|
-
telepointerLabel
|
|
107
|
+
telepointerLabel,
|
|
108
|
+
phase,
|
|
109
|
+
highlightDurationMs
|
|
84
110
|
}) => {
|
|
85
111
|
try {
|
|
112
|
+
// `getValidPos` already clamps to the last valid position, so only the lower bound needs
|
|
113
|
+
// guarding here (a raw `from < 1` would throw in `doc.resolve`).
|
|
86
114
|
const validFrom = getValidPos(tr, Math.max(from, 1));
|
|
87
|
-
const validTo = getValidPos(tr,
|
|
115
|
+
const validTo = getValidPos(tr, to);
|
|
88
116
|
if (validTo <= validFrom) {
|
|
89
117
|
return;
|
|
90
118
|
}
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
// Inline decoration over the whole range: the grey skeleton loader, or (once revealed) the
|
|
120
|
+
// purple "just edited" highlight. The highlight eases in and out over its lifetime via a CSS
|
|
121
|
+
// animation whose duration is set inline so it matches the removal timer.
|
|
122
|
+
const isHighlight = phase === 'highlight';
|
|
123
|
+
const inlineAttrs = {
|
|
124
|
+
class: isHighlight ? AGENT_EDIT_HIGHLIGHT_CLASS : AGENT_SHIMMER_CLASS
|
|
125
|
+
};
|
|
126
|
+
if (isHighlight && highlightDurationMs > 0) {
|
|
127
|
+
inlineAttrs.style = `animation-duration: ${highlightDurationMs}ms`;
|
|
128
|
+
}
|
|
129
|
+
decorations.push(Decoration.inline(validFrom, validTo, inlineAttrs, {
|
|
95
130
|
isAgentShimmer: true,
|
|
96
131
|
shimmerId
|
|
97
132
|
}));
|
|
98
|
-
//
|
|
99
|
-
//
|
|
133
|
+
// Rovo AI telepointer/cursor (labelled with the agent's type) at the end of the range, shown
|
|
134
|
+
// through BOTH the skeleton and purple-highlight phases so the agent's cursor stays put until
|
|
135
|
+
// the edit is fully revealed.
|
|
100
136
|
if (telepointerLabel) {
|
|
101
137
|
decorations.push(Decoration.widget(validTo, createRovoAgentTelepointer(telepointerLabel), {
|
|
102
138
|
isAgentShimmer: true,
|
|
@@ -2,8 +2,9 @@ import { getCollabState } from '@atlaskit/prosemirror-collab';
|
|
|
2
2
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
3
3
|
// When an agent step lands we cover the top-level block(s) it wrote with a skeleton-loader shimmer
|
|
4
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
|
|
6
|
-
//
|
|
5
|
+
// content. Gated behind the `platform_editor_agent_be_streaming` experiment. `shimmerDurationMs` and
|
|
6
|
+
// `highlightDurationMs` size the skeleton and purple-highlight phases and toggle independently: `0` on
|
|
7
|
+
// either skips that phase, `0` on both shows nothing.
|
|
7
8
|
let agentShimmerIdCounter = 0;
|
|
8
9
|
|
|
9
10
|
// A step is position-neutral when its StepMap changes no range's length — i.e. it shifts no
|
|
@@ -20,6 +21,22 @@ export const isPositionNeutralStep = step => {
|
|
|
20
21
|
return neutral;
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
// Top-level block/position helpers over a doc. Kept at module scope (rather than re-created as
|
|
25
|
+
// closures on every call) so they are defined once, reusable, and unit-testable. Each clamps into
|
|
26
|
+
// valid document coordinates before resolving.
|
|
27
|
+
const clampToDoc = (doc, pos) => Math.min(Math.max(pos, 1), doc.content.size);
|
|
28
|
+
const topLevelBlockIndexAt = (doc, pos) => doc.resolve(clampToDoc(doc, pos)).index(0);
|
|
29
|
+
// Start of the content of the top-level block containing `pos`, so the whole block is covered.
|
|
30
|
+
const topLevelBlockContentStart = (doc, pos) => {
|
|
31
|
+
const $pos = doc.resolve(clampToDoc(doc, pos));
|
|
32
|
+
return $pos.depth >= 1 ? $pos.start(1) : pos;
|
|
33
|
+
};
|
|
34
|
+
// End of the content of the top-level block containing `pos`, so the whole block is covered.
|
|
35
|
+
const topLevelBlockContentEnd = (doc, pos) => {
|
|
36
|
+
const $pos = doc.resolve(clampToDoc(doc, pos));
|
|
37
|
+
return $pos.depth >= 1 ? $pos.end(1) : pos;
|
|
38
|
+
};
|
|
39
|
+
|
|
23
40
|
/**
|
|
24
41
|
* Derive the shimmer ranges for the agent-authored steps in a received batch, in final-doc
|
|
25
42
|
* coordinates. `agentType` present ⇒ agent-authored (per the NCS↔Editor steps contract). Each
|
|
@@ -36,21 +53,30 @@ export const isPositionNeutralStep = step => {
|
|
|
36
53
|
* rebased local step changed sizes (a rare, safe degrade). Any unexpected error also degrades to no
|
|
37
54
|
* shimmer, so this never throws into the shared remote-step handler.
|
|
38
55
|
*/
|
|
39
|
-
export const getAgentShimmerRanges = (json, steps, tr, view,
|
|
56
|
+
export const getAgentShimmerRanges = (json, steps, tr, view, shimmerDurationMs, highlightDurationMs, telepointerEnabled, onNotShown) => {
|
|
40
57
|
var _json$find;
|
|
41
58
|
if (!expValEquals('platform_editor_agent_be_streaming', 'isEnabled', true)) {
|
|
42
59
|
return [];
|
|
43
60
|
}
|
|
44
|
-
//
|
|
45
|
-
|
|
61
|
+
// The two phases toggle independently: `shimmerDurationMs` sizes the skeleton, `highlightDurationMs`
|
|
62
|
+
// sizes the purple highlight, and `0` on either skips just that phase. Nothing to reveal only when
|
|
63
|
+
// both are off.
|
|
64
|
+
if (shimmerDurationMs <= 0 && highlightDurationMs <= 0) {
|
|
46
65
|
return [];
|
|
47
66
|
}
|
|
67
|
+
// Start in the skeleton phase when it's enabled, otherwise straight into the purple highlight phase
|
|
68
|
+
// (skeleton toggled off, highlight on).
|
|
69
|
+
const initialPhase = shimmerDurationMs > 0 ? 'skeleton' : 'highlight';
|
|
48
70
|
// Telepointer label = the agent's type upper-cased (e.g. `mcp` → "MCP"), falling back to a generic
|
|
49
71
|
// "Agent"; `undefined` when the telepointer is disabled, so the plugin skips it. `agentType` is the
|
|
50
72
|
// same on every step of an agent batch, so read it from the first agent-authored step.
|
|
51
73
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
74
|
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
|
-
|
|
75
|
+
// No agent-authored steps in this batch — nothing to shimmer, and not a "not shown" case.
|
|
76
|
+
if (agentType === undefined) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const telepointerLabel = telepointerEnabled ? agentType.toUpperCase() || 'Agent' : undefined;
|
|
54
80
|
// When the batch was rebased over local unconfirmed steps, our index-based range math is only
|
|
55
81
|
// valid if those local steps shifted no positions. Attribute-only steps (e.g. `localId`) and
|
|
56
82
|
// same-size replacements are position-neutral, so the shimmer stays correct. Skip only when a
|
|
@@ -59,6 +85,7 @@ export const getAgentShimmerRanges = (json, steps, tr, view, durationMs, telepoi
|
|
|
59
85
|
var _getCollabState$uncon, _getCollabState;
|
|
60
86
|
const unconfirmed = (_getCollabState$uncon = (_getCollabState = getCollabState(view.state)) === null || _getCollabState === void 0 ? void 0 : _getCollabState.unconfirmed) !== null && _getCollabState$uncon !== void 0 ? _getCollabState$uncon : [];
|
|
61
87
|
if (unconfirmed.some(entry => !isPositionNeutralStep(entry.step))) {
|
|
88
|
+
onNotShown === null || onNotShown === void 0 ? void 0 : onNotShown('rebasedConcurrentEdit', agentType);
|
|
62
89
|
return [];
|
|
63
90
|
}
|
|
64
91
|
}
|
|
@@ -98,6 +125,7 @@ export const getAgentShimmerRanges = (json, steps, tr, view, durationMs, telepoi
|
|
|
98
125
|
});
|
|
99
126
|
});
|
|
100
127
|
if (!infos.length) {
|
|
128
|
+
onNotShown === null || onNotShown === void 0 ? void 0 : onNotShown('nothingToShow', agentType);
|
|
101
129
|
return [];
|
|
102
130
|
}
|
|
103
131
|
|
|
@@ -109,22 +137,10 @@ export const getAgentShimmerRanges = (json, steps, tr, view, durationMs, telepoi
|
|
|
109
137
|
// touched blocks (index n and n+1) merge into one group so a multi-block rewrite reveals together;
|
|
110
138
|
// a genuinely untouched block in between (index gap > 1) splits the run, so far-apart edits stay
|
|
111
139
|
// 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
140
|
const sorted = [...infos].sort((a, b) => a.from - b.from || a.to - b.to);
|
|
125
141
|
const groups = [];
|
|
126
142
|
sorted.forEach(info => {
|
|
127
|
-
const block =
|
|
143
|
+
const block = topLevelBlockIndexAt(tr.doc, info.from);
|
|
128
144
|
const current = groups[groups.length - 1];
|
|
129
145
|
if (current && block <= current.maxBlock + 1) {
|
|
130
146
|
current.to = Math.max(current.to, info.to);
|
|
@@ -139,13 +155,16 @@ export const getAgentShimmerRanges = (json, steps, tr, view, durationMs, telepoi
|
|
|
139
155
|
});
|
|
140
156
|
return groups.map(group => ({
|
|
141
157
|
shimmerId: `agent-shimmer-${agentShimmerIdCounter++}`,
|
|
142
|
-
from:
|
|
143
|
-
to:
|
|
144
|
-
telepointerLabel
|
|
158
|
+
from: topLevelBlockContentStart(tr.doc, group.from),
|
|
159
|
+
to: topLevelBlockContentEnd(tr.doc, group.to),
|
|
160
|
+
telepointerLabel,
|
|
161
|
+
phase: initialPhase,
|
|
162
|
+
highlightDurationMs
|
|
145
163
|
}));
|
|
146
|
-
} catch {
|
|
164
|
+
} catch (err) {
|
|
147
165
|
// Never let shimmer range derivation throw into the shared remote-step handler; degrade to no
|
|
148
166
|
// shimmer.
|
|
167
|
+
onNotShown === null || onNotShown === void 0 ? void 0 : onNotShown('captureThrew', agentType, err);
|
|
149
168
|
return [];
|
|
150
169
|
}
|
|
151
170
|
};
|
|
@@ -5,7 +5,7 @@ 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';
|
|
8
|
+
import { ADD_AGENT_SHIMMER_META, buildAgentShimmerDecorations, HIGHLIGHT_AGENT_SHIMMER_META, reduceAgentShimmers, REMOVE_AGENT_SHIMMER_META } from './agent-shimmer-decorations';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Returns position where it's possible to place a decoration.
|
|
@@ -69,6 +69,7 @@ export class PluginState {
|
|
|
69
69
|
const nudgeTelepointerData = tr.getMeta('nudgeTelepointer');
|
|
70
70
|
const agentShimmerData = tr.getMeta(ADD_AGENT_SHIMMER_META);
|
|
71
71
|
const removeAgentShimmerId = tr.getMeta(REMOVE_AGENT_SHIMMER_META);
|
|
72
|
+
const highlightAgentShimmerId = tr.getMeta(HIGHLIGHT_AGENT_SHIMMER_META);
|
|
72
73
|
const sessionIdData = tr.getMeta('sessionId');
|
|
73
74
|
let collabInitialised = tr.getMeta('collabInitialised');
|
|
74
75
|
if (typeof collabInitialised !== 'boolean') {
|
|
@@ -224,7 +225,7 @@ export class PluginState {
|
|
|
224
225
|
const {
|
|
225
226
|
changed,
|
|
226
227
|
next
|
|
227
|
-
} = reduceAgentShimmers(this.agentShimmers, tr, agentShimmerData, removeAgentShimmerId);
|
|
228
|
+
} = reduceAgentShimmers(this.agentShimmers, tr, agentShimmerData, removeAgentShimmerId, highlightAgentShimmerId);
|
|
228
229
|
if (changed) {
|
|
229
230
|
// Build into local arrays; only merge into the shared add/remove once the block succeeds.
|
|
230
231
|
const agentRemove = this.decorationSet.find(undefined, undefined, spec => spec.isAgentShimmer);
|
|
@@ -9,7 +9,8 @@ import { Step } from '@atlaskit/editor-prosemirror/transform';
|
|
|
9
9
|
import { receiveTransaction } from '@atlaskit/prosemirror-collab';
|
|
10
10
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
11
11
|
import { expVal } from '@atlaskit/tmp-editor-statsig/expVal';
|
|
12
|
-
import {
|
|
12
|
+
import { getAgentEditShimmerNotShownPayload } from './analytics';
|
|
13
|
+
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';
|
|
13
14
|
import { getAgentShimmerRanges } from './main/agent-shimmer-ranges';
|
|
14
15
|
import { replaceDocument } from './utils';
|
|
15
16
|
|
|
@@ -31,7 +32,7 @@ export var handleInit = function handleInit(initData, view, options, editorAnaly
|
|
|
31
32
|
tr.setMeta('isRemote', true);
|
|
32
33
|
view.dispatch(tr);
|
|
33
34
|
} else if (json) {
|
|
34
|
-
applyRemoteSteps(json, view);
|
|
35
|
+
applyRemoteSteps(json, view, undefined, options, editorAnalyticsApi);
|
|
35
36
|
}
|
|
36
37
|
};
|
|
37
38
|
export var handleConnection = function handleConnection(connectionData, view) {
|
|
@@ -42,15 +43,15 @@ export var handlePresence = function handlePresence(presenceData, view) {
|
|
|
42
43
|
var tr = view.state.tr;
|
|
43
44
|
view.dispatch(tr.setMeta('presence', presenceData));
|
|
44
45
|
};
|
|
45
|
-
export var applyRemoteData = function applyRemoteData(remoteData, view, options) {
|
|
46
|
+
export var applyRemoteData = function applyRemoteData(remoteData, view, options, editorAnalyticsApi) {
|
|
46
47
|
var json = remoteData.json,
|
|
47
48
|
_remoteData$userIds = remoteData.userIds,
|
|
48
49
|
userIds = _remoteData$userIds === void 0 ? [] : _remoteData$userIds;
|
|
49
50
|
if (json) {
|
|
50
|
-
applyRemoteSteps(json, view, userIds, options);
|
|
51
|
+
applyRemoteSteps(json, view, userIds, options, editorAnalyticsApi);
|
|
51
52
|
}
|
|
52
53
|
};
|
|
53
|
-
export var applyRemoteSteps = function applyRemoteSteps(json, view, userIds, options) {
|
|
54
|
+
export var applyRemoteSteps = function applyRemoteSteps(json, view, userIds, options, editorAnalyticsApi) {
|
|
54
55
|
if (!json || !json.length) {
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
@@ -75,16 +76,28 @@ export var applyRemoteSteps = function applyRemoteSteps(json, view, userIds, opt
|
|
|
75
76
|
tr.setMeta('isRemote', true);
|
|
76
77
|
|
|
77
78
|
// Agent edit shimmer: mark the ranges agent steps just wrote so the plugin reveals them with the
|
|
78
|
-
//
|
|
79
|
-
// off-experiment; off-path leaves `agentShimmers` empty and everything below
|
|
80
|
-
|
|
79
|
+
// skeleton loader, then (optionally) a purple "just edited" highlight. Gated as a whole so no
|
|
80
|
+
// experiment reads run off-experiment; off-path leaves `agentShimmers` empty and everything below
|
|
81
|
+
// is a no-op.
|
|
82
|
+
var shimmerDurationMs = 0;
|
|
83
|
+
var highlightDurationMs = 0;
|
|
81
84
|
var agentShimmers = [];
|
|
82
85
|
if (expValEquals('platform_editor_agent_be_streaming', 'isEnabled', true)) {
|
|
83
|
-
|
|
86
|
+
// The skeleton and purple-highlight phases toggle independently. `shimmerDurationMs` is the
|
|
87
|
+
// skeleton lifetime; `0` skips the skeleton (an edit can still get the purple highlight).
|
|
88
|
+
shimmerDurationMs = expVal('platform_editor_agent_be_streaming', 'shimmerDurationMs', AGENT_SHIMMER_DEFAULT_DURATION_MS);
|
|
89
|
+
// `highlightDurationMs` is the purple "just edited" highlight lifetime; `0` skips the highlight
|
|
90
|
+
// (the skeleton still shows). `0` on both shows nothing.
|
|
91
|
+
highlightDurationMs = expVal('platform_editor_agent_be_streaming', 'highlightDurationMs', AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS);
|
|
84
92
|
// Telepointer shown by default; `telepointerDisabled` hides it (inverted because `expVal`
|
|
85
93
|
// only permits `false` as a boolean default).
|
|
86
94
|
var telepointerEnabled = !expVal('platform_editor_agent_be_streaming', 'telepointerDisabled', false);
|
|
87
|
-
agentShimmers = getAgentShimmerRanges(json, steps, tr, view,
|
|
95
|
+
agentShimmers = getAgentShimmerRanges(json, steps, tr, view, shimmerDurationMs, highlightDurationMs, telepointerEnabled,
|
|
96
|
+
// An agent edit that applied without the shimmer fires the operational event on the same
|
|
97
|
+
// transaction, so success stays event-free and only degrades are reported.
|
|
98
|
+
function (reason, agentType, error) {
|
|
99
|
+
return editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(getAgentEditShimmerNotShownPayload(reason, agentType, error))(tr);
|
|
100
|
+
});
|
|
88
101
|
if (agentShimmers.length) {
|
|
89
102
|
tr.setMeta(ADD_AGENT_SHIMMER_META, agentShimmers);
|
|
90
103
|
}
|
|
@@ -100,22 +113,45 @@ export var applyRemoteSteps = function applyRemoteSteps(json, view, userIds, opt
|
|
|
100
113
|
}
|
|
101
114
|
view.dispatch(tr);
|
|
102
115
|
|
|
103
|
-
//
|
|
116
|
+
// Schedule each shimmer's phase changes. `dispatchMeta` is guarded so a timer firing after
|
|
104
117
|
// teardown is a harmless no-op. Only reached when the gated block above produced ranges, so this
|
|
105
118
|
// whole path is off-experiment-safe.
|
|
106
119
|
if (agentShimmers.length) {
|
|
120
|
+
var tornDownReported = false;
|
|
107
121
|
var dispatchMeta = function dispatchMeta(metaKey, value) {
|
|
108
122
|
try {
|
|
109
123
|
view.dispatch(view.state.tr.setMeta(metaKey, value));
|
|
110
124
|
} catch (_unused) {
|
|
111
|
-
// View torn down before the timer fired — nothing to clean up.
|
|
125
|
+
// View torn down before the timer fired — nothing to clean up. Report once (not once
|
|
126
|
+
// per shimmer) so we can see how often shimmers are interrupted by teardown.
|
|
127
|
+
if (!tornDownReported) {
|
|
128
|
+
tornDownReported = true;
|
|
129
|
+
editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.fireAnalyticsEvent(getAgentEditShimmerNotShownPayload('tornDownMidAnimation'));
|
|
130
|
+
}
|
|
112
131
|
}
|
|
113
132
|
};
|
|
114
133
|
agentShimmers.forEach(function (_ref) {
|
|
115
134
|
var shimmerId = _ref.shimmerId;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
if (shimmerDurationMs > 0) {
|
|
136
|
+
// Skeleton first. When it clears, show the purple highlight (if enabled) then remove;
|
|
137
|
+
// otherwise remove straight away.
|
|
138
|
+
setTimeout(function () {
|
|
139
|
+
if (highlightDurationMs > 0) {
|
|
140
|
+
dispatchMeta(HIGHLIGHT_AGENT_SHIMMER_META, shimmerId);
|
|
141
|
+
setTimeout(function () {
|
|
142
|
+
dispatchMeta(REMOVE_AGENT_SHIMMER_META, shimmerId);
|
|
143
|
+
}, highlightDurationMs);
|
|
144
|
+
} else {
|
|
145
|
+
dispatchMeta(REMOVE_AGENT_SHIMMER_META, shimmerId);
|
|
146
|
+
}
|
|
147
|
+
}, shimmerDurationMs);
|
|
148
|
+
} else {
|
|
149
|
+
// Skeleton disabled: the range already starts in the highlight phase, so just time its
|
|
150
|
+
// removal. (Reached only when `highlightDurationMs > 0`, else no ranges were produced.)
|
|
151
|
+
setTimeout(function () {
|
|
152
|
+
dispatchMeta(REMOVE_AGENT_SHIMMER_META, shimmerId);
|
|
153
|
+
}, highlightDurationMs);
|
|
154
|
+
}
|
|
119
155
|
});
|
|
120
156
|
}
|
|
121
157
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
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; }
|
|
1
4
|
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
2
5
|
import { getDocStructure } from '@atlaskit/editor-common/core-utils';
|
|
3
6
|
import { sniffUserBrowserExtensions } from '@atlaskit/editor-common/utils';
|
|
@@ -26,6 +29,27 @@ export var addSynchronyErrorAnalytics = function addSynchronyErrorAnalytics(stat
|
|
|
26
29
|
return tr;
|
|
27
30
|
};
|
|
28
31
|
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Builds the `agentEditShimmerNotShown` operational event fired when an agent-authored edit applied
|
|
35
|
+
* instantly without the skeleton shimmer/telepointer. Success is the ABSENCE of this event; a
|
|
36
|
+
* neutral action + `reason` keeps expected degrades out of error dashboards. Attributes are non-PII:
|
|
37
|
+
* only the agent kind and a sanitised error name/message (never the raw Error or any doc content).
|
|
38
|
+
*/
|
|
39
|
+
export var getAgentEditShimmerNotShownPayload = function getAgentEditShimmerNotShownPayload(reason, agentType, error) {
|
|
40
|
+
return {
|
|
41
|
+
action: ACTION.AGENT_EDIT_SHIMMER_NOT_SHOWN,
|
|
42
|
+
actionSubject: ACTION_SUBJECT.COLLAB,
|
|
43
|
+
eventType: EVENT_TYPE.OPERATIONAL,
|
|
44
|
+
attributes: _objectSpread(_objectSpread({
|
|
45
|
+
reason: reason
|
|
46
|
+
}, agentType ? {
|
|
47
|
+
agentType: agentType
|
|
48
|
+
} : {}), error ? {
|
|
49
|
+
error: "".concat(error.name, ": ").concat(error.message)
|
|
50
|
+
} : {})
|
|
51
|
+
};
|
|
52
|
+
};
|
|
29
53
|
export var addSynchronyEntityAnalytics = function addSynchronyEntityAnalytics(state, tr) {
|
|
30
54
|
return function (type, editorAnalyticsApi) {
|
|
31
55
|
editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent({
|
|
@@ -50,7 +50,7 @@ export var subscribe = effect(function (view, provider, options, featureFlags, _
|
|
|
50
50
|
return handleConnection(data, view);
|
|
51
51
|
},
|
|
52
52
|
dataHandler: function dataHandler(data) {
|
|
53
|
-
return applyRemoteData(data, view, options);
|
|
53
|
+
return applyRemoteData(data, view, options, editorAnalyticsApi);
|
|
54
54
|
},
|
|
55
55
|
presenceHandler: function presenceHandler(data) {
|
|
56
56
|
return handlePresence(data, view);
|