@atlaskit/editor-plugin-collab-edit 13.0.30 → 13.1.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 +25 -0
- package/dist/cjs/pm-plugins/actions.js +39 -0
- package/dist/cjs/pm-plugins/main/agent-shimmer-decorations.js +126 -0
- package/dist/cjs/pm-plugins/main/agent-shimmer-ranges.js +169 -0
- package/dist/cjs/pm-plugins/main/plugin-state.js +54 -3
- package/dist/cjs/pm-plugins/utils.js +6 -2
- package/dist/es2019/pm-plugins/actions.js +40 -0
- package/dist/es2019/pm-plugins/main/agent-shimmer-decorations.js +115 -0
- package/dist/es2019/pm-plugins/main/agent-shimmer-ranges.js +151 -0
- package/dist/es2019/pm-plugins/main/plugin-state.js +52 -4
- package/dist/es2019/pm-plugins/utils.js +7 -1
- package/dist/esm/pm-plugins/actions.js +39 -0
- package/dist/esm/pm-plugins/main/agent-shimmer-decorations.js +120 -0
- package/dist/esm/pm-plugins/main/agent-shimmer-ranges.js +163 -0
- package/dist/esm/pm-plugins/main/plugin-state.js +55 -3
- package/dist/esm/pm-plugins/utils.js +6 -2
- package/dist/types/pm-plugins/main/agent-shimmer-decorations.d.ts +31 -0
- package/dist/types/pm-plugins/main/agent-shimmer-ranges.d.ts +22 -0
- package/dist/types/pm-plugins/main/plugin-state.d.ts +3 -1
- package/package.json +7 -4
|
@@ -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$
|
|
191
|
-
|
|
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(
|
|
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
|
+
};
|