@atlaskit/editor-plugin-autocomplete 4.0.3 → 4.0.4

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,20 @@
1
1
  # @atlaskit/editor-plugin-autocomplete
2
2
 
3
+ ## 4.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`ac12ed0b41ef4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ac12ed0b41ef4) -
8
+ Refactor contextual autocomplete wiring so the conversation-store-aware ChatInput owns the
9
+ autocomplete context bridge (getContext + subscribeToContextUpdates) via a new
10
+ useAutocompleteEditorContext hook, and the shared RovoChatPromptInput simply forwards a single
11
+ `autocomplete` prop to the editor preset. Removes the duplicated keying / listener-set plumbing
12
+ from the reusable input.
13
+
14
+ Adds an optional `subscribeToContextUpdates` option to the autocomplete plugin so host surfaces
15
+ that stream context after mount (e.g. Rovo chat messages) can push refreshes, complementing the
16
+ existing word-boundary retry that only covers a one-time, still-loading comment thread.
17
+
3
18
  ## 4.0.3
4
19
 
5
20
  ### Patch Changes
@@ -249,6 +249,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
249
249
  * user has already typed several words before the promise resolved.
250
250
  */
251
251
  var currentView = null;
252
+ var unsubscribeFromContextUpdates;
252
253
  /**
253
254
  * Set after accepting a suggestion so the next doc-change update
254
255
  * skips scheduling a new prediction for the just-inserted text.
@@ -670,7 +671,21 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
670
671
  }
671
672
  }
672
673
  },
673
- view: function view() {
674
+ view: function view(editorView) {
675
+ // Capture up front so a subscription notification before the first PM
676
+ // transaction can still drive slowLaneClient.updateContext (gated on currentView).
677
+ currentView = editorView;
678
+
679
+ // Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
680
+ if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
681
+ unsubscribeFromContextUpdates = options.subscribeToContextUpdates(function () {
682
+ // Bypass throttle for freshness; the in-flight guard prevents overlap.
683
+ refreshContext({
684
+ source: 'subscription',
685
+ allowThrottle: false
686
+ });
687
+ });
688
+ }
674
689
  return {
675
690
  update: function update(view, prevState) {
676
691
  if (!isAutocompleteEnabled) {
@@ -699,7 +714,9 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
699
714
  // thread still loading). Retry on word boundaries until we have
700
715
  // the parent comment, throttled so we don't refetch constantly
701
716
  // and capped so non-comment editors stop retrying entirely.
702
- if (!((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
717
+ // Skipped for push-channel hosts (subscribeToContextUpdates): they
718
+ // never grow parentCommentContent, so polling only burns the budget.
719
+ if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
703
720
  // Only count the attempt when a fetch actually started, so an
704
721
  // in-flight or throttled no-op doesn't burn the retry budget.
705
722
  if (refreshContext({
@@ -713,8 +730,11 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
713
730
  }
714
731
  },
715
732
  destroy: function destroy() {
733
+ var _unsubscribeFromConte;
716
734
  destroyed = true;
717
735
  currentView = null;
736
+ (_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 || _unsubscribeFromConte();
737
+ unsubscribeFromContextUpdates = undefined;
718
738
  if (debounceTimer) {
719
739
  clearTimeout(debounceTimer);
720
740
  }
@@ -264,7 +264,7 @@ var bePayloadDataPromise;
264
264
  * :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
265
265
  * :returns: The parsed JSON value, or `null` if neither interop mode applies.
266
266
  */
267
- var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
267
+ function unwrapJsonModule(mod, shape) {
268
268
  if (mod == null || (0, _typeof2.default)(mod) !== 'object') {
269
269
  return null;
270
270
  }
@@ -316,7 +316,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
316
316
  return namespace.default;
317
317
  }
318
318
  return null;
319
- };
319
+ }
320
320
 
