@atlaskit/editor-plugin-autocomplete 9.1.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.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ var _typeof = require("@babel/runtime/helpers/typeof");
4
5
  Object.defineProperty(exports, "__esModule", {
5
6
  value: true
6
7
  });
@@ -19,11 +20,13 @@ var _localSlowLaneClient = require("./local-slow-lane-client");
19
20
  var _scoringPipeline = require("./scoring-pipeline");
20
21
  var _slowLaneClient = require("./slow-lane-client");
21
22
  var _textPredictor = require("./text-predictor");
23
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
22
24
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
23
25
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
24
26
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
25
27
  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; }
26
- function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
28
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } // Type-only, so the harvester chunk is still reached exclusively through the
29
+ // dynamic import in `ensureInlineCodeHarvester`.
27
30
  var autocompletePluginKey = exports.autocompletePluginKey = new _state.PluginKey('autocomplete');
28
31
  var PREDICTION_COALESCE_MS = 0;
29
32
  var GHOST_DECISION_BUDGET_MS = 100;
@@ -34,6 +37,32 @@ var CONTEXT_REFRESH_THROTTLE_MS = 1000;
34
37
  // version of the (potentially hundreds-of-KB) page content for the plugin's
35
38
  // lifetime. Eviction is FIFO; a re-ingest of an evicted text is harmless.
36
39
  var MAX_INGESTED_CONTEXT_TEXTS = 50;
40
+ // Re-prime callback of every mounted editor. The harvested set and the L1
41
+ // boosts are shared by all of them, and more than one is mounted routinely — a
42
+ // comment box alongside its replies, or a comment box alongside the chat input.
43
+ // Membership doubles as the mount count, so a teardown only clears the shared
44
+ // stores once the last editor has gone.
45
+ //
46
+ // Decision: one pot, shared across hosts. Terms harvested in a comment box can
47
+ // be offered in the chat input and the other way round. Both are content from
48
+ // the page the reader is looking at, so the mixing is between things they can
49
+ // already see; partitioning the boosts would mean a per-host overlay on every
50
+ // trie node, which the scoring path reads on every keystroke.
51
+ var mountedEditors = new Set();
52
+ // The scope that shared learning belongs to, as last reported by a host that
53
+ // scopes its context. Held here rather than per editor because the data it
54
+ // guards is shared and some hosts replace one editor with another across a
55
+ // navigation: an instance-local key would read as unset on exactly the change it
56
+ // exists to catch, while the previous scope's terms carried on in these globals.
57
+ // Undefined until a scoping host says otherwise. An editor that sends no key
58
+ // still shares the pot being emptied, so it is re-primed from the context it
59
+ // already holds rather than left with learning it can no longer see.
60
+ var contextScopeKey;
61
+ // Set by whichever editor first finished importing the harvester chunk. The
62
+ // reset paths go through this rather than their own copy, because the editor
63
+ // that has to clear the set is not always one that imported it — the set is one
64
+ // per module, and nothing can be in it unless some editor got this far.
65
+ var loadedInlineCodeHarvester = null;
37
66
  // Caps how many times the word-boundary path will retry getContext() while the
38
67
  // parent comment is still missing. Combined with the 1s throttle this gives a
39
68
  // ~5s window to cover a still-loading comment thread, then stops permanently so
@@ -110,10 +139,14 @@ var advanceGhostThroughTypedCharacter = function advanceGhostThroughTypedCharact
110
139
  ghostPosition: nextPosition,
111
140
  decorationSet: (0, _ghostTextDecoration.createGhostTextDecorationSet)(tr.doc, nextPosition, remaining),
112
141
  // `surface` stays the full candidate so cooldown and analytics still
113
- // describe the suggestion that was originally committed.
142
+ // describe the suggestion that was originally committed. The replaced
143
+ // prefix grows with the keystroke: it is the run the insertion overwrites,
144
+ // and the user has just typed one more character of it.
114
145
  suggestion: _objectSpread(_objectSpread({}, suggestion), {}, {
115
146
  ghostText: remaining,
116
147
  position: nextPosition
148
+ }, suggestion.replacesTypedPrefixLength === undefined ? {} : {
149
+ replacesTypedPrefixLength: suggestion.replacesTypedPrefixLength + 1
117
150
  })
118
151
  });
119
152
  };
@@ -148,9 +181,12 @@ var getTextBeforeCursor = function getTextBeforeCursor(state) {
148
181
  }
149
182
  return fullText.slice(-maxChars);
150
183
  };
