@atlaskit/editor-plugin-autocomplete 8.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/dist/cjs/analytics/ufo.js +2 -1
  3. package/dist/cjs/pm-plugins/artifact-loader.js +135 -0
  4. package/dist/cjs/pm-plugins/artifacts-manifest.js +3 -0
  5. package/dist/cjs/pm-plugins/autocomplete-plugin.js +498 -160
  6. package/dist/cjs/pm-plugins/canonical-lm-scoring.js +371 -0
  7. package/dist/cjs/pm-plugins/debug-mode.js +105 -7
  8. package/dist/cjs/pm-plugins/ghost-text-decoration.js +12 -3
  9. package/dist/cjs/pm-plugins/local-slow-lane-client.js +1202 -453
  10. package/dist/cjs/pm-plugins/scoring-pipeline.js +164 -19
  11. package/dist/cjs/pm-plugins/slow-lane-client.js +100 -37
  12. package/dist/cjs/pm-plugins/text-predictor.js +2099 -309
  13. package/dist/es2019/analytics/ufo.js +2 -0
  14. package/dist/es2019/pm-plugins/artifact-loader.js +70 -0
  15. package/dist/es2019/pm-plugins/artifacts-manifest.js +3 -0
  16. package/dist/es2019/pm-plugins/autocomplete-plugin.js +489 -152
  17. package/dist/es2019/pm-plugins/canonical-lm-scoring.js +270 -0
  18. package/dist/es2019/pm-plugins/debug-mode.js +102 -7
  19. package/dist/es2019/pm-plugins/ghost-text-decoration.js +12 -3
  20. package/dist/es2019/pm-plugins/local-slow-lane-client.js +940 -326
  21. package/dist/es2019/pm-plugins/scoring-pipeline.js +137 -13
  22. package/dist/es2019/pm-plugins/slow-lane-client.js +94 -37
  23. package/dist/es2019/pm-plugins/text-predictor.js +1692 -200
  24. package/dist/esm/analytics/ufo.js +2 -1
  25. package/dist/esm/pm-plugins/artifact-loader.js +128 -0
  26. package/dist/esm/pm-plugins/artifacts-manifest.js +3 -0
  27. package/dist/esm/pm-plugins/autocomplete-plugin.js +501 -163
  28. package/dist/esm/pm-plugins/canonical-lm-scoring.js +364 -0
  29. package/dist/esm/pm-plugins/debug-mode.js +104 -6
  30. package/dist/esm/pm-plugins/ghost-text-decoration.js +12 -3
  31. package/dist/esm/pm-plugins/local-slow-lane-client.js +1200 -451
  32. package/dist/esm/pm-plugins/scoring-pipeline.js +163 -18
  33. package/dist/esm/pm-plugins/slow-lane-client.js +100 -37
  34. package/dist/esm/pm-plugins/text-predictor.js +2101 -313
  35. package/dist/types/analytics/ufo.d.ts +1 -0
  36. package/dist/types/pm-plugins/artifact-loader.d.ts +44 -0
  37. package/dist/types/pm-plugins/artifacts-manifest.d.ts +3 -0
  38. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +39 -0
  39. package/dist/types/pm-plugins/canonical-lm-scoring.d.ts +158 -0
  40. package/dist/types/pm-plugins/debug-mode.d.ts +55 -1
  41. package/dist/types/pm-plugins/ghost-text-decoration.d.ts +5 -2
  42. package/dist/types/pm-plugins/local-slow-lane-client.d.ts +87 -54
  43. package/dist/types/pm-plugins/scoring-pipeline.d.ts +33 -1
  44. package/dist/types/pm-plugins/slow-lane-client.d.ts +51 -3
  45. package/dist/types/pm-plugins/text-predictor.d.ts +156 -3
  46. package/package.json +2 -2
  47. package/src/analytics/ufo.ts +5 -0
  48. package/src/pm-plugins/artifact-loader.ts +92 -0
  49. package/src/pm-plugins/artifacts-manifest.ts +3 -0
  50. package/src/pm-plugins/autocomplete-plugin.ts +584 -154
  51. package/src/pm-plugins/canonical-lm-scoring.ts +412 -0
  52. package/src/pm-plugins/debug-mode.ts +134 -7
  53. package/src/pm-plugins/ghost-text-decoration.ts +13 -4
  54. package/src/pm-plugins/local-slow-lane-client.ts +1302 -386
  55. package/src/pm-plugins/scoring-pipeline.ts +167 -17
  56. package/src/pm-plugins/slow-lane-client.ts +147 -62
  57. package/src/pm-plugins/text-predictor.ts +2119 -254
  58. package/CANONICAL_FIX__DO_NOT_USE_ME_A/package.json +0 -8
  59. package/CANONICAL_FIX__DO_NOT_USE_ME_B/package.json +0 -8
  60. package/CANONICAL_FIX__DO_NOT_USE_ME_C/package.json +0 -8
  61. package/dist/cjs/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -18
  62. package/dist/cjs/entry-points/src-pm-plugins-slow-lane-client.js +0 -36
  63. package/dist/cjs/entry-points/src-pm-plugins-text-predictor.js +0 -66
  64. package/dist/es2019/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -2
  65. package/dist/es2019/entry-points/src-pm-plugins-slow-lane-client.js +0 -2
  66. package/dist/es2019/entry-points/src-pm-plugins-text-predictor.js +0 -2
  67. package/dist/esm/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -2
  68. package/dist/esm/entry-points/src-pm-plugins-slow-lane-client.js +0 -2
  69. package/dist/esm/entry-points/src-pm-plugins-text-predictor.js +0 -2
  70. package/dist/types/entry-points/src-pm-plugins-autocomplete-plugin.d.ts +0 -2
  71. package/dist/types/entry-points/src-pm-plugins-slow-lane-client.d.ts +0 -2
  72. package/dist/types/entry-points/src-pm-plugins-text-predictor.d.ts +0 -2
  73. package/scripts/gen_first_token_to_words.py +0 -170
  74. package/src/entry-points/src-pm-plugins-autocomplete-plugin.ts +0 -7
  75. package/src/entry-points/src-pm-plugins-slow-lane-client.ts +0 -13
  76. package/src/entry-points/src-pm-plugins-text-predictor.ts +0 -14
  77. package/src/pm-plugins/autocomplete-plugin/package.json +0 -8
  78. package/src/pm-plugins/slow-lane-client/package.json +0 -8
  79. package/src/pm-plugins/text-predictor/package.json +0 -8