321
321
  /**
322
322
  * Lazily load and build the BE-parity lookup tables from their JSON payloads.
@@ -964,6 +964,12 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
964
964
  return _regenerator.default.wrap(function (_context4) {
965
965
  while (1) switch (_context4.prev = _context4.next) {
966
966
  case 0:
967
+ captureCompletionTime = function _captureCompletionTim(promise, onResolved) {
968
+ return promise.then(function (value) {
969
+ onResolved(performance.now());
970
+ return value;
971
+ });
972
+ };
967
973
  if (!(!engine || destroyed)) {
968
974
  _context4.next = 1;
969
975
  break;
@@ -983,12 +989,6 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
983
989
  lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
984
990
  semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
985
991
  arcticInput = wrapForArctic(semanticText);
986
- captureCompletionTime = function captureCompletionTime(promise, onResolved) {
987
- return promise.then(function (value) {
988
- onResolved(performance.now());
989
- return value;
990
- });
991
- };
992
992
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
993
993
  // eslint-disable-next-line no-console
994
994
  console.log("%c[LocalSlowLane] %c\uD83D\uDD22 Arctic input (".concat(arcticInput.length, " chars, ").concat(splitOnWhitespace(semanticText).length, " words): \"").concat(arcticInput.length > 100 ? "".concat(arcticInput.slice(0, 100), "\u2026") : arcticInput, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #009688;');
@@ -694,7 +694,7 @@ var predict = exports.predict = function predict(textBefore) {
694
694
  * array and a sparse numeric-keyed object are emitted identically as named
695
695
  * exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
696
696
  */
697
- var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
697
+ function unwrapJsonModule(mod, shape) {
698
698
  if (mod == null || (0, _typeof2.default)(mod) !== 'object') {
699
699
  return null;
700
700
  }
@@ -730,7 +730,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
730
730
  return namespace.default;
731
731
  }
732
732
  return null;
733
- };
733
+ }
734
734
  var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
735
735
  var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
736
736
  var _options$isLocalLLM;