151
- var getTrailingSurfacePrefixLength = function getTrailingSurfacePrefixLength(text) {
184
+ var getTrailingSurfaceToken = function getTrailingSurfaceToken(text) {
152
185
  var _text$trimEnd$match$, _text$trimEnd$match;
153
- 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;
186
+ 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$ : '';
187
+ };
188
+ var getTrailingSurfacePrefixLength = function getTrailingSurfacePrefixLength(text) {
189
+ return getTrailingSurfaceToken(text).length;
154
190
  };
155
191
 
156
192
  /**
@@ -164,11 +200,13 @@ var setAutocompleteMeta = function setAutocompleteMeta(tr, meta) {
164
200
  * Apply a ghost text suggestion to the editor state.
165
201
  */
166
202
  var lastShownGhostText = '';
167
- var showGhostText = function showGhostText(view, prediction, position, revision, decisionStartedAt) {
203
+ var showGhostText = function showGhostText(view, prediction, position, revision, decisionStartedAt, replacesTypedPrefixLength) {
168
204
  try {
169
205
  var state = view.state,
170
206
  dispatch = view.dispatch;
171
- var suggestion = {
207
+ var suggestion = _objectSpread(_objectSpread({}, replacesTypedPrefixLength === undefined ? {} : {
208
+ replacesTypedPrefixLength: replacesTypedPrefixLength
209
+ }), {}, {
172
210
  decisionLatencyMs: performance.now() - decisionStartedAt,
173
211
  evidenceTier: prediction.evidenceTier,
174
212
  ghostText: prediction.text,
@@ -182,7 +220,7 @@ var showGhostText = function showGhostText(view, prediction, position, revision,
182
220
  surface: prediction.surface,
183
221
  termType: prediction.termType,
184
222
  winnerMargin: prediction.winnerMargin
185
- };
223
+ });
186
224
  var decorationSet = (0, _ghostTextDecoration.createGhostTextDecorationSet)(state.doc, position, prediction.text);
187
225
  var tr = setAutocompleteMeta(state.tr, {
188
226
  ghostText: prediction.text,
@@ -237,10 +275,31 @@ var acceptGhostText = function acceptGhostText(state, dispatch) {
237
275
  }
238
276
  if (dispatch) {
239
277
  try {
278
+ var _suggestion$replacesT;
240
279
  var ghostText = pluginState.ghostText,
241
280
  ghostPosition = pluginState.ghostPosition,
242
281
  suggestion = pluginState.suggestion;
243
- var tr = state.tr.insertText(ghostText, ghostPosition);
282
+ // Replacing back over the typed prefix rewrites it in the surface's own
283
+ // casing; every other path appends and leaves the prefix untouched.
284
+ var replaceFrom = Math.max(0, ghostPosition - Math.min((_suggestion$replacesT = suggestion.replacesTypedPrefixLength) !== null && _suggestion$replacesT !== void 0 ? _suggestion$replacesT : 0, ghostPosition));
285
+ var tr = state.tr;
286
+ if (replaceFrom === ghostPosition) {
287
+ tr = tr.insertText(ghostText, ghostPosition);
288
+ } else {
289
+ tr = tr.insertText(suggestion.surface, replaceFrom, ghostPosition);
290
+ // Only the harvested path replaces, and what authorized it was the
291
+ // surface being marked as code somewhere in the session. Inserting it
292
+ // as plain text loses that, so the identifier the user accepted reads
293
+ // as prose while the same identifier they typed by hand does not.
294
+ var codeMark = state.schema.marks.code;
295
+ if (codeMark) {
296
+ tr = tr.addMark(replaceFrom, replaceFrom + suggestion.surface.length, codeMark.create());
297
+ // The backtick input rule never ran, so nothing else will close this
298
+ // mark. Without dropping it from the stored set the next character
299
+ // the user types continues the code span.
300
+ tr = tr.removeStoredMark(codeMark);
301
+ }
302
+ }
244
303
  tr = setAutocompleteMeta(tr, {
245
304
  ghostText: '',
246
305
  ghostPosition: -1,
@@ -328,6 +387,71 @@ var getLeadingTextCharacter = function getLeadingTextCharacter(text) {
328
387
  }
329
388
  return String.fromCodePoint(firstCodePoint);
330
389
  };
390
+
391
+ /**
392
+ * The one verdict a harvested surface may speak on.
393
+ *
394
+ * `no-candidate` is the only abstention that means no vocabulary reaches the
395
+ * prefix at all. Every other one — a margin the model cannot clear, a rival
396
+ * still being read — describes known words in contention, where a session
397
+ * surface with no score behind it would be overruling the ranker rather than
398
+ * filling a gap it left.
399
+ */
400
+ var HARVEST_ELIGIBLE_ABSTAIN_REASON = 'no-candidate';
401
+
402
+ /**
403
+ * Whether the scored path has finished with this exact prefix and found nothing.
404
+ *
405
+ * The 100ms deadline expiring is not the same answer: it means the ranker was
406
+ * still working, and displaying then would race a suggestion that is about to
407
+ * arrive. So the harvest path reads the recorded verdict rather than the clock.
408
+ */
409
+ var scoredPathFinishedEmpty = function scoredPathFinishedEmpty(textBefore) {
410
+ var outcome = (0, _textPredictor.getLastPredictionOutcome)();
411
+ return outcome !== null && outcome.textBefore === textBefore && !outcome.awaitingAsyncEvidence && outcome.abstainReason === HARVEST_ELIGIBLE_ABSTAIN_REASON;
412
+ };
413
+
414
+ /**
415
+ * Whether the surface is already sitting immediately before the typed prefix.
416
+ *
417
+ * Completing `ml-s` to `ml-studio` right after `ml-studio` produces the echo the
418
+ * scored path's repetition guard exists to stop, and this path does not go
419
+ * through arbitration to inherit it.
420
+ */
421
+ var repeatsPrecedingText = function repeatsPrecedingText(match, textBefore) {
422
+ var trimmed = textBefore.trimEnd();
423
+ var preceding = trimmed.slice(0, trimmed.length - match.typedPrefixLength).trimEnd();
424
+ return preceding.toLowerCase().endsWith(match.surface.toLowerCase());
425
+ };
426
+
427
+ /**
428
+ * Dress a harvested match as a prediction so it commits through the same path as
429
+ * a scored one.
430
+ *
431
+ * The scoring fields are zeroed rather than invented: there is no posterior, no
432
+ * margin and no shortlist behind this surface, and `session-harvest` on the
433
+ * evidence tier is what says so wherever the snapshot is read.
434
+ */
435
+ var buildHarvestPrediction = function buildHarvestPrediction(match) {
436
+ return {
437
+ evidenceDepth: {
438
+ totalChars: 0,
439
+ totalTokens: 0,
440
+ verifiedChars: 0,
441
+ verifiedTokens: 0
442
+ },
443
+ evidenceTier: 'session-harvest',
444
+ meanTokenLogProbability: 0,
445
+ poolHeldExtension: false,
446
+ posterior: 0,
447
+ rankScore: 0,
448
+ shortlistSize: 0,
449
+ surface: match.surface,
450
+ termType: 'word',
451
+ text: match.ghostText,
452
+ winnerMargin: 0
453
+ };
454
+ };
331
455
  var isEnglishLocale = function isEnglishLocale(locale) {
332
456
  if (!locale) {
333
457
  return false;
@@ -339,6 +463,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
339
463
  var surface = (_options$surface = options === null || options === void 0 ? void 0 : options.surface) !== null && _options$surface !== void 0 ? _options$surface : 'editor';
340
464
  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;
341
465
  var isAutocompleteEnabled = isEnglishLocale(locale);
466
+ var isInlineCodeHarvestEnabled = isAutocompleteEnabled && (options === null || options === void 0 ? void 0 : options.harvestInlineCode) === true;
342
467
  var debounceTimer = null;
343
468
  var decisionDeadlineTimer = null;
344
469
  var hasIngestedPage = false;
@@ -381,14 +506,26 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
381
506
  }
382
507
  return options !== null && options !== void 0 && options.useLocalModel ? 'localLlm' : 'server';
383
508
  };
384
- var fireSuggestionDismissedAnalytics = function fireSuggestionDismissedAnalytics(reason) {
509
+
510
+ /**
511
+ * Which path produced a given suggestion.
512
+ *
513
+ * A harvested surface is reported as `harvest` rather than by the slow-lane
514
+ * state it happened to be shown under, so its views and acceptances stay
515
+ * separable from the scored path's and cannot quietly move the headline
516
+ * acceptance rate.
517
+ */
518
+ var completionSourceFor = function completionSourceFor(suggestion) {
519
+ return (suggestion === null || suggestion === void 0 ? void 0 : suggestion.evidenceTier) === 'session-harvest' ? 'harvest' : getCompletionSource();
520
+ };
521
+ var fireSuggestionDismissedAnalytics = function fireSuggestionDismissedAnalytics(reason, suggestion) {
385
522
  var _api$analytics;
386
523
  api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({
387
524
  action: _analytics.ACTION.SUGGESTION_DISMISSED,
388
525
  actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
389
526
  eventType: _analytics.EVENT_TYPE.TRACK,
390
527
  attributes: {
391
- completionSource: getCompletionSource(),
528
+ completionSource: completionSourceFor(suggestion),
392
529
  reason: reason,
393
530
  surface: surface
394
531
  }
@@ -443,7 +580,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
443
580
  actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
444
581
  eventType: _analytics.EVENT_TYPE.TRACK,
445
582
  attributes: {
446
- completionSource: getCompletionSource(),
583
+ completionSource: completionSourceFor(suggestion),
447
584
  suggestionLength: suggestionLength,
448
585
  typedLength: typedLength,
449
586
  kssDelta: kssDelta,
@@ -509,6 +646,10 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
509
646
  var registeredSlowLaneStatus = (0, _slowLaneClient.getDefaultSlowLaneClientStatus)();
510
647
  (0, _debugMode.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' ? _debugMode.CTC_STYLES.good : _debugMode.CTC_STYLES.warn);
511
648
  var contextRequestInFlight = false;
649
+ // A read the host asked for while another was open, kept so it can be made
650
+ // once that one settles. One slot, not a queue: a burst of notifications only
651
+ // ever means "read again", and the last of them describes the current state.
652
+ var queuedContextRefreshSource;
512
653
  var lastContextRefreshAt = 0;
513
654
  // Bounds the word-boundary retry loop so it terminates even when the editor is
514
655
  // not in a comment thread (parentCommentContent never resolves).
@@ -525,6 +666,17 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
525
666
  var sessionIngestedContextTexts = new Set();
526
667
  var isVocabularyReady = false;
527
668
  var vocabularyLoadPromise;
669
+
670
+ // Context text waiting on the harvester chunk, the vocabulary, or both. The
671
+ // harvester drops surfaces the vocabulary already holds, so intake before the
672
+ // load has settled would admit ordinary words no one needs completed.
673
+ var pendingHarvestTexts = new Set();
674
+ // Separate from `sessionIngestedContextTexts` because the two consumers settle
675
+ // at different times: reply occurrences accumulate, so text fed twice would
676
+ // count twice.
677
+ var harvestedContextTexts = new Set();
678
+ var inlineCodeHarvester = null;
679
+ var inlineCodeHarvesterPromise;
528
680
  var addBoundedContextText = function addBoundedContextText(texts, text) {
529
681
  texts.add(text);
530
682
  // Evict oldest entries (Set preserves insertion order) to bound memory.
@@ -557,10 +709,91 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
557
709
  _iterator.f();
558
710
  }
559
711
  };
712
+ var flushPendingHarvestTexts = function flushPendingHarvestTexts() {
713
+ var harvester = inlineCodeHarvester;
714
+ if (!harvester || !isVocabularyReady || destroyed || pendingHarvestTexts.size === 0) {
715
+ return;
716
+ }
717
+ var _iterator2 = _createForOfIteratorHelper(pendingHarvestTexts),
718
+ _step2;
719
+ try {
720
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
721
+ var text = _step2.value;
722
+ if (!harvestedContextTexts.has(text)) {
723
+ harvester.harvestInlineCodeFromText(text);
724
+ addBoundedContextText(harvestedContextTexts, text);
725
+ }
726
+ pendingHarvestTexts.delete(text);
727
+ }
728
+ } catch (err) {
729
+ _iterator2.e(err);
730
+ } finally {
731
+ _iterator2.f();
732
+ }
733
+ harvester.logInlineCodeHarvest('context');
734
+ };
735
+
736
+ /**
737
+ * Load the harvester chunk, once, and only for a host that asked for it.
738
+ *
739
+ * Kept off the critical path in the same way the vocabulary and vector loads
740
+ * are: nothing is requested until the editor is focused, so a session that
741
+ * never types in the chat pays nothing for the feature.
742
+ */
743
+ var ensureInlineCodeHarvester = function ensureInlineCodeHarvester() {
744
+ if (!isInlineCodeHarvestEnabled || destroyed) {
745
+ return Promise.resolve(null);
746
+ }
747
+ inlineCodeHarvesterPromise !== null && inlineCodeHarvesterPromise !== void 0 ? inlineCodeHarvesterPromise : inlineCodeHarvesterPromise = Promise.resolve().then(function () {
748
+ return _interopRequireWildcard(require( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-inline-code-harvester" */
749
+ './inline-code-harvester'));
750
+ }).then(function (module) {
751
+ if (destroyed) {
752
+ return null;
753
+ }
754
+ inlineCodeHarvester = module;
755
+ loadedInlineCodeHarvester = module;
756
+ flushPendingHarvestTexts();
757
+ return module;
758
+ }).catch(function (error) {
759
+ // Clear the promise so a later focus can retry; the queued text is
760
+ // still held and is fed by whichever attempt succeeds.
761
+ inlineCodeHarvesterPromise = undefined;
762
+ (0, _monitoring.logException)(error, {
763
+ location: 'editor-plugin-autocomplete/loadInlineCodeHarvester'
764
+ });
765
+ return null;
766
+ });
767
+ return inlineCodeHarvesterPromise;
768
+ };
769
+
770
+ /**
771
+ * Re-read the document's code-marked spans.
772
+ *
773
+ * The backtick input rule consumes both delimiters on the closing tick, so a
774
+ * finished span in the live document is only findable through its mark — and
775
+ * only by walking, since a mark carries no notification.
776
+ */
777
+ var harvestDocument = function harvestDocument(doc, trigger) {
778
+ if (!isInlineCodeHarvestEnabled || destroyed) {
779
+ return;
780
+ }
781
+ var harvester = inlineCodeHarvester;
782
+ if (!harvester) {
783
+ void ensureInlineCodeHarvester();
784
+ return;
785
+ }
786
+ if (!isVocabularyReady) {
787
+ return;
788
+ }
789
+ harvester.harvestInlineCodeFromDoc(doc);
790
+ harvester.logInlineCodeHarvest(trigger);
791
+ };
560
792
  var ensureVocabularyReady = function ensureVocabularyReady() {
561
793
  var _options$useLocalMode;
562
794
  if (isVocabularyReady) {
563
795
  flushPendingSessionContext();
796
+ flushPendingHarvestTexts();
564
797
  return Promise.resolve();
565
798
  }
566
799
  vocabularyLoadPromise !== null && vocabularyLoadPromise !== void 0 ? vocabularyLoadPromise : vocabularyLoadPromise = (0, _textPredictor.loadDefaultVocabulary)({
@@ -572,6 +805,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
572
805
  }
573
806
  isVocabularyReady = true;
574
807
  flushPendingSessionContext();
808
+ flushPendingHarvestTexts();
575
809
  }).catch(function (error) {
576
810
  // Do not consume the pending text on failure. A later focus retries the
577
811
  // load and can still apply the original context exactly once.
@@ -591,14 +825,134 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
591
825
  // eslint-disable-next-line no-console
592
826
  console.log('%c[CTC:signal]%c getContext resolved', _debugMode.CTC_STYLES.brand, _debugMode.CTC_STYLES.body, {
593
827
  source: source,
828
+ scope: context === null || context === void 0 ? void 0 : context.contextScopeKey,
594
829
  hasParentComment: !!(context !== null && context !== void 0 && context.parentCommentContent),
595
830
  parentCommentPreview: context === null || context === void 0 || (_context$parentCommen = context.parentCommentContent) === null || _context$parentCommen === void 0 ? void 0 : _context$parentCommen.slice(0, 80),
596
831
  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,
597
832
  hasFullPage: !!(context !== null && context !== void 0 && context.fullPageContent)
598
833
  });
599
834
  };
835
+ var ingestContextText = function ingestContextText(text) {
836
+ if (!text) {
837
+ return;
838
+ }
839
+ if (!sessionIngestedContextTexts.has(text)) {
840
+ addBoundedContextText(pendingSessionContextTexts, text);
841
+ }
842
+ // Queued unconditionally rather than only once the chunk is present:
843
+ // context usually resolves in the same tick the load starts, and text
844
+ // dropped for arriving early is a reply that can never be harvested.
845
+ if (isInlineCodeHarvestEnabled && !harvestedContextTexts.has(text)) {
846
+ addBoundedContextText(pendingHarvestTexts, text);
847
+ }
848
+ };
849
+ var ingestResolvedContext = function ingestResolvedContext() {
850
+ var _resolvedContext, _resolvedContext2, _resolvedContext$sibl, _resolvedContext3;
851
+ ingestContextText((_resolvedContext = resolvedContext) === null || _resolvedContext === void 0 ? void 0 : _resolvedContext.fullPageContent);
852
+ ingestContextText((_resolvedContext2 = resolvedContext) === null || _resolvedContext2 === void 0 ? void 0 : _resolvedContext2.parentCommentContent);
853
+ var _iterator3 = _createForOfIteratorHelper((_resolvedContext$sibl = (_resolvedContext3 = resolvedContext) === null || _resolvedContext3 === void 0 ? void 0 : _resolvedContext3.siblingCommentsContents) !== null && _resolvedContext$sibl !== void 0 ? _resolvedContext$sibl : []),
854
+ _step3;
855
+ try {
856
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
857
+ var siblingCommentContent = _step3.value;
858
+ ingestContextText(siblingCommentContent);
859
+ }
860
+ } catch (err) {
861
+ _iterator3.e(err);
862
+ } finally {
863
+ _iterator3.f();
864
+ }
865
+ flushPendingSessionContext();
866
+ flushPendingHarvestTexts();
867
+ };
868
+
869
+ /**
870
+ * Put back what this editor had contributed to the pot another editor just
871
+ * emptied.
872
+ *
873
+ * The scope that ended belongs to the host that reported it, not to everyone
874
+ * sharing these globals. Without this an editor alongside it — a comment box
875
+ * under a chat panel that switched conversation — is left with none of its
876
+ * learning and no way back to it: the dedupe sets below are per editor, so
877
+ * its own page reads as already ingested and is skipped from then on.
878
+ *
879
+ * Nothing is re-fetched. The context this editor resolved is still held, and
880
+ * what it describes has not changed just because another host moved on.
881
+ *
882
+ * Only for an editor that reported no scope. One that did is party to the
883
+ * same scope system and the scope just dropped may well be its own: a
884
+ * Confluence page transition mounts the incoming editor before tearing down
885
+ * the outgoing one, and the outgoing one is still holding the page that was
886
+ * left. Putting that back is exactly what the eviction was for.
887
+ */
888
+ var reprimeSessionLearning = function reprimeSessionLearning() {
889
+ var _resolvedContext4;
890
+ if (destroyed || ((_resolvedContext4 = resolvedContext) === null || _resolvedContext4 === void 0 ? void 0 : _resolvedContext4.contextScopeKey) !== undefined) {
891
+ return;
892
+ }
893
+ pendingSessionContextTexts.clear();
894
+ sessionIngestedContextTexts.clear();
895
+ pendingHarvestTexts.clear();
896
+ harvestedContextTexts.clear();
897
+ ingestResolvedContext();
898
+ if (currentView) {
899
+ harvestDocument(currentView.state.doc, 'reprime');
900
+ }
901
+ };
902
+
903
+ /**
904
+ * Drop everything learned for the scope that just ended.
905
+ *
906
+ * Both stores are claims about what is being discussed, and neither survives
907
+ * the discussion changing: L1 boosts would keep a page's words ranked above
908
+ * the next page's, and the harvested set holds one conversation's content.
909
+ * The dedupe sets go too, so the context that arrives next is treated as
910
+ * unseen and re-primes both — which is what makes this safe to do on a page
911
+ * change, since the transcript is re-ingested along with the new page.
912
+ *
913
+ * The stores are shared, so every other mounted editor is put back in the
914
+ * same tick from context it already holds. Only the scope that ended is
915
+ * actually dropped.
916
+ */
917
+ var resetSessionScopedLearning = function resetSessionScopedLearning(reason) {
918
+ var _loadedInlineCodeHarv;
919
+ (0, _textPredictor.resetSessionBoosts)();
920
+ (_loadedInlineCodeHarv = loadedInlineCodeHarvester) === null || _loadedInlineCodeHarv === void 0 || _loadedInlineCodeHarv.resetInlineCodeHarvest();
921
+ resolvedContext = undefined;
922
+ pendingSessionContextTexts.clear();
923
+ sessionIngestedContextTexts.clear();
924
+ pendingHarvestTexts.clear();
925
+ harvestedContextTexts.clear();
926
+ (0, _debugMode.ctcTag)('init', "session-scoped learning reset \xB7 ".concat(reason), _debugMode.CTC_STYLES.dim);
927
+ var _iterator4 = _createForOfIteratorHelper(mountedEditors),
928
+ _step4;
929
+ try {
930
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
931
+ var reprimeOther = _step4.value;
932
+ if (reprimeOther !== reprimeSessionLearning) {
933
+ reprimeOther();
934
+ }
935
+ }
936
+ } catch (err) {
937
+ _iterator4.e(err);
938
+ } finally {
939
+ _iterator4.f();
940
+ }
941
+ };
600
942
  var applyContext = function applyContext(context) {
601
- var _context$siblingComme6;
943
+ // A host that scopes its context tells us which scope each read belongs to.
944
+ // A read with no recorded key to compare against only records: that is the
945
+ // first since the last editor went away, so there is nothing left to throw
946
+ // away and resetting would drop the priming focus just started. A read from
947
+ // a newly mounted editor is not that case — the key outlives the editor
948
+ // precisely so a host that remounts across a navigation still evicts.
949
+ if (context.contextScopeKey !== undefined && context.contextScopeKey !== contextScopeKey) {
950
+ if (contextScopeKey !== undefined) {
951
+ resetSessionScopedLearning("scope ".concat(contextScopeKey, " \u2192 ").concat(context.contextScopeKey));
952
+ }
953
+ contextScopeKey = context.contextScopeKey;
954
+ }
955
+
602
956
  // Merge rather than replace: the word-boundary retry may resolve only a
603
957
  // late-arriving field (e.g. parentCommentContent) without re-sending
604
958
  // fullPageContent, so replacing would drop previously resolved context.
@@ -610,27 +964,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
610
964
  return value !== undefined;
611
965
  }));
612
966
  resolvedContext = _objectSpread(_objectSpread({}, resolvedContext), definedContext);
613
- var ingestContextText = function ingestContextText(text) {
614
- if (!text || sessionIngestedContextTexts.has(text)) {
615
- return;
616
- }
617
- addBoundedContextText(pendingSessionContextTexts, text);
618
- };
619
- ingestContextText(context.fullPageContent);
620
- ingestContextText(context.parentCommentContent);
621
- var _iterator2 = _createForOfIteratorHelper((_context$siblingComme6 = context.siblingCommentsContents) !== null && _context$siblingComme6 !== void 0 ? _context$siblingComme6 : []),
622
- _step2;
623
- try {
624
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
625
- var siblingCommentContent = _step2.value;
626
- ingestContextText(siblingCommentContent);
627
- }
628
- } catch (err) {
629
- _iterator2.e(err);
630
- } finally {
631
- _iterator2.f();
632
- }
633
- flushPendingSessionContext();
967
+ ingestResolvedContext();
634
968
 
635
969
  // Context arrived after word boundaries may already have fired. Re-send
636
970
  // slow-lane context immediately so the next inference includes the thread.
@@ -644,11 +978,23 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
644
978
  * (no getContext, a request already in flight, or throttled). Callers that
645
979
  * track a retry budget should only count attempts where this returns true.
646
980
  */
647
- var refreshContext = function refreshContext(_ref5) {
981
+ var _refreshContext = function refreshContext(_ref5) {
648
982
  var source = _ref5.source,
649
983
  _ref5$allowThrottle = _ref5.allowThrottle,
650
984
  allowThrottle = _ref5$allowThrottle === void 0 ? true : _ref5$allowThrottle;
651
- if (!(options !== null && options !== void 0 && options.getContext) || contextRequestInFlight) {
985
+ if (!(options !== null && options !== void 0 && options.getContext)) {
986
+ return false;
987
+ }
988
+ if (contextRequestInFlight) {
989
+ // Only the reads that bypass the throttle are worth keeping: those are
990
+ // the ones the host asked for by name, and a scope change travels among
991
+ // them with no second channel to arrive on, so losing one would strand
992
+ // the eviction until something else happened to move. A word-boundary
993
+ // poll is a retry loop for context that has not landed yet, and the read
994
+ // already open will bring it.
995
+ if (!allowThrottle) {
996
+ queuedContextRefreshSource = source;
997
+ }
652
998
  return false;
653
999
  }
654
1000
  var now = Date.now();
@@ -675,6 +1021,14 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
675
1021
  });
676
1022
  }).finally(function () {
677
1023
  contextRequestInFlight = false;
1024
+ var queuedSource = queuedContextRefreshSource;
1025
+ queuedContextRefreshSource = undefined;
1026
+ if (queuedSource !== undefined && !destroyed) {
1027
+ _refreshContext({
1028
+ source: queuedSource,
1029
+ allowThrottle: false
1030
+ });
1031
+ }
678
1032
  });
679
1033
  return true;
680
1034
  };
@@ -707,6 +1061,48 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
707
1061
  }
708
1062
  cancelActiveDecision();
709
1063
  };
1064
+
1065
+ /**
1066
+ * Offer a harvested inline-code surface on a prefix the scored path left
1067
+ * empty. Returns whether one was committed.
1068
+ */
1069
+ var commitHarvestedSuggestion = function commitHarvestedSuggestion(view, decision) {
1070
+ var harvester = inlineCodeHarvester;
1071
+ if (!harvester || !isInlineCodeHarvestEnabled) {
1072
+ return false;
1073
+ }
1074
+ if (!scoredPathFinishedEmpty(decision.textBefore)) {
1075
+ return false;
1076
+ }
1077
+ var typedPrefix = getTrailingSurfaceToken(decision.textBefore);
1078
+ var match = harvester.findHarvestedCompletion(typedPrefix);
1079
+ if (!match) {
1080
+ return false;
1081
+ }
1082
+ if ((0, _textPredictor.isSurfaceInAcceptCooldown)(match.surface) || repeatsPrecedingText(match, decision.textBefore)) {
1083
+ return false;
1084
+ }
1085
+ var suggestion = showGhostText(view, buildHarvestPrediction(match), decision.position, decision.revision, decision.startedAt, match.typedPrefixLength);
1086
+ if (!suggestion) {
1087
+ return false;
1088
+ }
1089
+ cancelActiveDecision();
1090
+ (0, _debugMode.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(', ')) : ''), _debugMode.CTC_STYLES.lm);
1091
+ if (suggestion.ghostText !== lastShownGhostText) {
1092
+ var _api$analytics5;
1093
+ lastShownGhostText = suggestion.ghostText;
1094
+ api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 || _api$analytics5.actions.fireAnalyticsEvent({
1095
+ action: _analytics.ACTION.SUGGESTION_VIEWED,
1096
+ actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
1097
+ eventType: _analytics.EVENT_TYPE.TRACK,
1098
+ attributes: {
1099
+ completionSource: completionSourceFor(suggestion),
1100
+ surface: surface
1101
+ }
1102
+ });
1103
+ }
1104
+ return true;
1105
+ };
710
1106
  var evaluateActiveDecision = function evaluateActiveDecision(view, revision) {
711
1107
  var decision = activeDecision;
712
1108
  if (!decision || decision.revision !== revision || !isDecisionCurrent(view, decision)) {
@@ -743,8 +1139,11 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
743
1139
  }
744
1140
  var prediction = (0, _textPredictor.predict)(decision.textBefore);
745
1141
  if (!prediction || prediction.text.length === 0) {
746
- // Remain in `collecting` until an async signal arrives or the hard
747
- // deadline expires. We never display a provisional fallback.
1142
+ // Only where the scored path has finished and come away with nothing
1143
+ // does a harvested surface get to answer; otherwise remain in
1144
+ // `collecting` until an async signal arrives or the hard deadline
1145
+ // expires. We never display a provisional fallback.
1146
+ commitHarvestedSuggestion(view, decision);
748
1147
  return;
749
1148
  }
750
1149
  var readyAtMs = performance.now() - decision.startedAt;
@@ -784,14 +1183,14 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
784
1183
  });
785
1184
  }
786
1185
  if (suggestion.ghostText !== lastShownGhostText) {
787
- var _api$analytics5;
1186
+ var _api$analytics6;
788
1187
  lastShownGhostText = suggestion.ghostText;
789
- api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 || _api$analytics5.actions.fireAnalyticsEvent({
1188
+ api === null || api === void 0 || (_api$analytics6 = api.analytics) === null || _api$analytics6 === void 0 || _api$analytics6.actions.fireAnalyticsEvent({
790
1189
  action: _analytics.ACTION.SUGGESTION_VIEWED,
791
1190
  actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
792
1191
  eventType: _analytics.EVENT_TYPE.TRACK,
793
1192
  attributes: {
794
- completionSource: getCompletionSource(),
1193
+ completionSource: completionSourceFor(suggestion),
795
1194
  surface: surface
796
1195
  }
797
1196
  });
@@ -975,11 +1374,13 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
975
1374
  });
976
1375
  },
977
1376
  Escape: function Escape(state, dispatch) {
1377
+ var _autocompletePluginKe;
1378
+ var dismissed = (_autocompletePluginKe = autocompletePluginKey.getState(state)) === null || _autocompletePluginKe === void 0 ? void 0 : _autocompletePluginKe.suggestion;
978
1379
  var didClear = clearGhostText(state, dispatch);
979
1380
  if (didClear) {
980
1381
  cancelActiveDecision();
981
1382
  dismissedContext = getTextBeforeCursor(state);
982
- fireSuggestionDismissedAnalytics('escape');
1383
+ fireSuggestionDismissedAnalytics('escape', dismissed !== null && dismissed !== void 0 ? dismissed : null);
983
1384
  }
984
1385
  return didClear;
985
1386
  }
@@ -993,7 +1394,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
993
1394
  if (pluginState !== null && pluginState !== void 0 && pluginState.ghostText) {
994
1395
  clearGhostText(view.state, view.dispatch);
995
1396
  cancelActiveDecision();
996
- fireSuggestionDismissedAnalytics('blur');
1397
+ fireSuggestionDismissedAnalytics('blur', pluginState.suggestion);
997
1398
  }
998
1399
  return false;
999
1400
  },
@@ -1002,12 +1403,14 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
1002
1403
  return false;
1003
1404
  }
