@atlaskit/editor-plugin-autocomplete 4.0.2 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @atlaskit/editor-plugin-autocomplete
2
2
 
3
+ ## 4.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`ac12ed0b41ef4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ac12ed0b41ef4) -
8
+ Refactor contextual autocomplete wiring so the conversation-store-aware ChatInput owns the
9
+ autocomplete context bridge (getContext + subscribeToContextUpdates) via a new
10
+ useAutocompleteEditorContext hook, and the shared RovoChatPromptInput simply forwards a single
11
+ `autocomplete` prop to the editor preset. Removes the duplicated keying / listener-set plumbing
12
+ from the reusable input.
13
+
14
+ Adds an optional `subscribeToContextUpdates` option to the autocomplete plugin so host surfaces
15
+ that stream context after mount (e.g. Rovo chat messages) can push refreshes, complementing the
16
+ existing word-boundary retry that only covers a one-time, still-loading comment thread.
17
+
18
+ ## 4.0.3
19
+
20
+ ### Patch Changes
21
+
22
+ - [`9869d944172b6`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/9869d944172b6) -
23
+ Instrument the local (LocalLLM) slow-lane client with the existing `slow-lane-fetch` UFO
24
+ experience, tagged with an `isLocalLLM` flag (matching `load-vectors`/`load-vocabulary`) so
25
+ on-device inference latency and success-rate feed the same FE Reliability SLO as the network
26
+ slow-lane fetch. The `isLocalLLM` flag is now also emitted on the abort path of both the local and
27
+ network clients.
28
+
3
29
  ## 4.0.2
4
30
 
5
31
  ### Patch Changes
@@ -7,7 +7,8 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.succeedExp = exports.startExp = exports.failExp = exports.abortExp = exports.EXPERIENCE_NAME = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _ufo = require("@atlaskit/ufo");
10
- /**
10
+ 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; }
11
+ 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; } /**
11
12
  * UFO experience tracking helpers for `@atlaskit/editor-plugin-autocomplete`.
12
13
  *
13
14
  * Pattern follows `packages/linking-platform/smart-card/src/state/analytics/ufoExperiences.ts`
@@ -19,7 +20,6 @@ var _ufo = require("@atlaskit/ufo");
19
20
  * they can power FE Reliability SLOs:
20
21
  * https://hello.atlassian.net/wiki/spaces/AA6/pages/3961393753
21
22
  */
22
-
23
23
  /**
24
24
  * Experience name strings are surfaced downstream by the UFO pipeline as
25
25
  * `platform.fe.<type>.<platform.component>.<name>` (e.g.
@@ -95,15 +95,21 @@ var failExp = exports.failExp = function failExp(name, id, metadata) {
95
95
  // UFO errors must never break the plugin
96
96
  }
97
97
  };
98
- var abortExp = exports.abortExp = function abortExp(name, id, reason) {
98
+ var abortExp = exports.abortExp = function abortExp(name, id, reason, metadata) {
99
99
  if (!isUfoEnabled()) {
100
100
  return;
101
101
  }
102
102
  try {
103
- experiences[name].getInstance(id).abort(reason ? {
104
- metadata: {
105
- reason: reason
106
- }
103
+ // `reason` is the dedicated, canonical channel for the abort reason and
104
+ // intentionally takes precedence over a `reason` key in `metadata`. Callers
105
+ // must pass the reason via the param, not inside `metadata` (the `reason`
106
+ // key there is reserved and would be overwritten).
107
+ var merged = _objectSpread(_objectSpread({}, metadata), reason ? {
108
+ reason: reason
109
+ } : {});
110
+ var hasMetadata = Object.keys(merged).length > 0;
111
+ experiences[name].getInstance(id).abort(hasMetadata ? {
112
+ metadata: merged
107
113
  } : undefined);
108
114
  } catch (_unused4) {
109
115
  // UFO errors must never break the plugin
@@ -249,6 +249,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
249
249
  * user has already typed several words before the promise resolved.
250
250
  */
251
251
  var currentView = null;