@@ -4,14 +4,15 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
4
4
  import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
5
5
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
6
6
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
7
- import { isAutocompleteDebugEnabled } from './debug-mode';
7
+ import { CTC_STYLES, ctcTag, isAutocompleteDebugEnabled, isAutocompleteDebugVerbose } from './debug-mode';
8
8
  import { createGhostTextDecorationSet } from './ghost-text-decoration';
9
9
  import { createLocalSlowLaneClient } from './local-slow-lane-client';
10
10
  import { loadGrammarDataAsync } from './scoring-pipeline';
11
- import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
12
- import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage } from './text-predictor';
11
+ import { clearDefaultSlowLaneClient, createSlowLaneClient, getDefaultSlowLaneClientStatus, isWordBoundary, setDefaultSlowLaneClient } from './slow-lane-client';
12
+ import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage, getLastPredictionDebug, noteSuggestionAccepted } from './text-predictor';
13
13
  export const autocompletePluginKey = new PluginKey('autocomplete');
14
- const DEBOUNCE_MS = 150;
14
+ const PREDICTION_COALESCE_MS = 0;
15
+ const GHOST_DECISION_BUDGET_MS = 100;
15
16
  const NETWORK_SLOW_LANE_DEBOUNCE_MS = 300;
16
17
  const LOCAL_SLOW_LANE_DEBOUNCE_MS = 100;
17
18
  const CONTEXT_REFRESH_THROTTLE_MS = 1000;
@@ -26,18 +27,82 @@ const MAX_INGESTED_CONTEXT_TEXTS = 50;
26
27
  // on every word boundary for the plugin's lifetime.
27
28
  const MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
28
29
  const INLINE_LEAF_TEXT = ' ';
30
+ let slowLaneClientInstanceSequence = 0;
29
31
 
30
- // eslint-disable-next-line require-unicode-regexp
31
- const TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
32
32
  // eslint-disable-next-line require-unicode-regexp
33
33
  const WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