1004
1405
  if (!event.target.closest('[data-autocomplete-ghost="true"]')) {
1406
+ var _autocompletePluginKe2;
1407
+ var dismissed = (_autocompletePluginKe2 = autocompletePluginKey.getState(view.state)) === null || _autocompletePluginKe2 === void 0 ? void 0 : _autocompletePluginKe2.suggestion;
1005
1408
  var didClear = clearGhostText(view.state, view.dispatch);
1006
1409
  if (didClear) {
1007
1410
  cancelActiveDecision();
1008
1411
  dismissedContext = getTextBeforeCursor(view.state);
1009
1412
  lastShownGhostText = '';
1010
- fireSuggestionDismissedAnalytics('click');
1413
+ fireSuggestionDismissedAnalytics('click', dismissed !== null && dismissed !== void 0 ? dismissed : null);
1011
1414
  }
1012
1415
  return false;
1013
1416
  }
@@ -1023,12 +1426,21 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
1023
1426
  }
1024
1427
  return accepted;
1025
1428
  },
1026
- focus: function focus() {
1429
+ focus: function focus(view) {
1027
1430
  var _options$useLocalMode2, _options$useLocalMode3;
1028
1431
  if (!isAutocompleteEnabled) {
1029
1432
  return false;
1030
1433
  }
1031
1434
  void ensureVocabularyReady();
1435
+ // First point at which the session is known to be using the input,
1436
+ // which is where the rest of the artifacts are requested too.
1437
+ void ensureInlineCodeHarvester().then(function () {
1438
+ return harvestDocument(view.state.doc, 'focus');
1439
+ }).catch(function (error) {
1440
+ (0, _monitoring.logException)(error, {
1441
+ location: 'editor-plugin-autocomplete/harvestDocument'
1442
+ });
1443
+ });
1032
1444
  (0, _textPredictor.loadVectorsAsync)({
1033
1445
  isLocalLLM: (_options$useLocalMode2 = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode2 !== void 0 ? _options$useLocalMode2 : false,
1034
1446
  surface: surface
@@ -1047,7 +1459,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
1047
1459
  });
1048
1460
  if (!hasIngestedPage) {
1049
1461
  hasIngestedPage = true;
1050
- refreshContext({
1462
+ _refreshContext({
1051
1463
  source: 'focus',
1052
1464
  allowThrottle: false
1053
1465
  });
@@ -1060,12 +1472,14 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
1060
1472
  // Capture up front so a subscription notification before the first PM
1061
1473
  // transaction can still drive slowLaneClient.updateContext (gated on currentView).
1062
1474
  currentView = editorView;
1475
+ mountedEditors.add(reprimeSessionLearning);
1063
1476
 
1064
1477
  // Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
1065
1478
  if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
1066
1479
  unsubscribeFromContextUpdates = options.subscribeToContextUpdates(function () {
1067
- // Bypass throttle for freshness; the in-flight guard prevents overlap.
1068
- refreshContext({
1480
+ // Bypass throttle for freshness; a read still open defers this one
1481
+ // rather than overlapping it.
1482
+ _refreshContext({
1069
1483
  source: 'subscription',
1070
1484
  allowThrottle: false
1071
1485
  });
@@ -1090,19 +1504,24 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
1090
1504
  maybeUpdateSessionFrequency(view, prevState);
1091
1505
  var textBefore = getTextBeforeCursor(view.state);
1092
1506
  if ((0, _slowLaneClient.isWordBoundary)(textBefore)) {
1093
- var _resolvedContext;
1507
+ var _resolvedContext5;
1094
1508
  slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
1095
1509
 
1510
+ // The backtick input rule has fired by the time a span is
1511
+ // finished, so a word boundary is the earliest point the mark
1512
+ // exists to be found.
1513
+ harvestDocument(view.state.doc, 'word-boundary');
1514
+
1096
1515
  // Context may not have resolved on first focus (e.g. comment
1097
1516
  // thread still loading). Retry on word boundaries until we have
1098
1517
  // the parent comment, throttled so we don't refetch constantly
1099
1518
  // and capped so non-comment editors stop retrying entirely.
1100
1519
  // Skipped for push-channel hosts (subscribeToContextUpdates): they
1101
1520
  // never grow parentCommentContent, so polling only burns the budget.
1102
- if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
1521
+ if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext5 = resolvedContext) !== null && _resolvedContext5 !== void 0 && _resolvedContext5.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
1103
1522
  // Only count the attempt when a fetch actually started, so an
1104
1523
  // in-flight or throttled no-op doesn't burn the retry budget.
1105
- if (refreshContext({
1524
+ if (_refreshContext({
1106
1525
  source: 'word-boundary'
1107
1526
  })) {
1108
1527
  wordBoundaryRefreshAttempts++;
@@ -1126,8 +1545,35 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
1126
1545
  if (hasDestroy(slowLaneClient)) {
1127
1546
  slowLaneClient.destroy();
1128
1547
  }
1548
+ queuedContextRefreshSource = undefined;
1129
1549
  pendingSessionContextTexts.clear();
1130
1550
  sessionIngestedContextTexts.clear();
1551
+ pendingHarvestTexts.clear();
1552
+ harvestedContextTexts.clear();
1553
+ // The harvested set holds one conversation's user and assistant
1554
+ // content, and the L1 boosts describe what that conversation was
1555
+ // about. Both live at module scope, so they have to be emptied here
1556
+ // or they leak into whichever editor mounts next — but only once no
1557
+ // editor is left to be using them. The scope key goes with them, and
1558
+ // only with them: clearing it while another editor is still mounted
1559
+ // would leave that editor's next read looking like a first one, with
1560
+ // nothing to compare against and everything still loaded.
1561
+ mountedEditors.delete(reprimeSessionLearning);
1562
+ if (mountedEditors.size === 0) {
1563
+ var _loadedInlineCodeHarv2;
1564
+ (_loadedInlineCodeHarv2 = loadedInlineCodeHarvester) === null || _loadedInlineCodeHarv2 === void 0 || _loadedInlineCodeHarv2.resetInlineCodeHarvest();
1565
+ // Only for a host that scopes its context, which is the one asking
1566
+ // for its learning to end with the conversation. A host that sends
1567
+ // no scope has boosts belonging to the page it is on, and that page
1568
+ // outlives an editor being unmounted — a Confluence comment box is
1569
+ // closed far more often than the page under it changes.
1570
+ if (contextScopeKey !== undefined) {
1571
+ (0, _textPredictor.resetSessionBoosts)();
1572
+ contextScopeKey = undefined;
1573
+ }
1574
+ }
1575
+ inlineCodeHarvester = null;
1576
+ inlineCodeHarvesterPromise = undefined;
1131
1577
  }
1132
1578
  };
1133
1579
  }