@atlaskit/editor-plugin-autocomplete 9.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.
@@ -511,7 +511,63 @@ export const createAutocompletePlugin = (options, api) => {
511
511
  // Set when the plugin is torn down so in-flight getContext() resolutions don't
512
512
  // mutate the global text-predictor state after destruction.
513
513
  let destroyed = false;
514
- 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
+ };
515
571
  const logContextResolved = (source, context) => {
516
572
  var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
517
573
  if (!isAutocompleteDebugEnabled()) {
@@ -539,19 +595,10 @@ export const createAutocompletePlugin = (options, api) => {
539
595
  ...definedContext
540
596
  };
541
597
  const ingestContextText = text => {
542
- if (!text || ingestedContextTexts.has(text)) {
598
+ if (!text || sessionIngestedContextTexts.has(text)) {
543
599
  return;
544
600
  }
545
- ingestedContextTexts.add(text);
546
- // Evict oldest entries (Set preserves insertion order) to bound memory.
547
- while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
548
- const oldest = ingestedContextTexts.values().next().value;
549
- if (oldest === undefined) {
550
- break;
551
- }
552
- ingestedContextTexts.delete(oldest);
553
- }
554
- ingestDocumentPage(text);
601
+ addBoundedContextText(pendingSessionContextTexts, text);
555
602
  };
556
603
  ingestContextText(context.fullPageContent);
557
604
  ingestContextText(context.parentCommentContent);
@@ -559,6 +606,7 @@ export const createAutocompletePlugin = (options, api) => {
559
606
  var _context$siblingComme6;
560
607
  ingestContextText(siblingCommentContent);
561
608
  }
609
+ flushPendingSessionContext();
562
610
 
563
611
  // Context arrived after word boundaries may already have fired. Re-send
564
612
  // slow-lane context immediately so the next inference includes the thread.
@@ -962,18 +1010,11 @@ export const createAutocompletePlugin = (options, api) => {
962
1010
  return accepted;
963
1011
  },
964
1012
  focus: () => {
965
- var _options$useLocalMode, _options$useLocalMode2, _options$useLocalMode3;
1013
+ var _options$useLocalMode2, _options$useLocalMode3;
966
1014
  if (!isAutocompleteEnabled) {
967
1015
  return false;
968
1016
  }
969
- loadDefaultVocabulary({
970
- isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false,
971
- surface
972
- }).catch(error => {
973
- logException(error, {
974
- location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
975
- });
976
- });
1017
+ void ensureVocabularyReady();
977
1018
  loadVectorsAsync({
978
1019
  isLocalLLM: (_options$useLocalMode2 = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode2 !== void 0 ? _options$useLocalMode2 : false,
979
1020
  surface
@@ -1071,7 +1112,8 @@ export const createAutocompletePlugin = (options, api) => {
1071
1112
  if (hasDestroy(slowLaneClient)) {
1072
1113
  slowLaneClient.destroy();
1073
1114
  }
1074
- ingestedContextTexts.clear();
1115
+ pendingSessionContextTexts.clear();
1116
+ sessionIngestedContextTexts.clear();
1075
1117
  }
1076
1118
  };
1077
1119
  }
@@ -89,7 +89,7 @@ const printLegend = verbose => {
89
89
  // eslint-disable-next-line no-console
90
90
  console.log('%cPlanes%c [CTC:init] loads · [CTC:signal] semantic/network · [CTC:model] primes/exact · [CTC:model-cost] prefill/decode · [CTC:readiness] deadline progress', CTC_STYLES.section, CTC_STYLES.body);
91
91
  // eslint-disable-next-line no-console
92
- console.log('%cInspect%c __atlCtcDebug__.enable("verbose") · .disable()', CTC_STYLES.section, CTC_STYLES.body);
92
+ console.log('%cInspect%c __atlCtcDebug__.enable("verbose") · .disable() · .session() (L1 boosts, narrow to a family with .session("poll"))', CTC_STYLES.section, CTC_STYLES.body);
93
93
  // eslint-disable-next-line no-console
94
94
  console.groupEnd();
95
95
  };
@@ -133,5 +133,20 @@ export const isAutocompleteDebugVerbose = () => {
133
133
  return (_getDebugApi$isVerbos = (_getDebugApi2 = getDebugApi()) === null || _getDebugApi2 === void 0 ? void 0 : _getDebugApi2.isVerbose()) !== null && _getDebugApi$isVerbos !== void 0 ? _getDebugApi$isVerbos : false;
134
134
  };
135
135
 
136
+ /**
137
+ * Hang the L1 session-boost snapshot off the console API.
138
+ *
139
+ * Unlike the log helpers this is available whether or not debug is enabled:
140
+ * inspecting state on demand is not logging, and asking someone to turn on
141
+ * logging and retype to find out what the session already holds defeats the
142
+ * point of being able to ask.
143
+ */
144
+ export const registerCtcSessionInspector = inspect => {
145
+ const api = getDebugApi();
146
+ if (api) {
147
+ api.session = inspect;
148
+ }
149
+ };
150
+
136
151
  // Eagerly install so the console API is available on load, regardless of call order.
137
152
  getDebugApi();
@@ -13,14 +13,17 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
13
13
  * Falls back to cold mode (freq-only) when vectors not yet loaded.
14
14
  *
15
15
  * Session personalization (L1): words the user types are incrementally boosted
16
- * via incrementSessionFreq(), called on word boundaries from the plugin.
16
+ * via incrementSessionFreq(), called on word boundaries from the plugin, and
17
+ * words in ingested context text via ingestDocumentPage(). What the session has
18
+ * boosted is visible at any time from the console: `__atlCtcDebug__.session()`,
19
+ * or `__atlCtcDebug__.session('poll')` for one family — see inspectSessionBoosts.
17
20
  */
18
21
 
19
22
  import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
20
23
  import { fetchAutocompleteArtifactBinary, fetchAutocompleteArtifactJson } from './artifact-loader';
21
24
  import { ARTIFACT_NAME } from './artifacts-manifest';
22
25
  import { createCanonicalContextPositionCache, deriveCanonicalCandidateContext, deriveWhitespaceBoundaryContext, logSoftmaxAt, logSumExp, selectBoundaryPrimeRequests } from './canonical-lm-scoring';
23
- import { CTC_STYLES, ctcSection, ctcTag, isAutocompleteDebugEnabled, isAutocompleteDebugVerbose } from './debug-mode';
26
+ import { CTC_STYLES, ctcSection, ctcTag, isAutocompleteDebugEnabled, isAutocompleteDebugVerbose, registerCtcSessionInspector } from './debug-mode';
24
27
  import { loadGrammarDataAsync, rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE, MIN_WINNER_MARGIN } from './scoring-pipeline';
25
28
  import { getBoundaryLmState, getCanonicalSurfaceCount, getCanonicalSurfaceTokenIds, getDefaultSlowLaneClientStatus, getProgressiveSurfaceEvidence, getStoredContextInput, getStoredContextVector, getStoredLmLogits, getSurfaceScore, isCanonicalSurfaceScoringSupported, primeBoundaryLm, requestProgressiveSurfaceScores } from './slow-lane-client';
26
29
 
@@ -298,6 +301,44 @@ class WeightedWordTrie {
298
301
  node.sessionFreq += 1;
299
302
  return true;
300
303
  }
304
+
305
+ /**
306
+ * Every word carrying a session boost, optionally limited to one prefix's
307
+ * subtree. Unlike `getCandidates` a word equal to the prefix is included,
308
+ * since the question here is what the session holds rather than what could
309
+ * still be typed.
310
+ *
311
+ * Walks the trie instead of reading an index, so nothing has to be kept in
312
+ * step on the write path for the sake of being able to ask.
313
+ */
314
+ collectSessionBoosted(prefix = '') {
315
+ let node = this.root;
316
+ for (const char of prefix.toLowerCase()) {
317
+ const next = node.children.get(char);
318
+ if (!next) {
319
+ return [];
320
+ }
321
+ node = next;
322
+ }
323
+ const boosted = [];
324
+ const stack = [node];
325
+ while (stack.length > 0) {
326
+ const current = stack.pop();
327
+ if (!current) {
328
+ continue;
329
+ }
330
+ if (current.word !== null && current.sessionFreq > 0) {
331
+ boosted.push({
332
+ word: current.word,
333
+ node: current
334
+ });
335
+ }
336
+ for (const child of current.children.values()) {
337
+ stack.push(child);
338
+ }
339
+ }
340
+ return boosted;
341
+ }
301
342
  }
302
343
 
303
344
  // L1/L2 Trie (Session + Atlassian Domain)
@@ -672,7 +713,7 @@ export const ingestDocumentPage = pageContent => {
672
713
  }
673
714
  }
674
715
  if (isAutocompleteDebugEnabled() && validBoostedWords.size > 0) {
675
- ctcTag('init', `L1 session primed ${validBoostedWords.size} words from page`, CTC_STYLES.brand);
716
+ ctcTag('init', `L1 session primed ${validBoostedWords.size} words from page · __atlCtcDebug__.session() to inspect`, CTC_STYLES.brand);
676
717
  if (isAutocompleteDebugVerbose()) {
677
718
  // eslint-disable-next-line no-console
678
719
  console.dir(Array.from(validBoostedWords).sort());
@@ -680,6 +721,53 @@ export const ingestDocumentPage = pageContent => {
680
721
  }
681
722
  };
682
723
 
724
+ /**
725
+ * How many boosted words `inspectSessionBoosts` lists.
726
+ *
727
+ * A page ingest can boost thousands, and a list that long is not read. The
728
+ * strongest boosts are the ones that change an ordering, and `boosted` still
729
+ * reports the full size, so the cap loses nothing but volume.
730
+ */
731
+ const MAX_LISTED_SESSION_WORDS = 50;
732
+ /**
733
+ * Read the session's L1 boosts, optionally narrowed to a prefix.
734
+ *
735
+ * Installed as `__atlCtcDebug__.session()`, with `__atlCtcDebug__.session('poll')`
736
+ * to ask about one family. Returned rather than logged, so the console renders it
737
+ * as an inspectable object and a caller can assert on it.
738
+ *
739
+ * Only words the vocabulary already holds can carry a boost, because both writers
740
+ * go through `incrementSessionFreq` and it only finds existing nodes. An ingested
741
+ * word absent from the vocabulary is therefore missing from here and always will
742
+ * be.
743
+ */
744
+ export const inspectSessionBoosts = prefix => {
745
+ const boosted = wordTrie.collectSessionBoosted(prefix !== null && prefix !== void 0 ? prefix : '');
746
+ return {
747
+ boosted: boosted.length,
748
+ limit: MAX_LISTED_SESSION_WORDS,
749
+ ...(prefix === undefined ? {} : {
750
+ prefix
751
+ }),
752
+ words: boosted.sort((a, b) => b.node.sessionFreq - a.node.sessionFreq || a.word.localeCompare(b.word, 'en', {
753
+ numeric: true,
754
+ sensitivity: 'base'
755
+ })).slice(0, MAX_LISTED_SESSION_WORDS).map(({
756
+ node,
757
+ word
758
+ }) => ({
759
+ sessionFreq: node.sessionFreq,
760
+ sessionOnly: node.tenantFreq === 0,
761
+ surface: word,
762
+ tenantFreq: node.tenantFreq
763
+ }))
764
+ };
765
+ };
766
+
767
+ // At module scope so the console answers before the first keystroke, which is
768
+ // when someone reaching for it usually asks.
769
+ registerCtcSessionInspector(inspectSessionBoosts);
770
+
683
771
  /**
684
772
  * Result of a prediction: the ghost tail to insert plus an immutable record of
685
773
  * the evidence that authorized the UI commitment.
@@ -509,7 +509,72 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
509
509
  // Set when the plugin is torn down so in-flight getContext() resolutions don't
510
510
  // mutate the global text-predictor state after destruction.
511
511
  var destroyed = false;
512
- var ingestedContextTexts = new Set();
512
+ // L1 ingestion cannot run before the vocabulary has loaded: `incrementSessionFreq`
513
+ // only touches trie nodes that already exist, so an empty trie silently rejects
514
+ // every word. Text that arrives first — which a page usually does, since the
515
+ // chat resolves it on focus in the same tick as the load starts — is held here
516
+ // and applied once the load has settled.
517
+ var pendingSessionContextTexts = new Set();
518
+ var sessionIngestedContextTexts = new Set();
519
+ var isVocabularyReady = false;
520
+ var vocabularyLoadPromise;
521
+ var addBoundedContextText = function addBoundedContextText(texts, text) {
522
+ texts.add(text);
523
+ // Evict oldest entries (Set preserves insertion order) to bound memory.
524
+ while (texts.size > MAX_INGESTED_CONTEXT_TEXTS) {
525
+ var oldest = texts.values().next().value;
526
+ if (oldest === undefined) {
527
+ break;
528
+ }
529
+ texts.delete(oldest);
530
+ }
531
+ };
532
+ var flushPendingSessionContext = function flushPendingSessionContext() {
533
+ if (!isVocabularyReady || destroyed) {
534
+ return;
535
+ }
536
+ var _iterator = _createForOfIteratorHelper(pendingSessionContextTexts),
537
+ _step;
538
+ try {
539
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
540
+ var text = _step.value;
541
+ if (!sessionIngestedContextTexts.has(text)) {
542
+ ingestDocumentPage(text);
543
+ addBoundedContextText(sessionIngestedContextTexts, text);
544
+ }
545
+ pendingSessionContextTexts.delete(text);
546
+ }
547
+ } catch (err) {
548
+ _iterator.e(err);
549
+ } finally {
550
+ _iterator.f();
551
+ }
552
+ };
553
+ var ensureVocabularyReady = function ensureVocabularyReady() {
554
+ var _options$useLocalMode;
555
+ if (isVocabularyReady) {
556
+ flushPendingSessionContext();
557
+ return Promise.resolve();
558
+ }
559
+ vocabularyLoadPromise !== null && vocabularyLoadPromise !== void 0 ? vocabularyLoadPromise : vocabularyLoadPromise = loadDefaultVocabulary({
560
+ isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false,
561
+ surface: surface
562
+ }).then(function () {
563
+ if (destroyed) {
564
+ return;
565
+ }
566
+ isVocabularyReady = true;
567
+ flushPendingSessionContext();
568
+ }).catch(function (error) {
569
+ // Do not consume the pending text on failure. A later focus retries the
570
+ // load and can still apply the original context exactly once.
571
+ vocabularyLoadPromise = undefined;
572
+ logException(error, {
573
+ location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
574
+ });
575
+ });
576
+ return vocabularyLoadPromise;
577
+ };
513
578
  var logContextResolved = function logContextResolved(source, context) {
514
579
  var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
515
580
  if (!isAutocompleteDebugEnabled()) {
@@ -539,37 +604,29 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
539
604
  }));
540
605
  resolvedContext = _objectSpread(_objectSpread({}, resolvedContext), definedContext);
541
606
  var ingestContextText = function ingestContextText(text) {
542
- if (!text || ingestedContextTexts.has(text)) {
607
+ if (!text || sessionIngestedContextTexts.has(text)) {
543
608
  return;
544
609
  }
545
- ingestedContextTexts.add(text);
546
- // Evict oldest entries (Set preserves insertion order) to bound memory.
547
- while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
548
- var oldest = ingestedContextTexts.values().next().value;
549
- if (oldest === undefined) {
550
- break;
551
- }
552
- ingestedContextTexts.delete(oldest);
553
- }
554
- ingestDocumentPage(text);
610
+ addBoundedContextText(pendingSessionContextTexts, text);
555
611
  };
556
612
  ingestContextText(context.fullPageContent);
557
613
  ingestContextText(context.parentCommentContent);
558
- var _iterator = _createForOfIteratorHelper((_context$siblingComme6 = context.siblingCommentsContents) !== null && _context$siblingComme6 !== void 0 ? _context$siblingComme6 : []),
559
- _step;
614
+ var _iterator2 = _createForOfIteratorHelper((_context$siblingComme6 = context.siblingCommentsContents) !== null && _context$siblingComme6 !== void 0 ? _context$siblingComme6 : []),
615
+ _step2;
560
616
  try {
561
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
562
- var siblingCommentContent = _step.value;
617
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
618
+ var siblingCommentContent = _step2.value;
563
619
  ingestContextText(siblingCommentContent);
564
620
  }
565
-
566
- // Context arrived after word boundaries may already have fired. Re-send
567
- // slow-lane context immediately so the next inference includes the thread.
568
621
  } catch (err) {
569
- _iterator.e(err);
622
+ _iterator2.e(err);
570
623
  } finally {
571
- _iterator.f();
624
+ _iterator2.f();
572
625
  }
626
+ flushPendingSessionContext();
627
+
628
+ // Context arrived after word boundaries may already have fired. Re-send
629
+ // slow-lane context immediately so the next inference includes the thread.
573
630
  if (currentView) {
574
631
  slowLaneClient.updateContext(buildSlowLaneText(currentView.state.doc.textContent, resolvedContext));
575
632
  }
@@ -960,18 +1017,11 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
960
1017
  return accepted;
961
1018
  },
962
1019
  focus: function focus() {
963
- var _options$useLocalMode, _options$useLocalMode2, _options$useLocalMode3;
1020
+ var _options$useLocalMode2, _options$useLocalMode3;
964
1021
  if (!isAutocompleteEnabled) {
965
1022
  return false;
966
1023
  }
967
- loadDefaultVocabulary({
968
- isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false,
969
- surface: surface
970
- }).catch(function (error) {
971
- logException(error, {
972
- location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
973
- });
974
- });
1024
+ void ensureVocabularyReady();
975
1025
  loadVectorsAsync({
976
1026
  isLocalLLM: (_options$useLocalMode2 = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode2 !== void 0 ? _options$useLocalMode2 : false,
977
1027
  surface: surface
@@ -1069,7 +1119,8 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
1069
1119
  if (hasDestroy(slowLaneClient)) {
1070
1120
  slowLaneClient.destroy();
1071
1121
  }
1072
- ingestedContextTexts.clear();
1122
+ pendingSessionContextTexts.clear();
1123
+ sessionIngestedContextTexts.clear();
1073
1124
  }
1074
1125
  };
1075
1126
  }
@@ -90,7 +90,7 @@ var printLegend = function printLegend(verbose) {
90
90
  // eslint-disable-next-line no-console
91
91
  console.log('%cPlanes%c [CTC:init] loads · [CTC:signal] semantic/network · [CTC:model] primes/exact · [CTC:model-cost] prefill/decode · [CTC:readiness] deadline progress', CTC_STYLES.section, CTC_STYLES.body);
92
92
  // eslint-disable-next-line no-console
93
- console.log('%cInspect%c __atlCtcDebug__.enable("verbose") · .disable()', CTC_STYLES.section, CTC_STYLES.body);
93
+ console.log('%cInspect%c __atlCtcDebug__.enable("verbose") · .disable() · .session() (L1 boosts, narrow to a family with .session("poll"))', CTC_STYLES.section, CTC_STYLES.body);
94
94
  // eslint-disable-next-line no-console
95
95
  console.groupEnd();
96
96
  };
@@ -138,5 +138,20 @@ export var isAutocompleteDebugVerbose = function isAutocompleteDebugVerbose() {
138
138
  return (_getDebugApi$isVerbos = (_getDebugApi2 = getDebugApi()) === null || _getDebugApi2 === void 0 ? void 0 : _getDebugApi2.isVerbose()) !== null && _getDebugApi$isVerbos !== void 0 ? _getDebugApi$isVerbos : false;
139
139
  };
140
140
 
141
+ /**
142
+ * Hang the L1 session-boost snapshot off the console API.
143
+ *
144
+ * Unlike the log helpers this is available whether or not debug is enabled:
145
+ * inspecting state on demand is not logging, and asking someone to turn on
146
+ * logging and retype to find out what the session already holds defeats the
147
+ * point of being able to ask.
148
+ */
149
+ export var registerCtcSessionInspector = function registerCtcSessionInspector(inspect) {
150
+ var api = getDebugApi();
151
+ if (api) {
152
+ api.session = inspect;
153
+ }
154
+ };
155
+
141
156
  // Eagerly install so the console API is available on load, regardless of call order.
142
157
  getDebugApi();