@atlaskit/editor-plugin-autocomplete 9.0.0 → 9.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.
@@ -13,10 +13,13 @@ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
13
13
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
14
14
  import { CTC_STYLES, ctcTag, isAutocompleteDebugEnabled, isAutocompleteDebugVerbose } from './debug-mode';
15
15
  import { createGhostTextDecorationSet } from './ghost-text-decoration';
16
+ // Type-only, so the harvester chunk is still reached exclusively through the
17
+ // dynamic import in `ensureInlineCodeHarvester`.
18
+
16
19
  import { createLocalSlowLaneClient } from './local-slow-lane-client';
17
20
  import { loadGrammarDataAsync } from './scoring-pipeline';
18
21
  import { clearDefaultSlowLaneClient, createSlowLaneClient, getDefaultSlowLaneClientStatus, isWordBoundary, setDefaultSlowLaneClient } from './slow-lane-client';
19
- import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage, getLastPredictionDebug, noteSuggestionAccepted } from './text-predictor';
22
+ import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage, getLastPredictionDebug, getLastPredictionOutcome, isSurfaceInAcceptCooldown, noteSuggestionAccepted, resetSessionBoosts } from './text-predictor';
20
23
  export var autocompletePluginKey = new PluginKey('autocomplete');
21
24
  var PREDICTION_COALESCE_MS = 0;
22
25
  var GHOST_DECISION_BUDGET_MS = 100;
@@ -27,6 +30,32 @@ var CONTEXT_REFRESH_THROTTLE_MS = 1000;
27
30
  // version of the (potentially hundreds-of-KB) page content for the plugin's
28
31
  // lifetime. Eviction is FIFO; a re-ingest of an evicted text is harmless.
29
32
  var MAX_INGESTED_CONTEXT_TEXTS = 50;
33
+ // Re-prime callback of every mounted editor. The harvested set and the L1
34
+ // boosts are shared by all of them, and more than one is mounted routinely — a
35
+ // comment box alongside its replies, or a comment box alongside the chat input.
36
+ // Membership doubles as the mount count, so a teardown only clears the shared
37
+ // stores once the last editor has gone.
38
+ //
39
+ // Decision: one pot, shared across hosts. Terms harvested in a comment box can
40
+ // be offered in the chat input and the other way round. Both are content from
41
+ // the page the reader is looking at, so the mixing is between things they can
42
+ // already see; partitioning the boosts would mean a per-host overlay on every
43
+ // trie node, which the scoring path reads on every keystroke.
44
+ var mountedEditors = new Set();
45
+ // The scope that shared learning belongs to, as last reported by a host that
46
+ // scopes its context. Held here rather than per editor because the data it
47
+ // guards is shared and some hosts replace one editor with another across a
48
+ // navigation: an instance-local key would read as unset on exactly the change it
49
+ // exists to catch, while the previous scope's terms carried on in these globals.
50
+ // Undefined until a scoping host says otherwise. An editor that sends no key
51
+ // still shares the pot being emptied, so it is re-primed from the context it
52
+ // already holds rather than left with learning it can no longer see.
53
+ var contextScopeKey;
54
+ // Set by whichever editor first finished importing the harvester chunk. The
55
+ // reset paths go through this rather than their own copy, because the editor
56
+ // that has to clear the set is not always one that imported it — the set is one
57
+ // per module, and nothing can be in it unless some editor got this far.
58
+ var loadedInlineCodeHarvester = null;
30
59
  // Caps how many times the word-boundary path will retry getContext() while the
31
60
  // parent comment is still missing. Combined with the 1s throttle this gives a
32
61
  // ~5s window to cover a still-loading comment thread, then stops permanently so
@@ -103,10 +132,14 @@ var advanceGhostThroughTypedCharacter = function advanceGhostThroughTypedCharact
103
132
  ghostPosition: nextPosition,
104
133
  decorationSet: createGhostTextDecorationSet(tr.doc, nextPosition, remaining),
105
134
  // `surface` stays the full candidate so cooldown and analytics still
106
- // describe the suggestion that was originally committed.
135
+ // describe the suggestion that was originally committed. The replaced
136
+ // prefix grows with the keystroke: it is the run the insertion overwrites,
137
+ // and the user has just typed one more character of it.
107
138
  suggestion: _objectSpread(_objectSpread({}, suggestion), {}, {
108
139
  ghostText: remaining,
109
140
  position: nextPosition
141
+ }, suggestion.replacesTypedPrefixLength === undefined ? {} : {
142
+ replacesTypedPrefixLength: suggestion.replacesTypedPrefixLength + 1
110
143
  })
111
144
  });
112
145
  };
@@ -141,9 +174,12 @@ var getTextBeforeCursor = function getTextBeforeCursor(state) {
141
174
  }
142
175
  return fullText.slice(-maxChars);
143
176
  };