252
+ var unsubscribeFromContextUpdates;
252
253
  /**
253
254
  * Set after accepting a suggestion so the next doc-change update
254
255
  * skips scheduling a new prediction for the just-inserted text.
@@ -670,7 +671,21 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
670
671
  }
671
672
  }
672
673
  },
673
- view: function view() {
674
+ view: function view(editorView) {
675
+ // Capture up front so a subscription notification before the first PM
676
+ // transaction can still drive slowLaneClient.updateContext (gated on currentView).
677
+ currentView = editorView;
678
+
679
+ // Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
680
+ if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
681
+ unsubscribeFromContextUpdates = options.subscribeToContextUpdates(function () {
682
+ // Bypass throttle for freshness; the in-flight guard prevents overlap.
683
+ refreshContext({
684
+ source: 'subscription',
685
+ allowThrottle: false
686
+ });
687
+ });
688
+ }
674
689
  return {
675
690
  update: function update(view, prevState) {
676
691
  if (!isAutocompleteEnabled) {
@@ -699,7 +714,9 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
699
714
  // thread still loading). Retry on word boundaries until we have
700
715
  // the parent comment, throttled so we don't refetch constantly
701
716
  // and capped so non-comment editors stop retrying entirely.
702
- if (!((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
717
+ // Skipped for push-channel hosts (subscribeToContextUpdates): they
718
+ // never grow parentCommentContent, so polling only burns the budget.
719
+ if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
703
720
  // Only count the attempt when a fetch actually started, so an
704
721
  // in-flight or throttled no-op doesn't burn the retry budget.
705
722
  if (refreshContext({
@@ -713,8 +730,11 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
713
730
  }
714
731
  },
715
732
  destroy: function destroy() {
733
+ var _unsubscribeFromConte;
716
734
  destroyed = true;
717
735
  currentView = null;
736
+ (_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 || _unsubscribeFromConte();
737
+ unsubscribeFromContextUpdates = undefined;
718
738
  if (debounceTimer) {
719
739
  clearTimeout(debounceTimer);
720
740
  }
@@ -14,6 +14,7 @@ var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
14
14
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
15
15
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
16
16
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
17
+ var _ufo = require("../analytics/ufo");
17
18
  var _debugMode = require("./debug-mode");
18
19
  var _slowLaneClient = require("./slow-lane-client");
19
20
  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; }
@@ -263,7 +264,7 @@ var bePayloadDataPromise;
263
264
  * :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
264
265
  * :returns: The parsed JSON value, or `null` if neither interop mode applies.
265
266
  */
266
- var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
267
+ function unwrapJsonModule(mod, shape) {
267
268
  if (mod == null || (0, _typeof2.default)(mod) !== 'object') {
268
269
  return null;
269
270
  }
@@ -315,7 +316,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
315
316
  return namespace.default;
316
317
  }
317
318
  return null;
318
- };
319
+ }
319
320
 
320
321
  /**
321
322
  * Lazily load and build the BE-parity lookup tables from their JSON payloads.
@@ -959,17 +960,23 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
959
960
  */