34
+ const TRAILING_SURFACE_PREFIX_REGEX = /[\p{L}\p{N}_'-]+$/u;
34
35
  const hasDestroy = client => 'destroy' in client && typeof client.destroy === 'function';
35
36
  const createInitialState = () => ({
36
37
  ghostText: '',
37
38
  ghostPosition: -1,
38
- decorationSet: DecorationSet.empty
39
+ decorationSet: DecorationSet.empty,
40
+ suggestion: null
41
+ });
42
+ const clearGhostState = pluginState => ({
43
+ ...pluginState,
44
+ ghostText: '',
45
+ ghostPosition: -1,
46
+ decorationSet: DecorationSet.empty,
47
+ suggestion: null
39
48
  });
40
49
 
50
+ /**
51
+ * Keep a displayed ghost alive when the user types the character it already
52
+ * predicted.
53
+ *
54
+ * Clearing on every document change throws away a suggestion the keystroke just
55
+ * confirmed, and the replacement decision has to re-earn it from an empty state
56
+ * inside the budget — which it often misses, so the ghost blinks out mid-word.
57
+ * It is worse for multi-word surfaces: typing the space in `machine learning`
58
+ * empties the current word, and the display gate refuses to show anything again
59
+ * until three characters of `learning` exist.
60
+ *
61
+ * A confirming keystroke is not new information about the prediction, so the
62
+ * ghost simply advances by one character and the still-running decision is free
63
+ * to replace it with something better. Anything else — a different character,
64
+ * a paste, a deletion, an edit elsewhere — clears as before.
65
+ */
66
+ const advanceGhostThroughTypedCharacter = (tr, pluginState) => {
67
+ const {
68
+ ghostPosition,
69
+ ghostText,
70
+ suggestion
71
+ } = pluginState;
72
+ if (ghostText.length < 2 || ghostPosition < 0 || !suggestion) {
73
+ // A one-character tail leaves nothing to show after advancing.
74
+ return null;
75
+ }
76
+
77
+ // Exactly one character longer, with the cursor sitting just past the ghost
78
+ // anchor: together these identify a single insertion made at the anchor
79
+ // rather than a paste or an edit elsewhere in the document.
80
+ if (tr.doc.content.size - tr.before.content.size !== 1) {
81
+ return null;
82
+ }
83
+ if (!tr.selection.empty || tr.selection.from !== ghostPosition + 1) {
84
+ return null;
85
+ }
86
+ if (tr.doc.textBetween(ghostPosition, ghostPosition + 1) !== ghostText[0]) {
87
+ return null;
88
+ }
89
+ const remaining = ghostText.slice(1);
90
+ const nextPosition = ghostPosition + 1;
91
+ return {
92
+ ...pluginState,
93
+ ghostText: remaining,
94
+ ghostPosition: nextPosition,
95
+ decorationSet: createGhostTextDecorationSet(tr.doc, nextPosition, remaining),
96
+ // `surface` stays the full candidate so cooldown and analytics still
97
+ // describe the suggestion that was originally committed.
98
+ suggestion: {
99
+ ...suggestion,
100
+ ghostText: remaining,
101
+ position: nextPosition
102
+ }
103
+ };
104
+ };
105
+
41
106
  /**
42
107
  * Extract text content before the cursor from the current document.
43
108
  * Returns the last ~200 characters for context.
@@ -70,6 +135,10 @@ const getTextBeforeCursor = state => {
70
135
  }
71
136
  return fullText.slice(-maxChars);
72
137
  };
138
+ const getTrailingSurfacePrefixLength = text => {
139
+ var _text$trimEnd$match$, _text$trimEnd$match;
140
+ 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;
141
+ };
73
142
 
74
143
  /**
75
144
  * Set the autocomplete state via a transaction metadata.
@@ -82,23 +151,41 @@ const setAutocompleteMeta = (tr, meta) => {
82
151
  * Apply a ghost text suggestion to the editor state.
83
152
  */
84
153
  let lastShownGhostText = '';
85
- const showGhostText = (view, text, position) => {
154
+ const showGhostText = (view, prediction, position, revision, decisionStartedAt) => {
86
155
  try {
87
156
  const {
88
157
  state,
89
158
  dispatch
90
159
  } = view;
91
- const decorationSet = createGhostTextDecorationSet(state, position, text);
160
+ const suggestion = {
161
+ decisionLatencyMs: performance.now() - decisionStartedAt,
162
+ evidenceTier: prediction.evidenceTier,
163
+ ghostText: prediction.text,
164
+ meanTokenLogProbability: prediction.meanTokenLogProbability,
165
+ poolHeldExtension: prediction.poolHeldExtension,
166
+ position,
167
+ posterior: prediction.posterior,
168
+ rankScore: prediction.rankScore,
169
+ revision,
170
+ shortlistSize: prediction.shortlistSize,
171
+ surface: prediction.surface,
172
+ termType: prediction.termType,
173
+ winnerMargin: prediction.winnerMargin
174
+ };
175
+ const decorationSet = createGhostTextDecorationSet(state.doc, position, prediction.text);
92
176
  const tr = setAutocompleteMeta(state.tr, {
93
- ghostText: text,
177
+ ghostText: prediction.text,
94
178
  ghostPosition: position,
95
- decorationSet
179
+ decorationSet,
180
+ suggestion
96
181
  });
97
182
  dispatch(tr);
183
+ return suggestion;
98
184
  } catch (error) {
99
185
  logException(error, {
100
186
  location: 'editor-plugin-autocomplete/showGhostText'
101
187
  });
188
+ return null;
102
189
  }
103
190
  };
104
191
 
@@ -115,7 +202,8 @@ const clearGhostText = (state, dispatch) => {
115
202
  const tr = setAutocompleteMeta(state.tr, {
116
203
  ghostText: '',
117
204
  ghostPosition: -1,
118
- decorationSet: DecorationSet.empty
205
+ decorationSet: DecorationSet.empty,
206
+ suggestion: null
119
207
  });
120
208
  dispatch(tr);
121
209
  } catch (error) {
@@ -133,40 +221,43 @@ const clearGhostText = (state, dispatch) => {
133
221
  */
134
222
  const acceptGhostText = (state, dispatch) => {
135
223
  const pluginState = autocompletePluginKey.getState(state);
136
- if (!pluginState || !pluginState.ghostText) {
137
- return false;
224
+ if (!(pluginState !== null && pluginState !== void 0 && pluginState.suggestion) || !pluginState.ghostText) {
225
+ return null;
138
226
  }
139
227
  if (dispatch) {
140
228
  try {
141
229
  const {
142
230
  ghostText,
143
- ghostPosition
231
+ ghostPosition,
232
+ suggestion
144
233
  } = pluginState;
145
234
  let tr = state.tr.insertText(ghostText, ghostPosition);
146
235
  tr = setAutocompleteMeta(tr, {
147
236
  ghostText: '',
148
237
  ghostPosition: -1,
149
- decorationSet: DecorationSet.empty
238
+ decorationSet: DecorationSet.empty,
239
+ suggestion: null
150
240
  });
151
241
  dispatch(tr);
242
+ return suggestion;
152
243
  } catch (error) {
153
244
  logException(error, {
154
245
  location: 'editor-plugin-autocomplete/acceptGhostText'
155
246
  });
156
- return false;
247
+ return null;
157
248
  }
158
249
  }
159
- return true;
250
+ return pluginState.suggestion;
160
251
  };
161
252
  const acceptGhostTextWithAnalytics = (state, dispatch, onAccepted) => {
162
- var _pluginState$ghostTex;
163
- const pluginState = autocompletePluginKey.getState(state);
164
- const ghostText = (_pluginState$ghostTex = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex !== void 0 ? _pluginState$ghostTex : '';
165
- const accepted = acceptGhostText(state, dispatch);
166
- if (accepted) {
167
- onAccepted(ghostText);
253
+ const acceptedSuggestion = acceptGhostText(state, dispatch);
254
+ if (acceptedSuggestion) {
255
+ // Start the post-accept cooldown so the just-accepted unit isn't immediately
256
+ // re-offered (QI-2 echo, e.g. `end to end` → `end to end to end`).
257
+ noteSuggestionAccepted(acceptedSuggestion.surface);
258
+ onAccepted(acceptedSuggestion);
168
259
  }
169
- return accepted;
260
+ return acceptedSuggestion !== null;
170
261
  };
171
262
 
172
263
  /**
@@ -228,14 +319,6 @@ const getLeadingTextCharacter = text => {
228
319
  }
229
320
  return String.fromCodePoint(firstCodePoint);
230
321
  };
231
- const getTypedLengthForPrediction = textBefore => {
232
- var _textBefore$match$, _textBefore$match;
233
- if (isWordBoundary(textBefore)) {
234
- return 0;
235
- }
236
- const trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(TRAILING_NON_BOUNDARY_TOKEN_REGEX)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
237
- return trailingToken.length;
238
- };
239
322
  const isEnglishLocale = locale => {
240
323
  if (!locale) {
241
324
  return false;
@@ -243,11 +326,12 @@ const isEnglishLocale = locale => {
243
326
  return locale.toLowerCase().startsWith('en');
244
327
  };
245
328
  export const createAutocompletePlugin = (options, api) => {
246
- var _options$surface, _options$locale;
329
+ var _options$surface, _options$locale, _registeredSlowLaneSt;
247
330
  const surface = (_options$surface = options === null || options === void 0 ? void 0 : options.surface) !== null && _options$surface !== void 0 ? _options$surface : 'editor';
248
331
  const 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;
249
332
  const isAutocompleteEnabled = isEnglishLocale(locale);
250
333
  let debounceTimer = null;
334
+ let decisionDeadlineTimer = null;
251
335
  let hasIngestedPage = false;
252
336
  let resolvedContext;
253
337
  /**
@@ -257,6 +341,10 @@ export const createAutocompletePlugin = (options, api) => {
257
341
  */
258
342
  let currentView = null;
259
343
  let unsubscribeFromContextUpdates;
344
+ // "Prime, don't ask-and-read": semantic, Tier-A, and Tier-B results land
345
+ // asynchronously, often with no following keystroke. Re-run prediction when
346
+ // they arrive so the caches are consumed immediately.
347
+ let reschedulePredictionOnSlowLaneResult = null;
260
348
  /**
261
349
  * Set after accepting a suggestion so the next doc-change update
262
350
  * skips scheduling a new prediction for the just-inserted text.
@@ -270,8 +358,8 @@ export const createAutocompletePlugin = (options, api) => {
270
358
  * re-showing the same suggestion. Resets to null as soon as the text changes.
271
359
  */
272
360
  let dismissedContext = null;
273
- let lastSuggestionTypedLength = 0;
274
- let lastSuggestionLength = 0;
361
+ let decisionRevision = 0;
362
+ let activeDecision;
275
363
 
276
364
  /**
277
365
  * cold → no slow-lane vector received yet; frequency-only trie scoring
@@ -338,10 +426,10 @@ export const createAutocompletePlugin = (options, api) => {
338
426
  }
339
427
  });
340
428
  };
341
- const fireSuggestionInsertedAnalytics = ghostText => {
429
+ const fireSuggestionInsertedAnalytics = suggestion => {
342
430
  var _api$analytics4;
343
- const typedLength = lastSuggestionTypedLength;
344
- const suggestionLength = lastSuggestionLength || typedLength + ghostText.length;
431
+ const typedLength = Math.max(0, suggestion.surface.length - suggestion.ghostText.length);
432
+ const suggestionLength = suggestion.surface.length;
345
433
  const kssDelta = suggestionLength - typedLength;
346
434
  api === null || api === void 0 ? void 0 : (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : _api$analytics4.actions.fireAnalyticsEvent({
347
435
  action: ACTION.SUGGESTION_INSERTED,
@@ -360,13 +448,61 @@ export const createAutocompletePlugin = (options, api) => {
360
448
  debounceMs: LOCAL_SLOW_LANE_DEBOUNCE_MS,
361
449
  onLoadSuccess: fireLocalModelLoadedAnalytics,
362
450
  onLoadError: fireLocalModelLoadFailedAnalytics,
451
+ // Re-run prediction for each independently scheduled model result.
452
+ onUpdate: () => {
453
+ var _reschedulePrediction;
454
+ return (_reschedulePrediction = reschedulePredictionOnSlowLaneResult) === null || _reschedulePrediction === void 0 ? void 0 : _reschedulePrediction({
455
+ kind: 'semantic'
456
+ });
457
+ },
458
+ onBoundaryLmUpdate: ({
459
+ contextKey,
460
+ familyKey,
461
+ latencyMs
462
+ }) => {
463
+ var _reschedulePrediction2;
464
+ return (_reschedulePrediction2 = reschedulePredictionOnSlowLaneResult) === null || _reschedulePrediction2 === void 0 ? void 0 : _reschedulePrediction2({
465
+ contextKey,
466
+ familyKey,
467
+ kind: 'boundary',
468
+ latencyMs
469
+ });
470
+ },
471
+ onSurfaceScoreUpdate: ({
472
+ contextKey,
473
+ familyKey,
474
+ latencyMs
475
+ }) => {
476
+ var _reschedulePrediction3;
477
+ return (_reschedulePrediction3 = reschedulePredictionOnSlowLaneResult) === null || _reschedulePrediction3 === void 0 ? void 0 : _reschedulePrediction3({
478
+ contextKey,
479
+ familyKey,
480
+ kind: 'surface',
481
+ latencyMs
482
+ });
483
+ },
363
484
  surface
364
485
  }) : createSlowLaneClient({
365
486
  baseUrl: '',
366
487
  debounceMs: NETWORK_SLOW_LANE_DEBOUNCE_MS,
488
+ // Re-run prediction when the BE semantic vector / LM logits land so
489
+ // warm-mode unigram ranking applies without waiting for a keystroke.
490
+ onUpdate: () => {
491
+ var _reschedulePrediction4;
492
+ return (_reschedulePrediction4 = reschedulePredictionOnSlowLaneResult) === null || _reschedulePrediction4 === void 0 ? void 0 : _reschedulePrediction4({
493
+ kind: 'semantic'
494
+ });
495
+ },
367
496
  surface
368
497
  });
369
- setDefaultSlowLaneClient(slowLaneClient);
498
+ const slowLaneClientKind = options !== null && options !== void 0 && options.useLocalModel ? 'localLlm' : 'server';
499
+ const slowLaneClientId = `${surface}:${slowLaneClientKind}:${++slowLaneClientInstanceSequence}`;
500
+ setDefaultSlowLaneClient(slowLaneClient, {
501
+ clientId: slowLaneClientId,
502
+ kind: slowLaneClientKind
503
+ });
504
+ const registeredSlowLaneStatus = getDefaultSlowLaneClientStatus();
505
+ ctcTag('init', `slow lane registered · id=${slowLaneClientId} · selected=${slowLaneClientKind} · canonical=${registeredSlowLaneStatus.canonicalScoringSupported ? 'yes' : 'no'} · ready=${(_registeredSlowLaneSt = registeredSlowLaneStatus.localModelReady) !== null && _registeredSlowLaneSt !== void 0 ? _registeredSlowLaneSt : 'n/a'}`, slowLaneClientKind === 'localLlm' ? CTC_STYLES.good : CTC_STYLES.warn);
370
506
  let contextRequestInFlight = false;
371
507
  let lastContextRefreshAt = 0;
372
508
  // Bounds the word-boundary retry loop so it terminates even when the editor is
@@ -375,7 +511,63 @@ export const createAutocompletePlugin = (options, api) => {
375
511
  // Set when the plugin is torn down so in-flight getContext() resolutions don't
376
512
  // mutate the global text-predictor state after destruction.
377
513
  let destroyed = false;
378
- const ingestedContextTexts = new Set();
514
+ // L1 ingestion cannot run before the vocabulary has loaded: `incrementSessionFreq`
515
+ // only touches trie nodes that already exist, so an empty trie silently rejects
516
+ // every word. Text that arrives first — which a page usually does, since the
517
+ // chat resolves it on focus in the same tick as the load starts — is held here
518
+ // and applied once the load has settled.
519
+ const pendingSessionContextTexts = new Set();
520
+ const sessionIngestedContextTexts = new Set();
521
+ let isVocabularyReady = false;
522
+ let vocabularyLoadPromise;
523
+ const addBoundedContextText = (texts, text) => {
524
+ texts.add(text);
525
+ // Evict oldest entries (Set preserves insertion order) to bound memory.
526
+ while (texts.size > MAX_INGESTED_CONTEXT_TEXTS) {
527
+ const oldest = texts.values().next().value;
528
+ if (oldest === undefined) {
529
+ break;
530
+ }
531
+ texts.delete(oldest);
532
+ }
533
+ };
534
+ const flushPendingSessionContext = () => {
535
+ if (!isVocabularyReady || destroyed) {
536
+ return;
537
+ }
538
+ for (const text of pendingSessionContextTexts) {
539
+ if (!sessionIngestedContextTexts.has(text)) {
540
+ ingestDocumentPage(text);
541
+ addBoundedContextText(sessionIngestedContextTexts, text);
542
+ }
543
+ pendingSessionContextTexts.delete(text);
544
+ }
545
+ };
546
+ const ensureVocabularyReady = () => {
547
+ var _options$useLocalMode;
548
+ if (isVocabularyReady) {
549
+ flushPendingSessionContext();
550
+ return Promise.resolve();
551
+ }
552
+ vocabularyLoadPromise ??= loadDefaultVocabulary({
553
+ isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false,
554
+ surface
555
+ }).then(() => {
556
+ if (destroyed) {
557
+ return;
558
+ }
559
+ isVocabularyReady = true;
560
+ flushPendingSessionContext();
561
+ }).catch(error => {
562
+ // Do not consume the pending text on failure. A later focus retries the
563
+ // load and can still apply the original context exactly once.
564
+ vocabularyLoadPromise = undefined;
565
+ logException(error, {
566
+ location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
567
+ });
568
+ });
569
+ return vocabularyLoadPromise;
570
+ };
379
571
  const logContextResolved = (source, context) => {
380
572
  var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
381
573
  if (!isAutocompleteDebugEnabled()) {
@@ -383,7 +575,7 @@ export const createAutocompletePlugin = (options, api) => {
383
575
  }
384
576
 
385
577
  // eslint-disable-next-line no-console
386
- console.log('%c[Autocomplete] %cgetContext resolved', 'color: #00b8d9; font-weight: bold;', 'color: inherit;', {
578
+ console.log('%c[CTC:signal]%c getContext resolved', CTC_STYLES.brand, CTC_STYLES.body, {
387
579
  source,
388
580
  hasParentComment: !!(context !== null && context !== void 0 && context.parentCommentContent),
389
581
  parentCommentPreview: context === null || context === void 0 ? void 0 : (_context$parentCommen = context.parentCommentContent) === null || _context$parentCommen === void 0 ? void 0 : _context$parentCommen.slice(0, 80),
@@ -403,19 +595,10 @@ export const createAutocompletePlugin = (options, api) => {
403
595
  ...definedContext
404
596
  };
405
597
  const ingestContextText = text => {
406
- if (!text || ingestedContextTexts.has(text)) {
598
+ if (!text || sessionIngestedContextTexts.has(text)) {
407
599
  return;
408
600
  }
409
- ingestedContextTexts.add(text);
410
- // Evict oldest entries (Set preserves insertion order) to bound memory.
411
- while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
412
- const oldest = ingestedContextTexts.values().next().value;
413
- if (oldest === undefined) {
414
- break;
415
- }
416
- ingestedContextTexts.delete(oldest);
417
- }
418
- ingestDocumentPage(text);
601
+ addBoundedContextText(pendingSessionContextTexts, text);
419
602
  };
420
603
  ingestContextText(context.fullPageContent);
421
604
  ingestContextText(context.parentCommentContent);
@@ -423,6 +606,7 @@ export const createAutocompletePlugin = (options, api) => {
423
606
  var _context$siblingComme6;
424
607
  ingestContextText(siblingCommentContent);
425
608
  }
609
+ flushPendingSessionContext();
426
610
 
427
611
  // Context arrived after word boundaries may already have fired. Re-send
428
612
  // slow-lane context immediately so the next inference includes the thread.
@@ -470,77 +654,239 @@ export const createAutocompletePlugin = (options, api) => {
470
654
  });
471
655
  return true;
472
656
  };
473
-
474
- /**
475
- * Schedule a prediction after a short debounce.
476
- * Tier 1 predictions are synchronous (<0.1ms) but we still debounce
477
- * to avoid unnecessary work on rapid keystrokes.
478
- */
479
- const schedulePrediction = view => {
657
+ const clearDecisionTimers = () => {
480
658
  if (debounceTimer) {
481
659
  clearTimeout(debounceTimer);
660
+ debounceTimer = null;
482
661
  }
483
- debounceTimer = setTimeout(() => {
484
- try {
485
- const {
486
- state
487
- } = view;
488
- const {
489
- selection
490
- } = state;
491
- if (!selection.empty) {
492
- return;
493
- }
662
+ if (decisionDeadlineTimer) {
663
+ clearTimeout(decisionDeadlineTimer);
664
+ decisionDeadlineTimer = null;
665
+ }
666
+ };
667
+ const cancelActiveDecision = () => {
668
+ clearDecisionTimers();
669
+ activeDecision = undefined;
670
+ };
671
+ const isDecisionSnapshotCurrent = (view, decision) => {
672
+ const {
673
+ state
674
+ } = view;
675
+ return state.selection.empty && state.selection.from === decision.position && getTextBeforeCursor(state) === decision.textBefore;
676
+ };
677
+ const isDecisionCurrent = (view, decision) => {
678
+ var _activeDecision;
679
+ return ((_activeDecision = activeDecision) === null || _activeDecision === void 0 ? void 0 : _activeDecision.revision) === decision.revision && isDecisionSnapshotCurrent(view, decision);
680
+ };
681
+ const expireActiveDecision = decision => {
682
+ var _activeDecision2;
683
+ if (((_activeDecision2 = activeDecision) === null || _activeDecision2 === void 0 ? void 0 : _activeDecision2.revision) !== decision.revision) {
684
+ return;
685
+ }
686
+ cancelActiveDecision();
687
+ };
688
+ const evaluateActiveDecision = (view, revision) => {
689
+ const decision = activeDecision;
690
+ if (!decision || decision.revision !== revision || !isDecisionCurrent(view, decision)) {
691
+ return;
692
+ }
693
+ if (performance.now() > decision.deadlineAt) {
694
+ expireActiveDecision(decision);
695
+ return;
696
+ }
697
+ decision.evaluations++;
698
+ try {
699
+ const {
700
+ state
701
+ } = view;
702
+ const {
703
+ selection
704
+ } = state;
494
705
 
495
- // Suppress suggestions when the cursor is mid-word — only offer
496
- // completions when the cursor is at the trailing edge of a token.
497
- // nodeAfter correctly handles inline atoms (mentions, emojis) where
498
- // parentOffset and textContent indices diverge.
499
- const nodeAfter = selection.$from.nodeAfter;
500
- const charAfterCursor = nodeAfter !== null && nodeAfter !== void 0 && nodeAfter.isText ? getLeadingTextCharacter(nodeAfter.text) : undefined;
501
- // Only suppress for Unicode letters, digits, and underscore — hyphens,
502
- // apostrophes, and similar punctuation are valid left-edge boundaries
503
- // and should not block suggestions (e.g. cursor before '-' in compound-word).
504
- if (charAfterCursor && isMidWordCharacter(charAfterCursor)) {
505
- return;
506
- }
507
- const textBefore = getTextBeforeCursor(state);
706
+ // Suppress suggestions when the cursor is mid-word — only offer
707
+ // completions when the cursor is at the trailing edge of a token.
708
+ const nodeAfter = selection.$from.nodeAfter;
709
+ const charAfterCursor = nodeAfter !== null && nodeAfter !== void 0 && nodeAfter.isText ? getLeadingTextCharacter(nodeAfter.text) : undefined;
710
+ if (charAfterCursor && isMidWordCharacter(charAfterCursor)) {
711
+ cancelActiveDecision();
712
+ return;
713
+ }
714
+ if (decision.textBefore === dismissedContext) {
715
+ cancelActiveDecision();
716
+ return;
717
+ }
718
+ dismissedContext = null;
508
719
 
509
- // Suppress re-showing the same suggestion the user just dismissed.
510
- // Once the text context changes (user types or deletes), this clears automatically.
511
- if (textBefore === dismissedContext) {
512
- return;
513
- }
514
- dismissedContext = null;
515
- if (textBefore.trim().length < 3) {
516
- return;
720
+ // Two characters are enough to prefetch the canonical shortlist. The
721
+ // predictor itself withholds display until the three-character gate.
722
+ if (decision.textBefore.trim().length < 2) {
723
+ cancelActiveDecision();
724
+ return;
725
+ }
726
+ const prediction = predict(decision.textBefore);
727
+ if (!prediction || prediction.text.length === 0) {
728
+ // Remain in `collecting` until an async signal arrives or the hard
729
+ // deadline expires. We never display a provisional fallback.
730
+ return;
731
+ }
732
+ const readyAtMs = performance.now() - decision.startedAt;
733
+ if (readyAtMs > GHOST_DECISION_BUDGET_MS) {
734
+ cancelActiveDecision();
735
+ if (isAutocompleteDebugEnabled()) {
736
+ // eslint-disable-next-line no-console
737
+ console.log('%c[CTC:budget]%c fully gated after render deadline (render suppressed)', CTC_STYLES.section, CTC_STYLES.body, {
738
+ revision: decision.revision,
739
+ readyAtMs,
740
+ surface: prediction.surface
741
+ });
517
742
  }
518
- const prediction = predict(textBefore);
519
- if (prediction && prediction.length > 0) {
520
- const typedLength = getTypedLengthForPrediction(textBefore);
521
- lastSuggestionTypedLength = typedLength;
522
- lastSuggestionLength = typedLength + prediction.length;
523
- showGhostText(view, prediction, selection.from);
524
- if (prediction !== lastShownGhostText) {
525
- var _api$analytics5;
526
- lastShownGhostText = prediction;
527
- api === null || api === void 0 ? void 0 : (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions.fireAnalyticsEvent({
528
- action: ACTION.SUGGESTION_VIEWED,
529
- actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
530
- eventType: EVENT_TYPE.TRACK,
531
- attributes: {
532
- completionSource: getCompletionSource(),
533
- surface
534
- }
535
- });
743
+ return;
744
+ }
745
+ const suggestion = showGhostText(view, prediction, decision.position, decision.revision, decision.startedAt);
746
+ if (!suggestion) {
747
+ cancelActiveDecision();
748
+ return;
749
+ }
750
+ cancelActiveDecision();
751
+ if (isAutocompleteDebugEnabled()) {
752
+ // eslint-disable-next-line no-console
753
+ console.log('%c[CTC:decision]%c committed immutable ghost', CTC_STYLES.brand, CTC_STYLES.body, {
754
+ revision: suggestion.revision,
755
+ surface: suggestion.surface,
756
+ ghostText: suggestion.ghostText,
757
+ evidenceTier: suggestion.evidenceTier,
758
+ rankScore: suggestion.rankScore,
759
+ winnerMargin: suggestion.winnerMargin,
760
+ decisionLatencyMs: suggestion.decisionLatencyMs,
761
+ budgetMs: GHOST_DECISION_BUDGET_MS,
762
+ typedPrefixLength: getTrailingSurfacePrefixLength(decision.textBefore),
763
+ evaluations: decision.evaluations,
764
+ asyncSignals: decision.asyncSignals,
765
+ readyWithinBudget: suggestion.decisionLatencyMs <= GHOST_DECISION_BUDGET_MS
766
+ });
767
+ }
768
+ if (suggestion.ghostText !== lastShownGhostText) {
769
+ var _api$analytics5;
770
+ lastShownGhostText = suggestion.ghostText;
771
+ api === null || api === void 0 ? void 0 : (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions.fireAnalyticsEvent({
772
+ action: ACTION.SUGGESTION_VIEWED,
773
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
774
+ eventType: EVENT_TYPE.TRACK,
775
+ attributes: {
776
+ completionSource: getCompletionSource(),
777
+ surface
536
778
  }
537
- }
538
- } catch (error) {
539
- logException(error, {
540
- location: 'editor-plugin-autocomplete/schedulePrediction'
541
779
  });
542
780
  }
543
- }, DEBOUNCE_MS);
781
+ } catch (error) {
782
+ cancelActiveDecision();
783
+ logException(error, {
784
+ location: 'editor-plugin-autocomplete/evaluateActiveDecision'
785
+ });
786
+ }
787
+ };
788
+ const beginPredictionDecision = view => {
789
+ cancelActiveDecision();
790
+ const {
791
+ state
792
+ } = view;
793
+ if (!state.selection.empty) {
794
+ return;
795
+ }
796
+ const startedAt = performance.now();
797
+ const textBefore = getTextBeforeCursor(state);
798
+ const revision = ++decisionRevision;
799
+ activeDecision = {
800
+ asyncSignals: 0,
801
+ deadlineAt: startedAt + GHOST_DECISION_BUDGET_MS,
802
+ evaluations: 0,
803
+ position: state.selection.from,
804
+ revision,
805
+ startedAt,
806
+ textBefore
807
+ };
808
+ if (isAutocompleteDebugEnabled()) {
809
+ // eslint-disable-next-line no-console
810
+ console.log('%c[CTC:decision]%c collecting', CTC_STYLES.brand, CTC_STYLES.body, {
811
+ revision,
812
+ budgetMs: GHOST_DECISION_BUDGET_MS,
813
+ coalesceMs: PREDICTION_COALESCE_MS,
814
+ typedPrefixLength: getTrailingSurfacePrefixLength(activeDecision.textBefore),
815
+ textBefore: activeDecision.textBefore.slice(-80)
816
+ });
817
+ }
818
+ debounceTimer = setTimeout(() => {
819
+ debounceTimer = null;
820
+ evaluateActiveDecision(view, revision);
821
+ }, PREDICTION_COALESCE_MS);
822
+ decisionDeadlineTimer = setTimeout(() => {
823
+ var _activeDecision3;
824
+ decisionDeadlineTimer = null;
825
+ if (((_activeDecision3 = activeDecision) === null || _activeDecision3 === void 0 ? void 0 : _activeDecision3.revision) === revision) {
826
+ const expiredDecision = activeDecision;
827
+ if (isAutocompleteDebugEnabled()) {
828
+ const elapsedMs = performance.now() - expiredDecision.startedAt;
829
+ const predictionDebug = getLastPredictionDebug();
830
+ const matchingPredictionDebug = (predictionDebug === null || predictionDebug === void 0 ? void 0 : predictionDebug.textBefore) === expiredDecision.textBefore ? predictionDebug : null;
831
+ const abstentionPrefix = '— abstain: ';
832
+ const predictorDecision = matchingPredictionDebug === null || matchingPredictionDebug === void 0 ? void 0 : matchingPredictionDebug.decision;
833
+ const predictorReason = predictorDecision !== null && predictorDecision !== void 0 && predictorDecision.startsWith(abstentionPrefix) ? predictorDecision.slice(abstentionPrefix.length) : predictorDecision;
834
+ const decisionMessage = predictorReason && !(matchingPredictionDebug !== null && matchingPredictionDebug !== void 0 && matchingPredictionDebug.awaitingAsyncEvidence) ? `abstain: ${predictorReason}` : `abstain: ${GHOST_DECISION_BUDGET_MS}ms decision budget expired${predictorReason ? ` · ${predictorReason}` : ''}`;
835
+ // eslint-disable-next-line no-console
836
+ console.log(`%c[CTC:decision]%c ${decisionMessage}`, CTC_STYLES.brand, CTC_STYLES.body, {
837
+ revision,
838
+ budgetMs: GHOST_DECISION_BUDGET_MS,
839
+ elapsedMs,
840
+ typedPrefixLength: getTrailingSurfacePrefixLength(expiredDecision.textBefore),
841
+ evaluations: expiredDecision.evaluations,
842
+ asyncSignals: expiredDecision.asyncSignals,
843
+ textBefore: expiredDecision.textBefore.slice(-80),
844
+ predictorDecision: predictorReason !== null && predictorReason !== void 0 ? predictorReason : null
845
+ });
846
+ }
847
+ expireActiveDecision(expiredDecision);
848
+ }
849
+ }, GHOST_DECISION_BUDGET_MS);
850
+ };
851
+
852
+ // Async semantic/canonical results may complete the active decision, but they
853
+ // never create a new decision or replace an already committed ghost.
854
+ reschedulePredictionOnSlowLaneResult = signal => {
855
+ if (destroyed || !currentView) {
856
+ return;
857
+ }
858
+ if (!activeDecision) {
859
+ return;
860
+ }
861
+ const revision = activeDecision.revision;
862
+ const elapsedMs = performance.now() - activeDecision.startedAt;
863
+ const remainingMs = activeDecision.deadlineAt - performance.now();
864
+ if (remainingMs < 0) {
865
+ const expiredDecision = activeDecision;
866
+ expireActiveDecision(expiredDecision);
867
+ return;
868
+ }
869
+ activeDecision.asyncSignals++;
870
+ if (isAutocompleteDebugVerbose()) {
871
+ // eslint-disable-next-line no-console
872
+ console.log('%c[CTC:readiness]%c async evidence signal', CTC_STYLES.section, CTC_STYLES.body, {
873
+ ...signal,
874
+ revision,
875
+ elapsedMs,
876
+ remainingMs: Math.max(0, remainingMs),
877
+ typedPrefixLength: getTrailingSurfacePrefixLength(activeDecision.textBefore),
878
+ withinBudget: remainingMs >= 0
879
+ });
880
+ }
881
+ if (debounceTimer) {
882
+ clearTimeout(debounceTimer);
883
+ }
884
+ debounceTimer = setTimeout(() => {
885
+ debounceTimer = null;
886
+ if (currentView) {
887
+ evaluateActiveDecision(currentView, revision);
888
+ }
889
+ }, 0);
544
890
  };
545
891
  const maybeUpdateSessionFrequency = (view, prevState) => {
546
892
  const newText = getTextBeforeCursor(view.state);
@@ -579,22 +925,14 @@ export const createAutocompletePlugin = (options, api) => {
579
925
  };
580
926
  }
581
927
 
582
- // A new prediction is scheduled from view.update.
928
+ // A new prediction is scheduled from view.update either way; the
929
+ // surviving ghost only covers the gap until it commits.
583
930
  if (tr.docChanged) {
584
- return {
585
- ...pluginState,
586
- ghostText: '',
587
- ghostPosition: -1,
588
- decorationSet: DecorationSet.empty
589
- };
931
+ var _advanceGhostThroughT;
932
+ return (_advanceGhostThroughT = advanceGhostThroughTypedCharacter(tr, pluginState)) !== null && _advanceGhostThroughT !== void 0 ? _advanceGhostThroughT : clearGhostState(pluginState);
590
933
  }
591
934
  if (tr.selectionSet && pluginState.ghostText) {
592
- return {
593
- ...pluginState,
594
- ghostText: '',
595
- ghostPosition: -1,
596
- decorationSet: DecorationSet.empty
597
- };
935
+ return clearGhostState(pluginState);
598
936
  }
599
937
  return pluginState;
600
938
  }
@@ -607,22 +945,25 @@ export const createAutocompletePlugin = (options, api) => {
607
945
  },
608
946
  handleKeyDown: keydownHandler({
609
947
  Tab: (state, dispatch) => {
610
- return acceptGhostTextWithAnalytics(state, dispatch, ghostText => {
948
+ return acceptGhostTextWithAnalytics(state, dispatch, suggestion => {
611
949
  justAccepted = true;
612
950
  lastShownGhostText = '';
613
- fireSuggestionInsertedAnalytics(ghostText);
951
+ cancelActiveDecision();
952
+ fireSuggestionInsertedAnalytics(suggestion);
614
953
  });
615
954
  },
616
955
  ArrowRight: (state, dispatch) => {
617
- return acceptGhostTextWithAnalytics(state, dispatch, ghostText => {
956
+ return acceptGhostTextWithAnalytics(state, dispatch, suggestion => {
618
957
  justAccepted = true;
619
958
  lastShownGhostText = '';
620
- fireSuggestionInsertedAnalytics(ghostText);
959
+ cancelActiveDecision();
960
+ fireSuggestionInsertedAnalytics(suggestion);
621
961
  });
622
962
  },
623
963
  Escape: (state, dispatch) => {
624
964
  const didClear = clearGhostText(state, dispatch);
625
965
  if (didClear) {
966
+ cancelActiveDecision();
626
967
  dismissedContext = getTextBeforeCursor(state);
627
968
  fireSuggestionDismissedAnalytics('escape');
628
969
  }
@@ -637,6 +978,7 @@ export const createAutocompletePlugin = (options, api) => {
637
978
  const pluginState = autocompletePluginKey.getState(view.state);
638
979
  if (pluginState !== null && pluginState !== void 0 && pluginState.ghostText) {
639
980
  clearGhostText(view.state, view.dispatch);
981
+ cancelActiveDecision();
640
982
  fireSuggestionDismissedAnalytics('blur');
641
983
  }
642
984
  return false;
@@ -648,6 +990,7 @@ export const createAutocompletePlugin = (options, api) => {
648
990
  if (!event.target.closest('[data-autocomplete-ghost="true"]')) {
649
991
  const didClear = clearGhostText(view.state, view.dispatch);
650
992
  if (didClear) {
993
+ cancelActiveDecision();
651
994
  dismissedContext = getTextBeforeCursor(view.state);
652
995
  lastShownGhostText = '';
653
996
  fireSuggestionDismissedAnalytics('click');
@@ -655,10 +998,11 @@ export const createAutocompletePlugin = (options, api) => {
655
998
  return false;
656
999
  }
657
1000
  event.preventDefault();
658
- const accepted = acceptGhostTextWithAnalytics(view.state, view.dispatch, ghostText => {
1001
+ const accepted = acceptGhostTextWithAnalytics(view.state, view.dispatch, suggestion => {
659
1002
  justAccepted = true;
660
1003
  lastShownGhostText = '';
661
- fireSuggestionInsertedAnalytics(ghostText);
1004
+ cancelActiveDecision();
1005
+ fireSuggestionInsertedAnalytics(suggestion);
662
1006
  });
663
1007
  if (accepted) {
664
1008
  view.focus();
@@ -666,18 +1010,11 @@ export const createAutocompletePlugin = (options, api) => {
666
1010
  return accepted;
667
1011
  },
668
1012
  focus: () => {
669
- var _options$useLocalMode, _options$useLocalMode2, _options$useLocalMode3;
1013
+ var _options$useLocalMode2, _options$useLocalMode3;
670
1014
  if (!isAutocompleteEnabled) {
671
1015
  return false;
672
1016
  }
673
- loadDefaultVocabulary({
674
- isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false,
675
- surface
676
- }).catch(error => {
677
- logException(error, {
678
- location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
679
- });
680
- });
1017
+ void ensureVocabularyReady();
681
1018
  loadVectorsAsync({
682
1019
  isLocalLLM: (_options$useLocalMode2 = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode2 !== void 0 ? _options$useLocalMode2 : false,
683
1020
  surface
@@ -733,9 +1070,7 @@ export const createAutocompletePlugin = (options, api) => {
733
1070
  // Snapshot the post-acceptance text so follow-up transactions hit
734
1071
  // the dismissedContext guard and abort until the user types again.
735
1072
  dismissedContext = getTextBeforeCursor(view.state);
736
- if (debounceTimer) {
737
- clearTimeout(debounceTimer);
738
- }
1073
+ cancelActiveDecision();
739
1074
  return;
740
1075
  }
741
1076
  maybeUpdateSessionFrequency(view, prevState);
@@ -760,7 +1095,9 @@ export const createAutocompletePlugin = (options, api) => {
760
1095
  }
761
1096
  }
762
1097
  }
763
- schedulePrediction(view);
1098
+ beginPredictionDecision(view);
1099
+ } else if (!prevState.selection.eq(view.state.selection)) {
1100
+ cancelActiveDecision();
764
1101
  }
765
1102
  },
766
1103
  destroy: () => {
@@ -769,14 +1106,14 @@ export const createAutocompletePlugin = (options, api) => {
769
1106
  currentView = null;
770
1107
  (_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 ? void 0 : _unsubscribeFromConte();
771
1108
  unsubscribeFromContextUpdates = undefined;
772
- if (debounceTimer) {
773
- clearTimeout(debounceTimer);
774
- }
1109
+ cancelActiveDecision();
1110
+ const releasedDefaultClient = clearDefaultSlowLaneClient(slowLaneClient);
1111
+ ctcTag('init', `slow lane teardown · id=${slowLaneClientId} · default=${releasedDefaultClient ? 'released' : 'retained newer owner'}`, releasedDefaultClient ? CTC_STYLES.dim : CTC_STYLES.warn);
775
1112
  if (hasDestroy(slowLaneClient)) {
776
1113
  slowLaneClient.destroy();
777
1114
  }
778
- ingestedContextTexts.clear();
779
- setDefaultSlowLaneClient(null);
1115
+ pendingSessionContextTexts.clear();
1116
+ sessionIngestedContextTexts.clear();
780
1117
  }
781
1118
  };
782
1119
  }