144
- var getTrailingSurfacePrefixLength = function getTrailingSurfacePrefixLength(text) {
177
+ var getTrailingSurfaceToken = function getTrailingSurfaceToken(text) {
145
178
  var _text$trimEnd$match$, _text$trimEnd$match;
146
- return (_text$trimEnd$match$ = (_text$trimEnd$match = text.trimEnd().match(TRAILING_SURFACE_PREFIX_REGEX)) === null || _text$trimEnd$match === void 0 ? void 0 : _text$trimEnd$match[0].length) !== null && _text$trimEnd$match$ !== void 0 ? _text$trimEnd$match$ : 0;
179
+ return (_text$trimEnd$match$ = (_text$trimEnd$match = text.trimEnd().match(TRAILING_SURFACE_PREFIX_REGEX)) === null || _text$trimEnd$match === void 0 ? void 0 : _text$trimEnd$match[0]) !== null && _text$trimEnd$match$ !== void 0 ? _text$trimEnd$match$ : '';
180
+ };
181
+ var getTrailingSurfacePrefixLength = function getTrailingSurfacePrefixLength(text) {
182
+ return getTrailingSurfaceToken(text).length;
147
183
  };
148
184
 
149
185
  /**
@@ -157,11 +193,13 @@ var setAutocompleteMeta = function setAutocompleteMeta(tr, meta) {
157
193
  * Apply a ghost text suggestion to the editor state.
158
194
  */
159
195
  var lastShownGhostText = '';
160
- var showGhostText = function showGhostText(view, prediction, position, revision, decisionStartedAt) {
196
+ var showGhostText = function showGhostText(view, prediction, position, revision, decisionStartedAt, replacesTypedPrefixLength) {
161
197
  try {
162
198
  var state = view.state,
163
199
  dispatch = view.dispatch;
164
- var suggestion = {
200
+ var suggestion = _objectSpread(_objectSpread({}, replacesTypedPrefixLength === undefined ? {} : {
201
+ replacesTypedPrefixLength: replacesTypedPrefixLength
202
+ }), {}, {
165
203
  decisionLatencyMs: performance.now() - decisionStartedAt,
166
204
  evidenceTier: prediction.evidenceTier,
167
205
  ghostText: prediction.text,
@@ -175,7 +213,7 @@ var showGhostText = function showGhostText(view, prediction, position, revision,
175
213
  surface: prediction.surface,
176
214
  termType: prediction.termType,
177
215
  winnerMargin: prediction.winnerMargin
178
- };
216
+ });
179
217
  var decorationSet = createGhostTextDecorationSet(state.doc, position, prediction.text);
180
218
  var tr = setAutocompleteMeta(state.tr, {
181
219
  ghostText: prediction.text,
@@ -230,10 +268,31 @@ var acceptGhostText = function acceptGhostText(state, dispatch) {
230
268
  }
231
269
  if (dispatch) {
232
270
  try {
271
+ var _suggestion$replacesT;
233
272
  var ghostText = pluginState.ghostText,
234
273
  ghostPosition = pluginState.ghostPosition,
235
274
  suggestion = pluginState.suggestion;
236
- var tr = state.tr.insertText(ghostText, ghostPosition);
275
+ // Replacing back over the typed prefix rewrites it in the surface's own
276
+ // casing; every other path appends and leaves the prefix untouched.
277
+ var replaceFrom = Math.max(0, ghostPosition - Math.min((_suggestion$replacesT = suggestion.replacesTypedPrefixLength) !== null && _suggestion$replacesT !== void 0 ? _suggestion$replacesT : 0, ghostPosition));
278
+ var tr = state.tr;
279
+ if (replaceFrom === ghostPosition) {
280
+ tr = tr.insertText(ghostText, ghostPosition);
281
+ } else {
282
+ tr = tr.insertText(suggestion.surface, replaceFrom, ghostPosition);
283
+ // Only the harvested path replaces, and what authorized it was the
284
+ // surface being marked as code somewhere in the session. Inserting it
285
+ // as plain text loses that, so the identifier the user accepted reads
286
+ // as prose while the same identifier they typed by hand does not.
287
+ var codeMark = state.schema.marks.code;
288
+ if (codeMark) {
289
+ tr = tr.addMark(replaceFrom, replaceFrom + suggestion.surface.length, codeMark.create());
290
+ // The backtick input rule never ran, so nothing else will close this
291
+ // mark. Without dropping it from the stored set the next character
292
+ // the user types continues the code span.
293
+ tr = tr.removeStoredMark(codeMark);
294
+ }
295
+ }
237
296
  tr = setAutocompleteMeta(tr, {
238
297
  ghostText: '',
239
298
  ghostPosition: -1,
@@ -321,6 +380,71 @@ var getLeadingTextCharacter = function getLeadingTextCharacter(text) {
321
380
  }
322
381
  return String.fromCodePoint(firstCodePoint);
323
382
  };
383
+
384
+ /**
385
+ * The one verdict a harvested surface may speak on.
386
+ *
387
+ * `no-candidate` is the only abstention that means no vocabulary reaches the
388
+ * prefix at all. Every other one — a margin the model cannot clear, a rival
389
+ * still being read — describes known words in contention, where a session
390
+ * surface with no score behind it would be overruling the ranker rather than
391
+ * filling a gap it left.
392
+ */
393
+ var HARVEST_ELIGIBLE_ABSTAIN_REASON = 'no-candidate';
394
+
395
+ /**
396
+ * Whether the scored path has finished with this exact prefix and found nothing.
397
+ *
398
+ * The 100ms deadline expiring is not the same answer: it means the ranker was
399
+ * still working, and displaying then would race a suggestion that is about to
400
+ * arrive. So the harvest path reads the recorded verdict rather than the clock.
401
+ */
402
+ var scoredPathFinishedEmpty = function scoredPathFinishedEmpty(textBefore) {
403
+ var outcome = getLastPredictionOutcome();
404
+ return outcome !== null && outcome.textBefore === textBefore && !outcome.awaitingAsyncEvidence && outcome.abstainReason === HARVEST_ELIGIBLE_ABSTAIN_REASON;
405
+ };
406
+
407
+ /**
408
+ * Whether the surface is already sitting immediately before the typed prefix.
409
+ *
410
+ * Completing `ml-s` to `ml-studio` right after `ml-studio` produces the echo the
411
+ * scored path's repetition guard exists to stop, and this path does not go
412
+ * through arbitration to inherit it.
413
+ */
414
+ var repeatsPrecedingText = function repeatsPrecedingText(match, textBefore) {
415
+ var trimmed = textBefore.trimEnd();
416
+ var preceding = trimmed.slice(0, trimmed.length - match.typedPrefixLength).trimEnd();
417
+ return preceding.toLowerCase().endsWith(match.surface.toLowerCase());
418
+ };
419
+
420
+ /**
421
+ * Dress a harvested match as a prediction so it commits through the same path as
422
+ * a scored one.
423
+ *
424
+ * The scoring fields are zeroed rather than invented: there is no posterior, no
425
+ * margin and no shortlist behind this surface, and `session-harvest` on the
426
+ * evidence tier is what says so wherever the snapshot is read.
427
+ */
428
+ var buildHarvestPrediction = function buildHarvestPrediction(match) {
429
+ return {
430
+ evidenceDepth: {
431
+ totalChars: 0,
432
+ totalTokens: 0,
433
+ verifiedChars: 0,
434
+ verifiedTokens: 0
435
+ },
436
+ evidenceTier: 'session-harvest',
437
+ meanTokenLogProbability: 0,
438
+ poolHeldExtension: false,
439
+ posterior: 0,
440
+ rankScore: 0,
441
+ shortlistSize: 0,
442
+ surface: match.surface,
443
+ termType: 'word',
444
+ text: match.ghostText,
445
+ winnerMargin: 0
446
+ };
447
+ };
324
448
  var isEnglishLocale = function isEnglishLocale(locale) {
325
449
  if (!locale) {
326
450
  return false;
@@ -332,6 +456,7 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
332
456
  var surface = (_options$surface = options === null || options === void 0 ? void 0 : options.surface) !== null && _options$surface !== void 0 ? _options$surface : 'editor';
333
457
  var locale = (_options$locale = options === null || options === void 0 ? void 0 : options.locale) !== null && _options$locale !== void 0 ? _options$locale : typeof navigator !== 'undefined' ? navigator.language : undefined;
334
458
  var isAutocompleteEnabled = isEnglishLocale(locale);
459
+ var isInlineCodeHarvestEnabled = isAutocompleteEnabled && (options === null || options === void 0 ? void 0 : options.harvestInlineCode) === true;
335
460
  var debounceTimer = null;
336
461
  var decisionDeadlineTimer = null;
337
462
  var hasIngestedPage = false;
@@ -374,14 +499,26 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
374
499
  }
375
500
  return options !== null && options !== void 0 && options.useLocalModel ? 'localLlm' : 'server';
376
501
  };
377
- var fireSuggestionDismissedAnalytics = function fireSuggestionDismissedAnalytics(reason) {
502
+
503
+ /**
504
+ * Which path produced a given suggestion.
505
+ *
506
+ * A harvested surface is reported as `harvest` rather than by the slow-lane
507
+ * state it happened to be shown under, so its views and acceptances stay
508
+ * separable from the scored path's and cannot quietly move the headline
509
+ * acceptance rate.
510
+ */
511
+ var completionSourceFor = function completionSourceFor(suggestion) {
512
+ return (suggestion === null || suggestion === void 0 ? void 0 : suggestion.evidenceTier) === 'session-harvest' ? 'harvest' : getCompletionSource();
513
+ };
514
+ var fireSuggestionDismissedAnalytics = function fireSuggestionDismissedAnalytics(reason, suggestion) {
378
515
  var _api$analytics;
379
516
  api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({
380
517
  action: ACTION.SUGGESTION_DISMISSED,
381
518
  actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
382
519
  eventType: EVENT_TYPE.TRACK,
383
520
  attributes: {
384
- completionSource: getCompletionSource(),
521
+ completionSource: completionSourceFor(suggestion),
385
522
  reason: reason,
386
523
  surface: surface
387
524
  }
@@ -436,7 +573,7 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
436
573
  actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
437
574
  eventType: EVENT_TYPE.TRACK,
438
575
  attributes: {
439
- completionSource: getCompletionSource(),
576
+ completionSource: completionSourceFor(suggestion),
440
577
  suggestionLength: suggestionLength,
441
578
  typedLength: typedLength,
442
579
  kssDelta: kssDelta,
@@ -502,6 +639,10 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
502
639
  var registeredSlowLaneStatus = getDefaultSlowLaneClientStatus();
503
640
  ctcTag('init', "slow lane registered \xB7 id=".concat(slowLaneClientId, " \xB7 selected=").concat(slowLaneClientKind, " \xB7 canonical=").concat(registeredSlowLaneStatus.canonicalScoringSupported ? 'yes' : 'no', " \xB7 ready=").concat((_registeredSlowLaneSt = registeredSlowLaneStatus.localModelReady) !== null && _registeredSlowLaneSt !== void 0 ? _registeredSlowLaneSt : 'n/a'), slowLaneClientKind === 'localLlm' ? CTC_STYLES.good : CTC_STYLES.warn);
504
641
  var contextRequestInFlight = false;
642
+ // A read the host asked for while another was open, kept so it can be made
643
+ // once that one settles. One slot, not a queue: a burst of notifications only
644
+ // ever means "read again", and the last of them describes the current state.
645
+ var queuedContextRefreshSource;
505
646
  var lastContextRefreshAt = 0;
506
647
  // Bounds the word-boundary retry loop so it terminates even when the editor is
507
648
  // not in a comment thread (parentCommentContent never resolves).
@@ -509,7 +650,163 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
509
650
  // Set when the plugin is torn down so in-flight getContext() resolutions don't
510
651
  // mutate the global text-predictor state after destruction.
511
652
  var destroyed = false;
512
- var ingestedContextTexts = new Set();
653
+ // L1 ingestion cannot run before the vocabulary has loaded: `incrementSessionFreq`
654
+ // only touches trie nodes that already exist, so an empty trie silently rejects
655
+ // every word. Text that arrives first — which a page usually does, since the
656
+ // chat resolves it on focus in the same tick as the load starts — is held here
657
+ // and applied once the load has settled.
658
+ var pendingSessionContextTexts = new Set();
659
+ var sessionIngestedContextTexts = new Set();
660
+ var isVocabularyReady = false;
661
+ var vocabularyLoadPromise;
662
+
663
+ // Context text waiting on the harvester chunk, the vocabulary, or both. The
664
+ // harvester drops surfaces the vocabulary already holds, so intake before the
665
+ // load has settled would admit ordinary words no one needs completed.
666
+ var pendingHarvestTexts = new Set();
667
+ // Separate from `sessionIngestedContextTexts` because the two consumers settle
668
+ // at different times: reply occurrences accumulate, so text fed twice would
669
+ // count twice.
670
+ var harvestedContextTexts = new Set();
671
+ var inlineCodeHarvester = null;
672
+ var inlineCodeHarvesterPromise;
673
+ var addBoundedContextText = function addBoundedContextText(texts, text) {
674
+ texts.add(text);
675
+ // Evict oldest entries (Set preserves insertion order) to bound memory.
676
+ while (texts.size > MAX_INGESTED_CONTEXT_TEXTS) {
677
+ var oldest = texts.values().next().value;
678
+ if (oldest === undefined) {
679
+ break;
680
+ }
681
+ texts.delete(oldest);
682
+ }
683
+ };
684
+ var flushPendingSessionContext = function flushPendingSessionContext() {
685
+ if (!isVocabularyReady || destroyed) {
686
+ return;
687
+ }
688
+ var _iterator = _createForOfIteratorHelper(pendingSessionContextTexts),
689
+ _step;
690
+ try {
691
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
692
+ var text = _step.value;
693
+ if (!sessionIngestedContextTexts.has(text)) {
694
+ ingestDocumentPage(text);
695
+ addBoundedContextText(sessionIngestedContextTexts, text);
696
+ }
697
+ pendingSessionContextTexts.delete(text);
698
+ }
699
+ } catch (err) {
700
+ _iterator.e(err);
701
+ } finally {
702
+ _iterator.f();
703
+ }
704
+ };
705
+ var flushPendingHarvestTexts = function flushPendingHarvestTexts() {
706
+ var harvester = inlineCodeHarvester;
707
+ if (!harvester || !isVocabularyReady || destroyed || pendingHarvestTexts.size === 0) {
708
+ return;
709
+ }
710
+ var _iterator2 = _createForOfIteratorHelper(pendingHarvestTexts),
711
+ _step2;
712
+ try {
713
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
714
+ var text = _step2.value;
715
+ if (!harvestedContextTexts.has(text)) {
716
+ harvester.harvestInlineCodeFromText(text);
717
+ addBoundedContextText(harvestedContextTexts, text);
718
+ }
719
+ pendingHarvestTexts.delete(text);
720
+ }
721
+ } catch (err) {
722
+ _iterator2.e(err);
723
+ } finally {
724
+ _iterator2.f();
725
+ }
726
+ harvester.logInlineCodeHarvest('context');
727
+ };
728
+
729
+ /**
730
+ * Load the harvester chunk, once, and only for a host that asked for it.
731
+ *
732
+ * Kept off the critical path in the same way the vocabulary and vector loads
733
+ * are: nothing is requested until the editor is focused, so a session that
734
+ * never types in the chat pays nothing for the feature.
735
+ */
736
+ var ensureInlineCodeHarvester = function ensureInlineCodeHarvester() {
737
+ if (!isInlineCodeHarvestEnabled || destroyed) {
738
+ return Promise.resolve(null);
739
+ }
740
+ inlineCodeHarvesterPromise !== null && inlineCodeHarvesterPromise !== void 0 ? inlineCodeHarvesterPromise : inlineCodeHarvesterPromise = import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-inline-code-harvester" */
741
+ './inline-code-harvester').then(function (module) {
742
+ if (destroyed) {
743
+ return null;
744
+ }
745
+ inlineCodeHarvester = module;
746
+ loadedInlineCodeHarvester = module;
747
+ flushPendingHarvestTexts();
748
+ return module;
749
+ }).catch(function (error) {
750
+ // Clear the promise so a later focus can retry; the queued text is
751
+ // still held and is fed by whichever attempt succeeds.
752
+ inlineCodeHarvesterPromise = undefined;
753
+ logException(error, {
754
+ location: 'editor-plugin-autocomplete/loadInlineCodeHarvester'
755
+ });
756
+ return null;
757
+ });
758
+ return inlineCodeHarvesterPromise;
759
+ };
760
+
761
+ /**
762
+ * Re-read the document's code-marked spans.
763
+ *
764
+ * The backtick input rule consumes both delimiters on the closing tick, so a
765
+ * finished span in the live document is only findable through its mark — and
766
+ * only by walking, since a mark carries no notification.
767
+ */
768
+ var harvestDocument = function harvestDocument(doc, trigger) {
769
+ if (!isInlineCodeHarvestEnabled || destroyed) {
770
+ return;
771
+ }
772
+ var harvester = inlineCodeHarvester;
773
+ if (!harvester) {
774
+ void ensureInlineCodeHarvester();
775
+ return;
776
+ }
777
+ if (!isVocabularyReady) {
778
+ return;
779
+ }
780
+ harvester.harvestInlineCodeFromDoc(doc);
781
+ harvester.logInlineCodeHarvest(trigger);
782
+ };
783
+ var ensureVocabularyReady = function ensureVocabularyReady() {
784
+ var _options$useLocalMode;
785
+ if (isVocabularyReady) {
786
+ flushPendingSessionContext();
787
+ flushPendingHarvestTexts();
788
+ return Promise.resolve();
789
+ }
790
+ vocabularyLoadPromise !== null && vocabularyLoadPromise !== void 0 ? vocabularyLoadPromise : vocabularyLoadPromise = loadDefaultVocabulary({
791
+ isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false,
792
+ surface: surface
793
+ }).then(function () {
794
+ if (destroyed) {
795
+ return;
796
+ }
797
+ isVocabularyReady = true;
798
+ flushPendingSessionContext();
799
+ flushPendingHarvestTexts();
800
+ }).catch(function (error) {
801
+ // Do not consume the pending text on failure. A later focus retries the
802
+ // load and can still apply the original context exactly once.
803
+ vocabularyLoadPromise = undefined;
804
+ logException(error, {
805
+ location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
806
+ });
807
+ });
808
+ return vocabularyLoadPromise;
809
+ };
513
810
  var logContextResolved = function logContextResolved(source, context) {
514
811
  var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
515
812
  if (!isAutocompleteDebugEnabled()) {
@@ -519,14 +816,134 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
519
816
  // eslint-disable-next-line no-console
520
817
  console.log('%c[CTC:signal]%c getContext resolved', CTC_STYLES.brand, CTC_STYLES.body, {
521
818
  source: source,
819
+ scope: context === null || context === void 0 ? void 0 : context.contextScopeKey,
522
820
  hasParentComment: !!(context !== null && context !== void 0 && context.parentCommentContent),
523
821
  parentCommentPreview: context === null || context === void 0 || (_context$parentCommen = context.parentCommentContent) === null || _context$parentCommen === void 0 ? void 0 : _context$parentCommen.slice(0, 80),
524
822
  siblingCount: (_context$siblingComme4 = context === null || context === void 0 || (_context$siblingComme5 = context.siblingCommentsContents) === null || _context$siblingComme5 === void 0 ? void 0 : _context$siblingComme5.length) !== null && _context$siblingComme4 !== void 0 ? _context$siblingComme4 : 0,
525
823
  hasFullPage: !!(context !== null && context !== void 0 && context.fullPageContent)
526
824
  });
527
825
  };
826
+ var ingestContextText = function ingestContextText(text) {
827
+ if (!text) {
828
+ return;
829
+ }
830
+ if (!sessionIngestedContextTexts.has(text)) {
831
+ addBoundedContextText(pendingSessionContextTexts, text);
832
+ }
833
+ // Queued unconditionally rather than only once the chunk is present:
834
+ // context usually resolves in the same tick the load starts, and text
835
+ // dropped for arriving early is a reply that can never be harvested.
836
+ if (isInlineCodeHarvestEnabled && !harvestedContextTexts.has(text)) {
837
+ addBoundedContextText(pendingHarvestTexts, text);
838
+ }
839
+ };
840
+ var ingestResolvedContext = function ingestResolvedContext() {
841
+ var _resolvedContext, _resolvedContext2, _resolvedContext$sibl, _resolvedContext3;
842
+ ingestContextText((_resolvedContext = resolvedContext) === null || _resolvedContext === void 0 ? void 0 : _resolvedContext.fullPageContent);
843
+ ingestContextText((_resolvedContext2 = resolvedContext) === null || _resolvedContext2 === void 0 ? void 0 : _resolvedContext2.parentCommentContent);
844
+ var _iterator3 = _createForOfIteratorHelper((_resolvedContext$sibl = (_resolvedContext3 = resolvedContext) === null || _resolvedContext3 === void 0 ? void 0 : _resolvedContext3.siblingCommentsContents) !== null && _resolvedContext$sibl !== void 0 ? _resolvedContext$sibl : []),
845
+ _step3;
846
+ try {
847
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
848
+ var siblingCommentContent = _step3.value;
849
+ ingestContextText(siblingCommentContent);
850
+ }
851
+ } catch (err) {
852
+ _iterator3.e(err);
853
+ } finally {
854
+ _iterator3.f();
855
+ }
856
+ flushPendingSessionContext();
857
+ flushPendingHarvestTexts();
858
+ };
859
+
860
+ /**
861
+ * Put back what this editor had contributed to the pot another editor just
862
+ * emptied.
863
+ *
864
+ * The scope that ended belongs to the host that reported it, not to everyone
865
+ * sharing these globals. Without this an editor alongside it — a comment box
866
+ * under a chat panel that switched conversation — is left with none of its
867
+ * learning and no way back to it: the dedupe sets below are per editor, so
868
+ * its own page reads as already ingested and is skipped from then on.
869
+ *
870
+ * Nothing is re-fetched. The context this editor resolved is still held, and
871
+ * what it describes has not changed just because another host moved on.
872
+ *
873
+ * Only for an editor that reported no scope. One that did is party to the
874
+ * same scope system and the scope just dropped may well be its own: a
875
+ * Confluence page transition mounts the incoming editor before tearing down
876
+ * the outgoing one, and the outgoing one is still holding the page that was
877
+ * left. Putting that back is exactly what the eviction was for.
878
+ */
879
+ var reprimeSessionLearning = function reprimeSessionLearning() {
880
+ var _resolvedContext4;
881
+ if (destroyed || ((_resolvedContext4 = resolvedContext) === null || _resolvedContext4 === void 0 ? void 0 : _resolvedContext4.contextScopeKey) !== undefined) {
882
+ return;
883
+ }
884
+ pendingSessionContextTexts.clear();
885
+ sessionIngestedContextTexts.clear();
886
+ pendingHarvestTexts.clear();
887
+ harvestedContextTexts.clear();
888
+ ingestResolvedContext();
889
+ if (currentView) {
890
+ harvestDocument(currentView.state.doc, 'reprime');
891
+ }
892
+ };
893
+
894
+ /**
895
+ * Drop everything learned for the scope that just ended.
896
+ *
897
+ * Both stores are claims about what is being discussed, and neither survives
898
+ * the discussion changing: L1 boosts would keep a page's words ranked above
899
+ * the next page's, and the harvested set holds one conversation's content.
900
+ * The dedupe sets go too, so the context that arrives next is treated as
901
+ * unseen and re-primes both — which is what makes this safe to do on a page
902
+ * change, since the transcript is re-ingested along with the new page.
903
+ *
904
+ * The stores are shared, so every other mounted editor is put back in the
905
+ * same tick from context it already holds. Only the scope that ended is
906
+ * actually dropped.
907
+ */
908
+ var resetSessionScopedLearning = function resetSessionScopedLearning(reason) {
909
+ var _loadedInlineCodeHarv;
910
+ resetSessionBoosts();
911
+ (_loadedInlineCodeHarv = loadedInlineCodeHarvester) === null || _loadedInlineCodeHarv === void 0 || _loadedInlineCodeHarv.resetInlineCodeHarvest();
912
+ resolvedContext = undefined;
913
+ pendingSessionContextTexts.clear();
914
+ sessionIngestedContextTexts.clear();
915
+ pendingHarvestTexts.clear();
916
+ harvestedContextTexts.clear();
917
+ ctcTag('init', "session-scoped learning reset \xB7 ".concat(reason), CTC_STYLES.dim);
918
+ var _iterator4 = _createForOfIteratorHelper(mountedEditors),
919
+ _step4;
920
+ try {
921
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
922
+ var reprimeOther = _step4.value;
923
+ if (reprimeOther !== reprimeSessionLearning) {
924
+ reprimeOther();
925
+ }
926
+ }
927
+ } catch (err) {
928
+ _iterator4.e(err);
929
+ } finally {
930
+ _iterator4.f();
931
+ }
932
+ };
528
933
  var applyContext = function applyContext(context) {
529
- var _context$siblingComme6;
934
+ // A host that scopes its context tells us which scope each read belongs to.
935
+ // A read with no recorded key to compare against only records: that is the
936
+ // first since the last editor went away, so there is nothing left to throw
937
+ // away and resetting would drop the priming focus just started. A read from
938
+ // a newly mounted editor is not that case — the key outlives the editor
939
+ // precisely so a host that remounts across a navigation still evicts.
940
+ if (context.contextScopeKey !== undefined && context.contextScopeKey !== contextScopeKey) {
941
+ if (contextScopeKey !== undefined) {
942
+ resetSessionScopedLearning("scope ".concat(contextScopeKey, " \u2192 ").concat(context.contextScopeKey));
943
+ }
944
+ contextScopeKey = context.contextScopeKey;
945
+ }
946
+
530
947
  // Merge rather than replace: the word-boundary retry may resolve only a
531
948
  // late-arriving field (e.g. parentCommentContent) without re-sending
532
949
  // fullPageContent, so replacing would drop previously resolved context.
@@ -538,38 +955,10 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
538
955
  return value !== undefined;
539
956
  }));
540
957
  resolvedContext = _objectSpread(_objectSpread({}, resolvedContext), definedContext);
541
- var ingestContextText = function ingestContextText(text) {
542
- if (!text || ingestedContextTexts.has(text)) {
543
- return;
544
- }
545
- ingestedContextTexts.add(text);
546
- // Evict oldest entries (Set preserves insertion order) to bound memory.
547
- while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
548
- var oldest = ingestedContextTexts.values().next().value;
549
- if (oldest === undefined) {
550
- break;
551
- }
552
- ingestedContextTexts.delete(oldest);
553
- }
554
- ingestDocumentPage(text);
555
- };
556
- ingestContextText(context.fullPageContent);
557
- ingestContextText(context.parentCommentContent);
558
- var _iterator = _createForOfIteratorHelper((_context$siblingComme6 = context.siblingCommentsContents) !== null && _context$siblingComme6 !== void 0 ? _context$siblingComme6 : []),
559
- _step;
560
- try {
561
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
562
- var siblingCommentContent = _step.value;
563
- ingestContextText(siblingCommentContent);
564
- }
958
+ ingestResolvedContext();
565
959
 
566
- // Context arrived after word boundaries may already have fired. Re-send
567
- // slow-lane context immediately so the next inference includes the thread.
568
- } catch (err) {
569
- _iterator.e(err);
570
- } finally {
571
- _iterator.f();
572
- }
960
+ // Context arrived after word boundaries may already have fired. Re-send
961
+ // slow-lane context immediately so the next inference includes the thread.
573
962
  if (currentView) {
574
963
  slowLaneClient.updateContext(buildSlowLaneText(currentView.state.doc.textContent, resolvedContext));
575
964
  }
@@ -580,11 +969,23 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
580
969
  * (no getContext, a request already in flight, or throttled). Callers that
581
970
  * track a retry budget should only count attempts where this returns true.
582
971
  */
583
- var refreshContext = function refreshContext(_ref5) {
972
+ var _refreshContext = function refreshContext(_ref5) {
584
973
  var source = _ref5.source,
585
974
  _ref5$allowThrottle = _ref5.allowThrottle,
586
975
  allowThrottle = _ref5$allowThrottle === void 0 ? true : _ref5$allowThrottle;
587
- if (!(options !== null && options !== void 0 && options.getContext) || contextRequestInFlight) {
976
+ if (!(options !== null && options !== void 0 && options.getContext)) {
977
+ return false;
978
+ }
979
+ if (contextRequestInFlight) {
980
+ // Only the reads that bypass the throttle are worth keeping: those are
981
+ // the ones the host asked for by name, and a scope change travels among
982
+ // them with no second channel to arrive on, so losing one would strand
983
+ // the eviction until something else happened to move. A word-boundary
984
+ // poll is a retry loop for context that has not landed yet, and the read
985
+ // already open will bring it.
986
+ if (!allowThrottle) {
987
+ queuedContextRefreshSource = source;
988
+ }
588
989
  return false;
589
990
  }
590
991
  var now = Date.now();
@@ -611,6 +1012,14 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
611
1012
  });
612
1013
  }).finally(function () {
613
1014
  contextRequestInFlight = false;
1015
+ var queuedSource = queuedContextRefreshSource;
1016
+ queuedContextRefreshSource = undefined;
1017
+ if (queuedSource !== undefined && !destroyed) {
1018
+ _refreshContext({
1019
+ source: queuedSource,
1020
+ allowThrottle: false
1021
+ });
1022
+ }
614
1023
  });
615
1024
  return true;
616
1025
  };
@@ -643,6 +1052,48 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
643
1052
  }
644
1053
  cancelActiveDecision();
645
1054
  };
1055
+
1056
+ /**
1057
+ * Offer a harvested inline-code surface on a prefix the scored path left
1058
+ * empty. Returns whether one was committed.
1059
+ */
1060
+ var commitHarvestedSuggestion = function commitHarvestedSuggestion(view, decision) {
1061
+ var harvester = inlineCodeHarvester;
1062
+ if (!harvester || !isInlineCodeHarvestEnabled) {
1063
+ return false;
1064
+ }
1065
+ if (!scoredPathFinishedEmpty(decision.textBefore)) {
1066
+ return false;
1067
+ }
1068
+ var typedPrefix = getTrailingSurfaceToken(decision.textBefore);
1069
+ var match = harvester.findHarvestedCompletion(typedPrefix);
1070
+ if (!match) {
1071
+ return false;
1072
+ }
1073
+ if (isSurfaceInAcceptCooldown(match.surface) || repeatsPrecedingText(match, decision.textBefore)) {
1074
+ return false;
1075
+ }
1076
+ var suggestion = showGhostText(view, buildHarvestPrediction(match), decision.position, decision.revision, decision.startedAt, match.typedPrefixLength);
1077
+ if (!suggestion) {
1078
+ return false;
1079
+ }
1080
+ cancelActiveDecision();
1081
+ ctcTag('harvest', "offered \"".concat(match.surface, "\" for \"").concat(typedPrefix, "\" \xB7 ").concat(match.collapsedBy, " \xB7 ").concat(match.replyOccurrences, " reply / ").concat(match.documentSpans, " doc sightings").concat(match.rivalSurfaces.length > 0 ? " \xB7 over ".concat(match.rivalSurfaces.join(', ')) : ''), CTC_STYLES.lm);
1082
+ if (suggestion.ghostText !== lastShownGhostText) {
1083
+ var _api$analytics5;
1084
+ lastShownGhostText = suggestion.ghostText;
1085
+ api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 || _api$analytics5.actions.fireAnalyticsEvent({
1086
+ action: ACTION.SUGGESTION_VIEWED,
1087
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
1088
+ eventType: EVENT_TYPE.TRACK,
1089
+ attributes: {
1090
+ completionSource: completionSourceFor(suggestion),
1091
+ surface: surface
1092
+ }
1093
+ });
1094
+ }
1095
+ return true;
1096
+ };
646
1097
  var evaluateActiveDecision = function evaluateActiveDecision(view, revision) {
647
1098
  var decision = activeDecision;
648
1099
  if (!decision || decision.revision !== revision || !isDecisionCurrent(view, decision)) {
@@ -679,8 +1130,11 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
679
1130
  }
680
1131
  var prediction = predict(decision.textBefore);
681
1132
  if (!prediction || prediction.text.length === 0) {
682
- // Remain in `collecting` until an async signal arrives or the hard
683
- // deadline expires. We never display a provisional fallback.
1133
+ // Only where the scored path has finished and come away with nothing
1134
+ // does a harvested surface get to answer; otherwise remain in
1135
+ // `collecting` until an async signal arrives or the hard deadline
1136
+ // expires. We never display a provisional fallback.
1137
+ commitHarvestedSuggestion(view, decision);
684
1138
  return;
685
1139
  }
686
1140
  var readyAtMs = performance.now() - decision.startedAt;
@@ -720,14 +1174,14 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
720
1174
  });
721
1175
  }
722
1176
  if (suggestion.ghostText !== lastShownGhostText) {
723
- var _api$analytics5;
1177
+ var _api$analytics6;
724
1178
  lastShownGhostText = suggestion.ghostText;
725
- api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 || _api$analytics5.actions.fireAnalyticsEvent({
1179
+ api === null || api === void 0 || (_api$analytics6 = api.analytics) === null || _api$analytics6 === void 0 || _api$analytics6.actions.fireAnalyticsEvent({
726
1180
  action: ACTION.SUGGESTION_VIEWED,
727
1181
  actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
728
1182
  eventType: EVENT_TYPE.TRACK,
729
1183
  attributes: {
730
- completionSource: getCompletionSource(),
1184
+ completionSource: completionSourceFor(suggestion),
731
1185
  surface: surface
732
1186
  }
733
1187
  });
@@ -911,11 +1365,13 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
911
1365
  });
912
1366
  },
913
1367
  Escape: function Escape(state, dispatch) {
1368
+ var _autocompletePluginKe;
1369
+ var dismissed = (_autocompletePluginKe = autocompletePluginKey.getState(state)) === null || _autocompletePluginKe === void 0 ? void 0 : _autocompletePluginKe.suggestion;
914
1370
  var didClear = clearGhostText(state, dispatch);
915
1371
  if (didClear) {
916
1372
  cancelActiveDecision();
917
1373
  dismissedContext = getTextBeforeCursor(state);
918
- fireSuggestionDismissedAnalytics('escape');
1374
+ fireSuggestionDismissedAnalytics('escape', dismissed !== null && dismissed !== void 0 ? dismissed : null);
919
1375
  }
920
1376
  return didClear;
921
1377
  }
@@ -929,7 +1385,7 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
929
1385
  if (pluginState !== null && pluginState !== void 0 && pluginState.ghostText) {
930
1386
  clearGhostText(view.state, view.dispatch);
931
1387
  cancelActiveDecision();
932
- fireSuggestionDismissedAnalytics('blur');
1388
+ fireSuggestionDismissedAnalytics('blur', pluginState.suggestion);
933
1389
  }
934
1390
  return false;
935
1391
  },