960
961
  var runInference = /*#__PURE__*/function () {
961
962
  var _ref0 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(text, requestId) {
962
- var lmText, semanticText, arcticInput, captureCompletionTime, _data, tStart, tLmDone, tEmbDone, _yield$Promise$all5, _yield$Promise$all6, embeddingResponse, rawLogits, payload, embedding, sumSq, i, topTokens, errorMsg, _t5;
963
+ var experienceId, lmText, semanticText, arcticInput, captureCompletionTime, _data, tStart, tLmDone, tEmbDone, _yield$Promise$all5, _yield$Promise$all6, embeddingResponse, rawLogits, payload, embedding, sumSq, i, topTokens, errorMsg, _t5;
963
964
  return _regenerator.default.wrap(function (_context4) {
964
965
  while (1) switch (_context4.prev = _context4.next) {
965
966
  case 0:
967
+ captureCompletionTime = function _captureCompletionTim(promise, onResolved) {
968
+ return promise.then(function (value) {
969
+ onResolved(performance.now());
970
+ return value;
971
+ });
972
+ };
966
973
  if (!(!engine || destroyed)) {
967
974
  _context4.next = 1;
968
975
  break;
969
976
  }
970
977
  return _context4.abrupt("return");
971
978
  case 1:
972
- // Clear the capture buffer so we read only this pass's logits. The engine
979
+ experienceId = String(requestId); // Clear the capture buffer so we read only this pass's logits. The engine
973
980
  // serialises per-model requests and updateContext is debounced, so the
974
981
  // latest request's decode step is the last to populate `captured` before
975
982
  // we read it below; stale requests bail on the latestRequestId guard.
@@ -982,12 +989,6 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
982
989
  lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
983
990
  semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
984
991
  arcticInput = wrapForArctic(semanticText);
985
- captureCompletionTime = function captureCompletionTime(promise, onResolved) {
986
- return promise.then(function (value) {
987
- onResolved(performance.now());
988
- return value;
989
- });
990
- };
991
992
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
992
993
  // eslint-disable-next-line no-console
993
994
  console.log("%c[LocalSlowLane] %c\uD83D\uDD22 Arctic input (".concat(arcticInput.length, " chars, ").concat(splitOnWhitespace(semanticText).length, " words): \"").concat(arcticInput.length > 100 ? "".concat(arcticInput.slice(0, 100), "\u2026") : arcticInput, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #009688;');
@@ -995,6 +996,16 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
995
996
  console.log("%c[LocalSlowLane] %c\uD83E\uDDE0 LM input (".concat(lmText.length, " chars, ").concat(splitOnWhitespace(lmText).length, " words): \"").concat(lmText.length > 100 ? "".concat(lmText.slice(0, 100), "\u2026") : lmText, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #2196f3;');
996
997
  }
997
998
  _context4.prev = 2;
999
+ // Reuse the network slow-lane-fetch UFO experience (tagged isLocalLLM:true,
1000
+ // matching LOAD_VECTORS/LOAD_VOCABULARY) so on-device inference
1001
+ // latency/success-rate feeds the same FE Reliability SLO. Started inside
1002
+ // the try — post engine-init (parity with the network fetch, which
1003
+ // excludes the one-time model load) and so the catch below always
1004
+ // terminates the experience, even on a synchronous throw.
1005
+ (0, _ufo.startExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, {
1006
+ textLength: text.length,
1007
+ isLocalLLM: true
1008
+ });
998
1009
  tStart = performance.now();
999
1010
  tLmDone = 0;
1000
1011
  tEmbDone = 0;
@@ -1027,6 +1038,9 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
1027
1038
  _context4.next = 4;
1028
1039
  break;
1029
1040
  }
1041
+ (0, _ufo.abortExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, destroyed ? 'destroyed' : 'superseded', {
1042
+ isLocalLLM: true
1043
+ });
1030
1044
  return _context4.abrupt("return");
1031
1045
  case 4:
1032
1046
  // ── LM logits: whole-word BE-parity payload ──────────────────
@@ -1078,6 +1092,12 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
1078
1092
  // eslint-disable-next-line no-console
1079
1093
  console.groupEnd();
1080
1094
  }
1095
+ (0, _ufo.succeedExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, {
1096
+ textLength: text.length,
1097
+ hasVector: storedContextVector !== null,
1098
+ hasLmLogits: storedLmLogits !== null,
1099
+ isLocalLLM: true
1100
+ });
1081
1101
  onUpdate === null || onUpdate === void 0 || onUpdate({
1082
1102
  textLength: text.length,
1083
1103
  hasVector: storedContextVector !== null,
@@ -1092,10 +1112,17 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
1092
1112
  _context4.next = 6;
1093
1113
  break;
1094
1114
  }
1115
+ (0, _ufo.abortExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, destroyed ? 'destroyed' : 'superseded', {
1116
+ isLocalLLM: true
1117
+ });
1095
1118
  return _context4.abrupt("return");
1096
1119
  case 6:
1097
1120
  storedContextVector = null;
1098
1121
  storedLmLogits = null;
1122
+ (0, _ufo.failExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, {
1123
+ errorType: 'inference',
1124
+ isLocalLLM: true
1125
+ });
1099
1126
  onUpdate === null || onUpdate === void 0 || onUpdate({
1100
1127
  textLength: text.length,
1101
1128
  hasVector: false,
@@ -87,7 +87,8 @@ var createSlowLaneClient = exports.createSlowLaneClient = function createSlowLan
87
87
  session_id: sessionId
88
88
  };
89
89
  (0, _ufo.startExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
90
- textLength: text.length
90
+ textLength: text.length,
91
+ isLocalLLM: false
91
92
  });
92
93
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
93
94
  // eslint-disable-next-line no-console
@@ -116,7 +117,8 @@ var createSlowLaneClient = exports.createSlowLaneClient = function createSlowLan
116
117
  storedLmLogits = null;
117
118
  (0, _ufo.failExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
118
119
  status: res.status,
119
- errorType: 'http_error'
120
+ errorType: 'http_error',
121
+ isLocalLLM: false
120
122
  });
121
123
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
122
124
  // eslint-disable-next-line no-console
@@ -151,7 +153,8 @@ var createSlowLaneClient = exports.createSlowLaneClient = function createSlowLan
151
153
  (0, _ufo.succeedExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
152
154
  textLength: text.length,
153
155
  hasVector: storedContextVector !== null,
154
- hasLmLogits: storedLmLogits !== null
156
+ hasLmLogits: storedLmLogits !== null,
157
+ isLocalLLM: false
155
158
  });
156
159
  onUpdate === null || onUpdate === void 0 || onUpdate({
157
160
  textLength: text.length,
@@ -167,7 +170,8 @@ var createSlowLaneClient = exports.createSlowLaneClient = function createSlowLan
167
170
  storedContextVector = null;
168
171
  storedLmLogits = null;
169
172
  (0, _ufo.failExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
170
- errorType: 'network'
173
+ errorType: 'network',
174
+ isLocalLLM: false
171
175
  });
172
176
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
173
177
  // eslint-disable-next-line no-console
@@ -197,7 +201,9 @@ var createSlowLaneClient = exports.createSlowLaneClient = function createSlowLan
197
201
  debounceTimer = setTimeout(function () {
198
202
  debounceTimer = null;
199
203
  if (inflightRequestId !== null) {
200
- (0, _ufo.abortExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, inflightRequestId, 'superseded');
204
+ (0, _ufo.abortExp)(_ufo.EXPERIENCE_NAME.SLOW_LANE_FETCH, inflightRequestId, 'superseded', {
205
+ isLocalLLM: false
206
+ });
201
207
  }
202
208
  var requestId = String(++requestSeq);
203
209
  inflightRequestId = requestId;
@@ -694,7 +694,7 @@ var predict = exports.predict = function predict(textBefore) {
694
694
  * array and a sparse numeric-keyed object are emitted identically as named
695
695
  * exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
696
696
  */
697
- var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
697
+ function unwrapJsonModule(mod, shape) {
698
698
  if (mod == null || (0, _typeof2.default)(mod) !== 'object') {
699
699
  return null;
700
700
  }
@@ -730,7 +730,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
730
730
  return namespace.default;
731
731
  }
732
732
  return null;
733
- };
733
+ }
734
734
  var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
735
735
  var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
736
736
  var _options$isLocalLLM;
@@ -94,15 +94,24 @@ export const failExp = (name, id, metadata) => {
94
94
  // UFO errors must never break the plugin
95
95
  }
96
96
  };
97
- export const abortExp = (name, id, reason) => {
97
+ export const abortExp = (name, id, reason, metadata) => {
98
98
  if (!isUfoEnabled()) {
99
99
  return;
100
100
  }
101
101
  try {
102
- experiences[name].getInstance(id).abort(reason ? {
103
- metadata: {
102
+ // `reason` is the dedicated, canonical channel for the abort reason and
103
+ // intentionally takes precedence over a `reason` key in `metadata`. Callers
104
+ // must pass the reason via the param, not inside `metadata` (the `reason`
105
+ // key there is reserved and would be overwritten).
106
+ const merged = {
107
+ ...metadata,
108
+ ...(reason ? {
104
109
  reason
105
- }
110
+ } : {})
111
+ };
112
+ const hasMetadata = Object.keys(merged).length > 0;
113
+ experiences[name].getInstance(id).abort(hasMetadata ? {
114
+ metadata: merged
106
115
  } : undefined);
107
116
  } catch {
108
117
  // UFO errors must never break the plugin
@@ -237,6 +237,7 @@ export const createAutocompletePlugin = (options, api) => {
237
237
  * user has already typed several words before the promise resolved.
238
238
  */
239
239
  let currentView = null;
240
+ let unsubscribeFromContextUpdates;
240
241
  /**
241
242
  * Set after accepting a suggestion so the next doc-change update
242
243
  * skips scheduling a new prediction for the just-inserted text.
@@ -657,59 +658,80 @@ export const createAutocompletePlugin = (options, api) => {
657
658
  }
658
659
  }
659
660
  },
660
- view: () => ({
661
- update: (view, prevState) => {
662
- if (!isAutocompleteEnabled) {
663
- return;
664
- }
665
- currentView = view;
666
- if (!prevState.doc.eq(view.state.doc)) {
667
- if (justAccepted) {
668
- justAccepted = false;
661
+ view: editorView => {
662
+ // Capture up front so a subscription notification before the first PM
663
+ // transaction can still drive slowLaneClient.updateContext (gated on currentView).
664
+ currentView = editorView;
669
665
 
670
- // Snapshot the post-acceptance text so follow-up transactions hit
671
- // the dismissedContext guard and abort until the user types again.
672
- dismissedContext = getTextBeforeCursor(view.state);
673
- if (debounceTimer) {
674
- clearTimeout(debounceTimer);
675
- }
666
+ // Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
667
+ if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
668
+ unsubscribeFromContextUpdates = options.subscribeToContextUpdates(() => {
669
+ // Bypass throttle for freshness; the in-flight guard prevents overlap.
670
+ refreshContext({
671
+ source: 'subscription',
672
+ allowThrottle: false
673
+ });
674
+ });
675
+ }
676
+ return {
677
+ update: (view, prevState) => {
678
+ if (!isAutocompleteEnabled) {
676
679
  return;
677
680
  }
678
- maybeUpdateSessionFrequency(view, prevState);
679
- const textBefore = getTextBeforeCursor(view.state);
680
- if (isWordBoundary(textBefore)) {
681
- var _resolvedContext;
682
- slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
681
+ currentView = view;
682
+ if (!prevState.doc.eq(view.state.doc)) {
683
+ if (justAccepted) {
684
+ justAccepted = false;
685
+
686
+ // Snapshot the post-acceptance text so follow-up transactions hit
687
+ // the dismissedContext guard and abort until the user types again.
688
+ dismissedContext = getTextBeforeCursor(view.state);
689
+ if (debounceTimer) {
690
+ clearTimeout(debounceTimer);
691
+ }
692
+ return;
693
+ }
694
+ maybeUpdateSessionFrequency(view, prevState);
695
+ const textBefore = getTextBeforeCursor(view.state);
696
+ if (isWordBoundary(textBefore)) {
697
+ var _resolvedContext;
698
+ slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
683
699
 
684
- // Context may not have resolved on first focus (e.g. comment
685
- // thread still loading). Retry on word boundaries until we have
686
- // the parent comment, throttled so we don't refetch constantly
687
- // and capped so non-comment editors stop retrying entirely.
688
- if (!((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
689
- // Only count the attempt when a fetch actually started, so an
690
- // in-flight or throttled no-op doesn't burn the retry budget.
691
- if (refreshContext({
692
- source: 'word-boundary'
693
- })) {
694
- wordBoundaryRefreshAttempts++;
700
+ // Context may not have resolved on first focus (e.g. comment
701
+ // thread still loading). Retry on word boundaries until we have
702
+ // the parent comment, throttled so we don't refetch constantly
703
+ // and capped so non-comment editors stop retrying entirely.
704
+ // Skipped for push-channel hosts (subscribeToContextUpdates): they
705
+ // never grow parentCommentContent, so polling only burns the budget.
706
+ if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
707
+ // Only count the attempt when a fetch actually started, so an
708
+ // in-flight or throttled no-op doesn't burn the retry budget.
709
+ if (refreshContext({
710
+ source: 'word-boundary'
711
+ })) {
712
+ wordBoundaryRefreshAttempts++;
713
+ }
695
714
  }
696
715
  }
716
+ schedulePrediction(view);
697
717
  }
698
- schedulePrediction(view);
699
- }
700
- },
701
- destroy: () => {
702
- destroyed = true;
703
- currentView = null;
704
- if (debounceTimer) {
705
- clearTimeout(debounceTimer);
706
- }
707
- if (hasDestroy(slowLaneClient)) {
708
- slowLaneClient.destroy();
718
+ },
719
+ destroy: () => {
720
+ var _unsubscribeFromConte;
721
+ destroyed = true;
722
+ currentView = null;
723
+ (_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 ? void 0 : _unsubscribeFromConte();
724
+ unsubscribeFromContextUpdates = undefined;
725
+ if (debounceTimer) {
726
+ clearTimeout(debounceTimer);
727
+ }
728
+ if (hasDestroy(slowLaneClient)) {
729
+ slowLaneClient.destroy();
730
+ }
731
+ ingestedContextTexts.clear();
732
+ setDefaultSlowLaneClient(null);
709
733
  }
710
- ingestedContextTexts.clear();
711
- setDefaultSlowLaneClient(null);
712
- }
713
- })
734
+ };
735
+ }
714
736
  });
715
737
  };
@@ -36,6 +36,7 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
36
36
  * asynchronously after each updateContext() call.
37
37
  */
38
38
 
39
+ import { abortExp, EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
39
40
  import { isAutocompleteDebugEnabled } from './debug-mode';
40
41
  import { isWordBoundary } from './slow-lane-client';
41
42
 
@@ -246,7 +247,7 @@ let bePayloadDataPromise;
246
247
  * :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
247
248
  * :returns: The parsed JSON value, or `null` if neither interop mode applies.
248
249
  */
249
- const unwrapJsonModule = (mod, shape) => {
250
+ function unwrapJsonModule(mod, shape) {
250
251
  if (mod == null || typeof mod !== 'object') {
251
252
  return null;
252
253
  }
@@ -287,7 +288,7 @@ const unwrapJsonModule = (mod, shape) => {
287
288
  return namespace.default;
288
289
  }
289
290
  return null;
290
- };
291
+ }
291
292
 
292
293
  /**
293
294
  * Lazily load and build the BE-parity lookup tables from their JSON payloads.
@@ -748,6 +749,7 @@ export const createLocalSlowLaneClient = (config = {}) => {
748
749
  if (!engine || destroyed) {
749
750
  return;
750
751
  }
752
+ const experienceId = String(requestId);
751
753
 
752
754
  // Clear the capture buffer so we read only this pass's logits. The engine
753
755
  // serialises per-model requests and updateContext is debounced, so the
@@ -762,10 +764,12 @@ export const createLocalSlowLaneClient = (config = {}) => {
762
764
  const lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
763
765
  const semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
764
766
  const arcticInput = wrapForArctic(semanticText);
765
- const captureCompletionTime = (promise, onResolved) => promise.then(value => {
766
- onResolved(performance.now());
767
- return value;
768
- });
767
+ function captureCompletionTime(promise, onResolved) {
768
+ return promise.then(value => {
769
+ onResolved(performance.now());
770
+ return value;
771
+ });
772
+ }
769
773
  if (isAutocompleteDebugEnabled()) {
770
774
  // eslint-disable-next-line no-console
771
775
  console.log(`%c[LocalSlowLane] %c🔢 Arctic input (${arcticInput.length} chars, ${splitOnWhitespace(semanticText).length} words): "${arcticInput.length > 100 ? `${arcticInput.slice(0, 100)}…` : arcticInput}"`, 'color: #9c27b0; font-weight: bold;', 'color: #009688;');
@@ -774,6 +778,16 @@ export const createLocalSlowLaneClient = (config = {}) => {
774
778
  }
775
779
  try {
776
780
  var _data, _data$;
781
+ // Reuse the network slow-lane-fetch UFO experience (tagged isLocalLLM:true,
782
+ // matching LOAD_VECTORS/LOAD_VOCABULARY) so on-device inference
783
+ // latency/success-rate feeds the same FE Reliability SLO. Started inside
784
+ // the try — post engine-init (parity with the network fetch, which
785
+ // excludes the one-time model load) and so the catch below always
786
+ // terminates the experience, even on a synchronous throw.
787
+ startExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, {
788
+ textLength: text.length,
789
+ isLocalLLM: true
790
+ });
777
791
  const tStart = performance.now();
778
792
  let tLmDone = 0;
779
793
  let tEmbDone = 0;
@@ -798,6 +812,9 @@ export const createLocalSlowLaneClient = (config = {}) => {
798
812
 
799
813
  // Discard stale results
800
814
  if (requestId < latestRequestId || destroyed) {
815
+ abortExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, destroyed ? 'destroyed' : 'superseded', {
816
+ isLocalLLM: true
817
+ });
801
818
  return;
802
819
  }
803
820
 
@@ -840,6 +857,12 @@ export const createLocalSlowLaneClient = (config = {}) => {
840
857
  // eslint-disable-next-line no-console
841
858
  console.groupEnd();
842
859
  }
860
+ succeedExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, {
861
+ textLength: text.length,
862
+ hasVector: storedContextVector !== null,
863
+ hasLmLogits: storedLmLogits !== null,
864
+ isLocalLLM: true
865
+ });
843
866
  onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate({
844
867
  textLength: text.length,
845
868
  hasVector: storedContextVector !== null,
@@ -848,10 +871,17 @@ export const createLocalSlowLaneClient = (config = {}) => {
848
871
  } catch (err) {
849
872
  // Discard errors for stale requests or after teardown
850
873
  if (requestId < latestRequestId || destroyed) {
874
+ abortExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, destroyed ? 'destroyed' : 'superseded', {
875
+ isLocalLLM: true
876
+ });
851
877
  return;
852
878
  }
853
879
  storedContextVector = null;
854
880
  storedLmLogits = null;
881
+ failExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, experienceId, {
882
+ errorType: 'inference',
883
+ isLocalLLM: true
884
+ });
855
885
  onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate({
856
886
  textLength: text.length,
857
887
  hasVector: false,
@@ -69,7 +69,8 @@ export const createSlowLaneClient = config => {
69
69
  session_id: sessionId
70
70
  };
71
71
  startExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
72
- textLength: text.length
72
+ textLength: text.length,
73
+ isLocalLLM: false
73
74
  });
74
75
  if (isAutocompleteDebugEnabled()) {
75
76
  // eslint-disable-next-line no-console
@@ -92,7 +93,8 @@ export const createSlowLaneClient = config => {
92
93
  storedLmLogits = null;
93
94
  failExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
94
95
  status: res.status,
95
- errorType: 'http_error'
96
+ errorType: 'http_error',
97
+ isLocalLLM: false
96
98
  });
97
99
  if (isAutocompleteDebugEnabled()) {
98
100
  // eslint-disable-next-line no-console
@@ -124,7 +126,8 @@ export const createSlowLaneClient = config => {
124
126
  succeedExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
125
127
  textLength: text.length,
126
128
  hasVector: storedContextVector !== null,
127
- hasLmLogits: storedLmLogits !== null
129
+ hasLmLogits: storedLmLogits !== null,
130
+ isLocalLLM: false
128
131
  });
129
132
  onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate({
130
133
  textLength: text.length,
@@ -136,7 +139,8 @@ export const createSlowLaneClient = config => {
136
139
  storedContextVector = null;
137
140
  storedLmLogits = null;
138
141
  failExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
139
- errorType: 'network'
142
+ errorType: 'network',
143
+ isLocalLLM: false
140
144
  });
141
145
  if (isAutocompleteDebugEnabled()) {
142
146
  // eslint-disable-next-line no-console
@@ -156,7 +160,9 @@ export const createSlowLaneClient = config => {
156
160
  debounceTimer = setTimeout(() => {
157
161
  debounceTimer = null;
158
162
  if (inflightRequestId !== null) {
159
- abortExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, inflightRequestId, 'superseded');
163
+ abortExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, inflightRequestId, 'superseded', {
164
+ isLocalLLM: false
165
+ });
160
166
  }
161
167
  const requestId = String(++requestSeq);
162
168
  inflightRequestId = requestId;