@@ -237,6 +237,7 @@ export const createAutocompletePlugin = (options, api) => {
237
237
  * user has already typed several words before the promise resolved.
238
238
  */
239
239
  let currentView = null;
240
+ let unsubscribeFromContextUpdates;
240
241
  /**
241
242
  * Set after accepting a suggestion so the next doc-change update
242
243
  * skips scheduling a new prediction for the just-inserted text.
@@ -657,59 +658,80 @@ export const createAutocompletePlugin = (options, api) => {
657
658
  }
658
659
  }
659
660
  },
660
- view: () => ({
661
- update: (view, prevState) => {
662
- if (!isAutocompleteEnabled) {
663
- return;
664
- }
665
- currentView = view;
666
- if (!prevState.doc.eq(view.state.doc)) {
667
- if (justAccepted) {
668
- justAccepted = false;
661
+ view: editorView => {
662
+ // Capture up front so a subscription notification before the first PM
663
+ // transaction can still drive slowLaneClient.updateContext (gated on currentView).
664
+ currentView = editorView;
669
665
 
670
- // Snapshot the post-acceptance text so follow-up transactions hit
671
- // the dismissedContext guard and abort until the user types again.
672
- dismissedContext = getTextBeforeCursor(view.state);
673
- if (debounceTimer) {
674
- clearTimeout(debounceTimer);
675
- }
666
+ // Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
667
+ if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
668
+ unsubscribeFromContextUpdates = options.subscribeToContextUpdates(() => {
669
+ // Bypass throttle for freshness; the in-flight guard prevents overlap.
670
+ refreshContext({
671
+ source: 'subscription',
672
+ allowThrottle: false
673
+ });
674
+ });
675
+ }
676
+ return {
677
+ update: (view, prevState) => {
678
+ if (!isAutocompleteEnabled) {
676
679
  return;
677
680
  }
678
- maybeUpdateSessionFrequency(view, prevState);
679
- const textBefore = getTextBeforeCursor(view.state);
680
- if (isWordBoundary(textBefore)) {
681
- var _resolvedContext;
682
- slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
681
+ currentView = view;
682
+ if (!prevState.doc.eq(view.state.doc)) {
683
+ if (justAccepted) {
684
+ justAccepted = false;
685
+
686
+ // Snapshot the post-acceptance text so follow-up transactions hit
687
+ // the dismissedContext guard and abort until the user types again.
688
+ dismissedContext = getTextBeforeCursor(view.state);
689
+ if (debounceTimer) {
690
+ clearTimeout(debounceTimer);
691
+ }
692
+ return;
693
+ }
694
+ maybeUpdateSessionFrequency(view, prevState);
695
+ const textBefore = getTextBeforeCursor(view.state);
696
+ if (isWordBoundary(textBefore)) {
697
+ var _resolvedContext;
698
+ slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
683
699
 
684
- // Context may not have resolved on first focus (e.g. comment
685
- // thread still loading). Retry on word boundaries until we have
686
- // the parent comment, throttled so we don't refetch constantly
687
- // and capped so non-comment editors stop retrying entirely.
688
- if (!((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
689
- // Only count the attempt when a fetch actually started, so an
690
- // in-flight or throttled no-op doesn't burn the retry budget.
691
- if (refreshContext({
692
- source: 'word-boundary'
693
- })) {
694
- wordBoundaryRefreshAttempts++;
700
+ // Context may not have resolved on first focus (e.g. comment
701
+ // thread still loading). Retry on word boundaries until we have
702
+ // the parent comment, throttled so we don't refetch constantly
703
+ // and capped so non-comment editors stop retrying entirely.
704
+ // Skipped for push-channel hosts (subscribeToContextUpdates): they
705
+ // never grow parentCommentContent, so polling only burns the budget.
706
+ if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
707
+ // Only count the attempt when a fetch actually started, so an
708
+ // in-flight or throttled no-op doesn't burn the retry budget.
709
+ if (refreshContext({
710
+ source: 'word-boundary'
711
+ })) {
712
+ wordBoundaryRefreshAttempts++;
713
+ }
695
714
  }
696
715
  }
716
+ schedulePrediction(view);
697
717
  }
698
- schedulePrediction(view);
699
- }
700
- },
701
- destroy: () => {
702
- destroyed = true;
703
- currentView = null;
704
- if (debounceTimer) {
705
- clearTimeout(debounceTimer);
706
- }
707
- if (hasDestroy(slowLaneClient)) {
708
- slowLaneClient.destroy();
718
+ },
719
+ destroy: () => {
720
+ var _unsubscribeFromConte;
721
+ destroyed = true;
722
+ currentView = null;
723
+ (_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 ? void 0 : _unsubscribeFromConte();
724
+ unsubscribeFromContextUpdates = undefined;
725
+ if (debounceTimer) {
726
+ clearTimeout(debounceTimer);
727
+ }
728
+ if (hasDestroy(slowLaneClient)) {
729
+ slowLaneClient.destroy();
730
+ }
731
+ ingestedContextTexts.clear();
732
+ setDefaultSlowLaneClient(null);
709
733
  }
710
- ingestedContextTexts.clear();
711
- setDefaultSlowLaneClient(null);
712
- }
713
- })
734
+ };
735
+ }
714
736
  });
715
737
  };
@@ -247,7 +247,7 @@ let bePayloadDataPromise;
247
247
  * :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
248
248
  * :returns: The parsed JSON value, or `null` if neither interop mode applies.
249
249
  */
250
- const unwrapJsonModule = (mod, shape) => {
250
+ function unwrapJsonModule(mod, shape) {
251
251
  if (mod == null || typeof mod !== 'object') {
252
252
  return null;
253
253
  }
@@ -288,7 +288,7 @@ const unwrapJsonModule = (mod, shape) => {
288
288
  return namespace.default;
289
289
  }
290
290
  return null;
291
- };
291
+ }
292
292
 
293
293
  /**
294
294
  * Lazily load and build the BE-parity lookup tables from their JSON payloads.
@@ -764,10 +764,12 @@ export const createLocalSlowLaneClient = (config = {}) => {
764
764
  const lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
765
765
  const semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
766
766
  const arcticInput = wrapForArctic(semanticText);
767
- const captureCompletionTime = (promise, onResolved) => promise.then(value => {
768
- onResolved(performance.now());
769
- return value;
770
- });
767
+ function captureCompletionTime(promise, onResolved) {
768
+ return promise.then(value => {
769
+ onResolved(performance.now());
770
+ return value;
771
+ });
772
+ }
771
773
  if (isAutocompleteDebugEnabled()) {
772
774
  // eslint-disable-next-line no-console
773
775
  console.log(`%c[LocalSlowLane] %c🔢 Arctic input (${arcticInput.length} chars, ${splitOnWhitespace(semanticText).length} words): "${arcticInput.length > 100 ? `${arcticInput.slice(0, 100)}…` : arcticInput}"`, 'color: #9c27b0; font-weight: bold;', 'color: #009688;');
@@ -560,7 +560,7 @@ export const predict = textBefore => {
560
560
  * array and a sparse numeric-keyed object are emitted identically as named
561
561
  * exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
562
562
  */
563
- const unwrapJsonModule = (mod, shape) => {
563
+ function unwrapJsonModule(mod, shape) {
564
564
  if (mod == null || typeof mod !== 'object') {
565
565
  return null;
566
566
  }
@@ -585,7 +585,7 @@ const unwrapJsonModule = (mod, shape) => {
585
585
  return namespace.default;
586
586
  }
587
587
  return null;
588
- };
588
+ }
589
589
  export const loadVectorsAsync = async options => {
590
590
  var _options$isLocalLLM;
591
591
  if (vectorStore || vectorsLoadStarted) {
@@ -242,6 +242,7 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
242
242
  * user has already typed several words before the promise resolved.
243
243
  */
244
244
  var currentView = null;
245
+ var unsubscribeFromContextUpdates;
245
246
  /**
246
247
  * Set after accepting a suggestion so the next doc-change update
247
248
  * skips scheduling a new prediction for the just-inserted text.
@@ -663,7 +664,21 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
663
664
  }
664
665
  }
665
666
  },
666
- view: function view() {
667
+ view: function view(editorView) {
668
+ // Capture up front so a subscription notification before the first PM
669
+ // transaction can still drive slowLaneClient.updateContext (gated on currentView).
670
+ currentView = editorView;
671
+
672
+ // Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
673
+ if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
674
+ unsubscribeFromContextUpdates = options.subscribeToContextUpdates(function () {
675
+ // Bypass throttle for freshness; the in-flight guard prevents overlap.
676
+ refreshContext({
677
+ source: 'subscription',
678
+ allowThrottle: false
679
+ });
680
+ });
681
+ }
667
682
  return {
668
683
  update: function update(view, prevState) {
669
684
  if (!isAutocompleteEnabled) {
@@ -692,7 +707,9 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
692
707
  // thread still loading). Retry on word boundaries until we have
693
708
  // the parent comment, throttled so we don't refetch constantly
694
709
  // and capped so non-comment editors stop retrying entirely.
695
- if (!((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
710
+ // Skipped for push-channel hosts (subscribeToContextUpdates): they
711
+ // never grow parentCommentContent, so polling only burns the budget.
712
+ if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
696
713
  // Only count the attempt when a fetch actually started, so an
697
714
  // in-flight or throttled no-op doesn't burn the retry budget.
698
715
  if (refreshContext({
@@ -706,8 +723,11 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
706
723
  }
707
724
  },
708
725
  destroy: function destroy() {
726
+ var _unsubscribeFromConte;
709
727
  destroyed = true;
710
728
  currentView = null;
729
+ (_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 || _unsubscribeFromConte();
730
+ unsubscribeFromContextUpdates = undefined;
711
731
  if (debounceTimer) {
712
732
  clearTimeout(debounceTimer);
713
733
  }
@@ -258,7 +258,7 @@ var bePayloadDataPromise;
258
258
  * :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
259
259
  * :returns: The parsed JSON value, or `null` if neither interop mode applies.
260
260
  */
261
- var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
261
+ function unwrapJsonModule(mod, shape) {
262
262
  if (mod == null || _typeof(mod) !== 'object') {
263
263
  return null;
264
264
  }
@@ -310,7 +310,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
310
310
  return namespace.default;
311
311
  }
312
312
  return null;
313
- };
313
+ }
314
314
 
315
315
  /**
316
316
  * Lazily load and build the BE-parity lookup tables from their JSON payloads.
@@ -952,6 +952,12 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
952
952
  return _regeneratorRuntime.wrap(function (_context4) {
953
953
  while (1) switch (_context4.prev = _context4.next) {
954
954
  case 0:
955
+ captureCompletionTime = function _captureCompletionTim(promise, onResolved) {
956
+ return promise.then(function (value) {
957
+ onResolved(performance.now());
958
+ return value;
959
+ });
960
+ };
955
961
  if (!(!engine || destroyed)) {
956
962
  _context4.next = 1;
957
963
  break;
@@ -971,12 +977,6 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
971
977
  lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
972
978
  semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
973
979
  arcticInput = wrapForArctic(semanticText);
974
- captureCompletionTime = function captureCompletionTime(promise, onResolved) {
975
- return promise.then(function (value) {
976
- onResolved(performance.now());
977
- return value;
978
- });
979
- };
980
980
  if (isAutocompleteDebugEnabled()) {
981
981
  // eslint-disable-next-line no-console
982
982
  console.log("%c[LocalSlowLane] %c\uD83D\uDD22 Arctic input (".concat(arcticInput.length, " chars, ").concat(splitOnWhitespace(semanticText).length, " words): \"").concat(arcticInput.length > 100 ? "".concat(arcticInput.slice(0, 100), "\u2026") : arcticInput, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #009688;');
@@ -690,7 +690,7 @@ export var predict = function predict(textBefore) {
690
690
  * array and a sparse numeric-keyed object are emitted identically as named
691
691
  * exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
692
692
  */
693
- var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
693
+ function unwrapJsonModule(mod, shape) {
694
694
  if (mod == null || _typeof(mod) !== 'object') {
695
695
  return null;
696
696
  }
@@ -726,7 +726,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
726
726
  return namespace.default;
727
727
  }
728
728
  return null;
729
- };
729
+ }
730
730
  export var loadVectorsAsync = /*#__PURE__*/function () {
731
731
  var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(options) {
732
732
  var _options$isLocalLLM;
@@ -1,2 +1,3 @@
1
1
  export { autocompletePlugin } from './autocompletePlugin';
2
2
  export type { AutocompletePlugin } from './autocompletePluginType';
3
+ export type { AutocompleteContext, AutocompletePluginOptions, } from './pm-plugins/autocomplete-plugin';
@@ -40,6 +40,12 @@ export interface AutocompletePluginOptions {
40
40
  * Use this to serve vectors from a CDN or media service in production.
41
41
  */
42
42
  getVectorsBinaryUrl?: () => Promise<string>;
43
+ /**
44
+ * Subscription for hosts with live external context (e.g. Rovo chat). The plugin
45
+ * re-fetches via getContext() on each notification and unsubscribes on destroy.
46
+ * Only meaningful alongside `getContext`; without it each notification is a no-op.
47
+ */
48
+ subscribeToContextUpdates?: (onContextUpdated: () => void) => () => void;
43
49
  /**
44
50
  * User locale used to determine whether autocomplete should run.
45
51
  * Defaults to browser locale when omitted.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-autocomplete",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
4
4
  "description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -27,7 +27,7 @@
27
27
  "wink-nlp": "^2.4.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "@atlaskit/editor-common": "^116.6.0",
30
+ "@atlaskit/editor-common": "^116.15.0",
31
31
  "@atlaskit/editor-plugin-analytics": "^12.0.0",
32
32
  "react": "^18.2.0"
33
33
  },
package/src/index.ts CHANGED
@@ -3,3 +3,7 @@
3
3
 
4
4
  export { autocompletePlugin } from './autocompletePlugin';
5
5
  export type { AutocompletePlugin } from './autocompletePluginType';
6
+ export type {
7
+ AutocompleteContext,
8
+ AutocompletePluginOptions,
9
+ } from './pm-plugins/autocomplete-plugin';
@@ -214,6 +214,12 @@ export interface AutocompletePluginOptions {
214
214
  * Use this to serve vectors from a CDN or media service in production.
215
215
  */
216
216
  getVectorsBinaryUrl?: () => Promise<string>;
217
+ /**
218
+ * Subscription for hosts with live external context (e.g. Rovo chat). The plugin
219
+ * re-fetches via getContext() on each notification and unsubscribes on destroy.
220
+ * Only meaningful alongside `getContext`; without it each notification is a no-op.
221
+ */
222
+ subscribeToContextUpdates?: (onContextUpdated: () => void) => () => void;
217
223
  /**
218
224
  * User locale used to determine whether autocomplete should run.
219
225
  * Defaults to browser locale when omitted.
@@ -329,6 +335,7 @@ export const createAutocompletePlugin = (
329
335
  * user has already typed several words before the promise resolved.
330
336
  */
331
337
  let currentView: EditorView | null = null;
338
+ let unsubscribeFromContextUpdates: (() => void) | undefined;
332
339
  /**
333
340
  * Set after accepting a suggestion so the next doc-change update
334
341
  * skips scheduling a new prediction for the just-inserted text.
@@ -582,7 +589,9 @@ export const createAutocompletePlugin = (
582
589
  // nodeAfter correctly handles inline atoms (mentions, emojis) where
583
590
  // parentOffset and textContent indices diverge.
584
591
  const nodeAfter = selection.$from.nodeAfter;
585
- const charAfterCursor = nodeAfter?.isText ? getLeadingTextCharacter(nodeAfter.text) : undefined;
592
+ const charAfterCursor = nodeAfter?.isText
593
+ ? getLeadingTextCharacter(nodeAfter.text)
594
+ : undefined;
586
595
  // Only suppress for Unicode letters, digits, and underscore — hyphens,
587
596
  // apostrophes, and similar punctuation are valid left-edge boundaries
588
597
  // and should not block suggestions (e.g. cursor before '-' in compound-word).
@@ -790,66 +799,85 @@ export const createAutocompletePlugin = (
790
799
  },
791
800
  },
792
801
 
793
- view: () => ({
794
- update: (view: EditorView, prevState: EditorState) => {
795
- if (!isAutocompleteEnabled) {
796
- return;
797
- }
802
+ view: (editorView: EditorView) => {
803
+ // Capture up front so a subscription notification before the first PM
804
+ // transaction can still drive slowLaneClient.updateContext (gated on currentView).
805
+ currentView = editorView;
798
806
 
799
- currentView = view;
800
- if (!prevState.doc.eq(view.state.doc)) {
801
- if (justAccepted) {
802
- justAccepted = false;
803
-
804
- // Snapshot the post-acceptance text so follow-up transactions hit
805
- // the dismissedContext guard and abort until the user types again.
806
- dismissedContext = getTextBeforeCursor(view.state);
807
+ // Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
808
+ if (isAutocompleteEnabled && options?.subscribeToContextUpdates) {
809
+ unsubscribeFromContextUpdates = options.subscribeToContextUpdates(() => {
810
+ // Bypass throttle for freshness; the in-flight guard prevents overlap.
811
+ refreshContext({ source: 'subscription', allowThrottle: false });
812
+ });
813
+ }
807
814
 
808
- if (debounceTimer) {
809
- clearTimeout(debounceTimer);
810
- }
815
+ return {
816
+ update: (view: EditorView, prevState: EditorState) => {
817
+ if (!isAutocompleteEnabled) {
811
818
  return;
812
819
  }
813
820
 
814
- maybeUpdateSessionFrequency(view, prevState);
815
-
816
- const textBefore = getTextBeforeCursor(view.state);
817
- if (isWordBoundary(textBefore)) {
818
- slowLaneClient.updateContext(
819
- buildSlowLaneText(view.state.doc.textContent, resolvedContext),
820
- );
821
-
822
- // Context may not have resolved on first focus (e.g. comment
823
- // thread still loading). Retry on word boundaries until we have
824
- // the parent comment, throttled so we don't refetch constantly
825
- // and capped so non-comment editors stop retrying entirely.
826
- if (
827
- !resolvedContext?.parentCommentContent &&
828
- wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS
829
- ) {
830
- // Only count the attempt when a fetch actually started, so an
831
- // in-flight or throttled no-op doesn't burn the retry budget.
832
- if (refreshContext({ source: 'word-boundary' })) {
833
- wordBoundaryRefreshAttempts++;
821
+ currentView = view;
822
+ if (!prevState.doc.eq(view.state.doc)) {
823
+ if (justAccepted) {
824
+ justAccepted = false;
825
+
826
+ // Snapshot the post-acceptance text so follow-up transactions hit
827
+ // the dismissedContext guard and abort until the user types again.
828
+ dismissedContext = getTextBeforeCursor(view.state);
829
+
830
+ if (debounceTimer) {
831
+ clearTimeout(debounceTimer);
834
832
  }
833
+ return;
835
834
  }
836
- }
837
835
 
838
- schedulePrediction(view);
839
- }
840
- },
841
- destroy: () => {
842
- destroyed = true;
843
- currentView = null;
844
- if (debounceTimer) {
845
- clearTimeout(debounceTimer);
846
- }
847
- if (hasDestroy(slowLaneClient)) {
848
- slowLaneClient.destroy();
849
- }
850
- ingestedContextTexts.clear();
851
- setDefaultSlowLaneClient(null);
852
- },
853
- }),
836
+ maybeUpdateSessionFrequency(view, prevState);
837
+
838
+ const textBefore = getTextBeforeCursor(view.state);
839
+ if (isWordBoundary(textBefore)) {
840
+ slowLaneClient.updateContext(
841
+ buildSlowLaneText(view.state.doc.textContent, resolvedContext),
842
+ );
843
+
844
+ // Context may not have resolved on first focus (e.g. comment
845
+ // thread still loading). Retry on word boundaries until we have
846
+ // the parent comment, throttled so we don't refetch constantly
847
+ // and capped so non-comment editors stop retrying entirely.
848
+ // Skipped for push-channel hosts (subscribeToContextUpdates): they
849
+ // never grow parentCommentContent, so polling only burns the budget.
850
+ if (
851
+ !options?.subscribeToContextUpdates &&
852
+ !resolvedContext?.parentCommentContent &&
853
+ wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS
854
+ ) {
855
+ // Only count the attempt when a fetch actually started, so an
856
+ // in-flight or throttled no-op doesn't burn the retry budget.
857
+ if (refreshContext({ source: 'word-boundary' })) {
858
+ wordBoundaryRefreshAttempts++;
859
+ }
860
+ }
861
+ }
862
+
863
+ schedulePrediction(view);
864
+ }
865
+ },
866
+ destroy: () => {
867
+ destroyed = true;
868
+ currentView = null;
869
+ unsubscribeFromContextUpdates?.();
870
+ unsubscribeFromContextUpdates = undefined;
871
+ if (debounceTimer) {
872
+ clearTimeout(debounceTimer);
873
+ }
874
+ if (hasDestroy(slowLaneClient)) {
875
+ slowLaneClient.destroy();
876
+ }
877
+ ingestedContextTexts.clear();
878
+ setDefaultSlowLaneClient(null);
879
+ },
880
+ };
881
+ },
854
882
  });
855
883
  };
@@ -369,7 +369,7 @@ let bePayloadDataPromise: Promise<void> | undefined;
369
369
  * :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
370
370
  * :returns: The parsed JSON value, or `null` if neither interop mode applies.
371
371
  */
372
- const unwrapJsonModule = <T>(mod: unknown, shape: 'object' | 'array'): T | null => {
372
+ function unwrapJsonModule<T>(mod: unknown, shape: 'object' | 'array'): T | null {
373
373
  if (mod == null || typeof mod !== 'object') {
374
374
  return null;
375
375
  }
@@ -411,7 +411,7 @@ const unwrapJsonModule = <T>(mod: unknown, shape: 'object' | 'array'): T | null
411
411
  }
412
412
 
413
413
  return null;
414
- };
414
+ }
415
415
 
416
416
  /**
417
417
  * Lazily load and build the BE-parity lookup tables from their JSON payloads.
@@ -1004,14 +1004,15 @@ export const createLocalSlowLaneClient = (
1004
1004
  const lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
1005
1005
  const semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
1006
1006
  const arcticInput = wrapForArctic(semanticText);
1007
- const captureCompletionTime = <T>(
1007
+ function captureCompletionTime<T>(
1008
1008
  promise: Promise<T>,
1009
1009
  onResolved: (resolvedAt: number) => void,
1010
- ): Promise<T> =>
1011
- promise.then((value: T) => {
1010
+ ): Promise<T> {
1011
+ return promise.then((value: T) => {
1012
1012
  onResolved(performance.now());
1013
1013
  return value;
1014
1014
  });
1015
+ }
1015
1016
 
1016
1017
  if (isAutocompleteDebugEnabled()) {
1017
1018
  // eslint-disable-next-line no-console
@@ -715,7 +715,7 @@ interface VocabularyJson {
715
715
  * array and a sparse numeric-keyed object are emitted identically as named
716
716
  * exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
717
717
  */
718
- const unwrapJsonModule = <T>(mod: unknown, shape: 'object' | 'array'): T | null => {
718
+ function unwrapJsonModule<T>(mod: unknown, shape: 'object' | 'array'): T | null {
719
719
  if (mod == null || typeof mod !== 'object') {
720
720
  return null;
721
721
  }
@@ -743,7 +743,7 @@ const unwrapJsonModule = <T>(mod: unknown, shape: 'object' | 'array'): T | null
743
743
  }
744
744
 
745
745
  return null;
746
- };
746
+ }
747
747
 
748
748
  export const loadVectorsAsync = async (options?: {
749
749
  getBinaryUrl?: () => Promise<string>;