@@ -938,12 +1394,14 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
938
1394
  return false;
939
1395
  }
940
1396
  if (!event.target.closest('[data-autocomplete-ghost="true"]')) {
1397
+ var _autocompletePluginKe2;
1398
+ var dismissed = (_autocompletePluginKe2 = autocompletePluginKey.getState(view.state)) === null || _autocompletePluginKe2 === void 0 ? void 0 : _autocompletePluginKe2.suggestion;
941
1399
  var didClear = clearGhostText(view.state, view.dispatch);
942
1400
  if (didClear) {
943
1401
  cancelActiveDecision();
944
1402
  dismissedContext = getTextBeforeCursor(view.state);
945
1403
  lastShownGhostText = '';
946
- fireSuggestionDismissedAnalytics('click');
1404
+ fireSuggestionDismissedAnalytics('click', dismissed !== null && dismissed !== void 0 ? dismissed : null);
947
1405
  }
948
1406
  return false;
949
1407
  }
@@ -959,17 +1417,19 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
959
1417
  }
960
1418
  return accepted;
961
1419
  },
962
- focus: function focus() {
963
- var _options$useLocalMode, _options$useLocalMode2, _options$useLocalMode3;
1420
+ focus: function focus(view) {
1421
+ var _options$useLocalMode2, _options$useLocalMode3;
964
1422
  if (!isAutocompleteEnabled) {
965
1423
  return false;
966
1424
  }
967
- loadDefaultVocabulary({
968
- isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false,
969
- surface: surface
1425
+ void ensureVocabularyReady();
1426
+ // First point at which the session is known to be using the input,
1427
+ // which is where the rest of the artifacts are requested too.
1428
+ void ensureInlineCodeHarvester().then(function () {
1429
+ return harvestDocument(view.state.doc, 'focus');
970
1430
  }).catch(function (error) {
971
1431
  logException(error, {
972
- location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
1432
+ location: 'editor-plugin-autocomplete/harvestDocument'
973
1433
  });
974
1434
  });
975
1435
  loadVectorsAsync({
@@ -990,7 +1450,7 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
990
1450
  });
991
1451
  if (!hasIngestedPage) {
992
1452
  hasIngestedPage = true;
993
- refreshContext({
1453
+ _refreshContext({
994
1454
  source: 'focus',
995
1455
  allowThrottle: false
996
1456
  });
@@ -1003,12 +1463,14 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
1003
1463
  // Capture up front so a subscription notification before the first PM
1004
1464
  // transaction can still drive slowLaneClient.updateContext (gated on currentView).
1005
1465
  currentView = editorView;
1466
+ mountedEditors.add(reprimeSessionLearning);
1006
1467
 
1007
1468
  // Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
1008
1469
  if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
1009
1470
  unsubscribeFromContextUpdates = options.subscribeToContextUpdates(function () {
1010
- // Bypass throttle for freshness; the in-flight guard prevents overlap.
1011
- refreshContext({
1471
+ // Bypass throttle for freshness; a read still open defers this one
1472
+ // rather than overlapping it.
1473
+ _refreshContext({
1012
1474
  source: 'subscription',
1013
1475
  allowThrottle: false
1014
1476
  });
@@ -1033,19 +1495,24 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
1033
1495
  maybeUpdateSessionFrequency(view, prevState);
1034
1496
  var textBefore = getTextBeforeCursor(view.state);
1035
1497
  if (isWordBoundary(textBefore)) {
1036
- var _resolvedContext;
1498
+ var _resolvedContext5;
1037
1499
  slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
1038
1500
 
1501
+ // The backtick input rule has fired by the time a span is
1502
+ // finished, so a word boundary is the earliest point the mark
1503
+ // exists to be found.
1504
+ harvestDocument(view.state.doc, 'word-boundary');
1505
+
1039
1506
  // Context may not have resolved on first focus (e.g. comment
1040
1507
  // thread still loading). Retry on word boundaries until we have
1041
1508
  // the parent comment, throttled so we don't refetch constantly
1042
1509
  // and capped so non-comment editors stop retrying entirely.
1043
1510
  // Skipped for push-channel hosts (subscribeToContextUpdates): they
1044
1511
  // never grow parentCommentContent, so polling only burns the budget.
1045
- if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
1512
+ if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext5 = resolvedContext) !== null && _resolvedContext5 !== void 0 && _resolvedContext5.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
1046
1513
  // Only count the attempt when a fetch actually started, so an
1047
1514
  // in-flight or throttled no-op doesn't burn the retry budget.
1048
- if (refreshContext({
1515
+ if (_refreshContext({
1049
1516
  source: 'word-boundary'
1050
1517
  })) {
1051
1518
  wordBoundaryRefreshAttempts++;
@@ -1069,7 +1536,35 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
1069
1536
  if (hasDestroy(slowLaneClient)) {
1070
1537
  slowLaneClient.destroy();
1071
1538
  }
1072
- ingestedContextTexts.clear();
1539
+ queuedContextRefreshSource = undefined;
1540
+ pendingSessionContextTexts.clear();
1541
+ sessionIngestedContextTexts.clear();
1542
+ pendingHarvestTexts.clear();
1543
+ harvestedContextTexts.clear();
1544
+ // The harvested set holds one conversation's user and assistant
1545
+ // content, and the L1 boosts describe what that conversation was
1546
+ // about. Both live at module scope, so they have to be emptied here
1547
+ // or they leak into whichever editor mounts next — but only once no
1548
+ // editor is left to be using them. The scope key goes with them, and
1549
+ // only with them: clearing it while another editor is still mounted
1550
+ // would leave that editor's next read looking like a first one, with
1551
+ // nothing to compare against and everything still loaded.
1552
+ mountedEditors.delete(reprimeSessionLearning);
1553
+ if (mountedEditors.size === 0) {
1554
+ var _loadedInlineCodeHarv2;
1555
+ (_loadedInlineCodeHarv2 = loadedInlineCodeHarvester) === null || _loadedInlineCodeHarv2 === void 0 || _loadedInlineCodeHarv2.resetInlineCodeHarvest();
1556
+ // Only for a host that scopes its context, which is the one asking
1557
+ // for its learning to end with the conversation. A host that sends
1558
+ // no scope has boosts belonging to the page it is on, and that page
1559
+ // outlives an editor being unmounted — a Confluence comment box is
1560
+ // closed far more often than the page under it changes.
1561
+ if (contextScopeKey !== undefined) {
1562
+ resetSessionBoosts();
1563
+ contextScopeKey = undefined;
1564
+ }
1565
+ }
1566
+ inlineCodeHarvester = null;
1567
+ inlineCodeHarvesterPromise = undefined;
1073
1568
  }
1074
1569
  };
1075
1570
  }