@atlaskit/editor-plugin-autocomplete 4.0.3 → 4.0.5
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 +23 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +30 -9
- package/dist/cjs/pm-plugins/local-slow-lane-client.js +11 -11
- package/dist/cjs/pm-plugins/slow-lane-client.js +3 -2
- package/dist/cjs/pm-plugins/text-predictor.js +13 -11
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +76 -53
- package/dist/es2019/pm-plugins/local-slow-lane-client.js +11 -9
- package/dist/es2019/pm-plugins/slow-lane-client.js +3 -3
- package/dist/es2019/pm-plugins/text-predictor.js +13 -11
- package/dist/esm/pm-plugins/autocomplete-plugin.js +30 -9
- package/dist/esm/pm-plugins/local-slow-lane-client.js +11 -11
- package/dist/esm/pm-plugins/slow-lane-client.js +3 -2
- package/dist/esm/pm-plugins/text-predictor.js +13 -11
- package/dist/types/index.d.ts +1 -0
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +6 -0
- package/package.json +2 -2
- package/src/index.ts +4 -0
- package/src/pm-plugins/autocomplete-plugin.ts +90 -60
- package/src/pm-plugins/local-slow-lane-client.ts +9 -7
- package/src/pm-plugins/slow-lane-client.ts +3 -2
- package/src/pm-plugins/text-predictor.ts +13 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-autocomplete
|
|
2
2
|
|
|
3
|
+
## 4.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`73743eb8b36e6`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/73743eb8b36e6) -
|
|
8
|
+
CLeanup prefer static regex violations
|
|
9
|
+
- Updated dependencies
|
|
10
|
+
|
|
11
|
+
## 4.0.4
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [`ac12ed0b41ef4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ac12ed0b41ef4) -
|
|
16
|
+
Refactor contextual autocomplete wiring so the conversation-store-aware ChatInput owns the
|
|
17
|
+
autocomplete context bridge (getContext + subscribeToContextUpdates) via a new
|
|
18
|
+
useAutocompleteEditorContext hook, and the shared RovoChatPromptInput simply forwards a single
|
|
19
|
+
`autocomplete` prop to the editor preset. Removes the duplicated keying / listener-set plumbing
|
|
20
|
+
from the reusable input.
|
|
21
|
+
|
|
22
|
+
Adds an optional `subscribeToContextUpdates` option to the autocomplete plugin so host surfaces
|
|
23
|
+
that stream context after mount (e.g. Rovo chat messages) can push refreshes, complementing the
|
|
24
|
+
existing word-boundary retry that only covers a one-time, still-loading comment thread.
|
|
25
|
+
|
|
3
26
|
## 4.0.3
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
|
@@ -38,6 +38,11 @@ var MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
38
38
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
39
39
|
// on every word boundary for the plugin's lifetime.
|
|
40
40
|
var MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
43
|
+
var TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
44
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
45
|
+
var WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
|
|
41
46
|
var hasDestroy = function hasDestroy(client) {
|
|
42
47
|
return 'destroy' in client && typeof client.destroy === 'function';
|
|
43
48
|
};
|
|
@@ -225,9 +230,7 @@ var getTypedLengthForPrediction = function getTypedLengthForPrediction(textBefor
|
|
|
225
230
|
if ((0, _slowLaneClient.isWordBoundary)(textBefore)) {
|
|
226
231
|
return 0;
|
|
227
232
|
}
|
|
228
|
-
|
|
229
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
230
|
-
var trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(/([^\s.,;:!?]*)$/)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
|
|
233
|
+
var 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$ : '';
|
|
231
234
|
return trailingToken.length;
|
|
232
235
|
};
|
|
233
236
|
var isEnglishLocale = function isEnglishLocale(locale) {
|
|
@@ -249,6 +252,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
249
252
|
* user has already typed several words before the promise resolved.
|
|
250
253
|
*/
|
|
251
254
|
var currentView = null;
|
|
255
|
+
var unsubscribeFromContextUpdates;
|
|
252
256
|
/**
|
|
253
257
|
* Set after accepting a suggestion so the next doc-change update
|
|
254
258
|
* skips scheduling a new prediction for the just-inserted text.
|
|
@@ -538,16 +542,14 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
538
542
|
return;
|
|
539
543
|
}
|
|
540
544
|
var lastChar = newText[newText.length - 1];
|
|
541
|
-
|
|
542
|
-
if (!/[\s.,;:!?]/.test(lastChar)) {
|
|
545
|
+
if (!WORD_BOUNDARY_CHARS_REGEX.test(lastChar)) {
|
|
543
546
|
return;
|
|
544
547
|
}
|
|
545
548
|
|
|
546
549
|
// Only fire if the previous state did not already end on a boundary,
|
|
547
550
|
// so we don't double-count when multiple boundary chars are inserted.
|
|
548
551
|
var prevLastChar = prevText[prevText.length - 1];
|
|
549
|
-
|
|
550
|
-
if (prevLastChar && /[\s.,;:!?]/.test(prevLastChar)) {
|
|
552
|
+
if (prevLastChar && WORD_BOUNDARY_CHARS_REGEX.test(prevLastChar)) {
|
|
551
553
|
return;
|
|
552
554
|
}
|
|
553
555
|
var beforeBoundary = newText.slice(0, -1).trimEnd();
|
|
@@ -670,7 +672,21 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
670
672
|
}
|
|
671
673
|
}
|
|
672
674
|
},
|
|
673
|
-
view: function view() {
|
|
675
|
+
view: function view(editorView) {
|
|
676
|
+
// Capture up front so a subscription notification before the first PM
|
|
677
|
+
// transaction can still drive slowLaneClient.updateContext (gated on currentView).
|
|
678
|
+
currentView = editorView;
|
|
679
|
+
|
|
680
|
+
// Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
|
|
681
|
+
if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
|
|
682
|
+
unsubscribeFromContextUpdates = options.subscribeToContextUpdates(function () {
|
|
683
|
+
// Bypass throttle for freshness; the in-flight guard prevents overlap.
|
|
684
|
+
refreshContext({
|
|
685
|
+
source: 'subscription',
|
|
686
|
+
allowThrottle: false
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
}
|
|
674
690
|
return {
|
|
675
691
|
update: function update(view, prevState) {
|
|
676
692
|
if (!isAutocompleteEnabled) {
|
|
@@ -699,7 +715,9 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
699
715
|
// thread still loading). Retry on word boundaries until we have
|
|
700
716
|
// the parent comment, throttled so we don't refetch constantly
|
|
701
717
|
// and capped so non-comment editors stop retrying entirely.
|
|
702
|
-
|
|
718
|
+
// Skipped for push-channel hosts (subscribeToContextUpdates): they
|
|
719
|
+
// never grow parentCommentContent, so polling only burns the budget.
|
|
720
|
+
if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
|
|
703
721
|
// Only count the attempt when a fetch actually started, so an
|
|
704
722
|
// in-flight or throttled no-op doesn't burn the retry budget.
|
|
705
723
|
if (refreshContext({
|
|
@@ -713,8 +731,11 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
713
731
|
}
|
|
714
732
|
},
|
|
715
733
|
destroy: function destroy() {
|
|
734
|
+
var _unsubscribeFromConte;
|
|
716
735
|
destroyed = true;
|
|
717
736
|
currentView = null;
|
|
737
|
+
(_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 || _unsubscribeFromConte();
|
|
738
|
+
unsubscribeFromContextUpdates = undefined;
|
|
718
739
|
if (debounceTimer) {
|
|
719
740
|
clearTimeout(debounceTimer);
|
|
720
741
|
}
|
|
@@ -78,6 +78,8 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
78
78
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
79
79
|
|
|
80
80
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
81
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
82
|
+
var OOM_REGEX = /\boom\b/;
|
|
81
83
|
var LOCAL_MLC_CAUSAL_MODEL_ID = exports.LOCAL_MLC_CAUSAL_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
82
84
|
|
|
83
85
|
/**
|
|
@@ -264,7 +266,7 @@ var bePayloadDataPromise;
|
|
|
264
266
|
* :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
|
|
265
267
|
* :returns: The parsed JSON value, or `null` if neither interop mode applies.
|
|
266
268
|
*/
|
|
267
|
-
|
|
269
|
+
function unwrapJsonModule(mod, shape) {
|
|
268
270
|
if (mod == null || (0, _typeof2.default)(mod) !== 'object') {
|
|
269
271
|
return null;
|
|
270
272
|
}
|
|
@@ -316,7 +318,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
|
|
|
316
318
|
return namespace.default;
|
|
317
319
|
}
|
|
318
320
|
return null;
|
|
319
|
-
}
|
|
321
|
+
}
|
|
320
322
|
|
|
321
323
|
/**
|
|
322
324
|
* Lazily load and build the BE-parity lookup tables from their JSON payloads.
|
|
@@ -766,9 +768,7 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
|
|
|
766
768
|
if (lower.includes('loading chunk') || lower.includes('dynamically imported module') || lower.includes('dynamic import')) {
|
|
767
769
|
return 'module_load_failed';
|
|
768
770
|
}
|
|
769
|
-
if (lower.includes('out of memory') ||
|
|
770
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
771
|
-
/\boom\b/.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
771
|
+
if (lower.includes('out of memory') || OOM_REGEX.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
772
772
|
return 'insufficient_memory';
|
|
773
773
|
}
|
|
774
774
|
// Pre-flight already returns missing_shader_f16 when the feature is absent,
|
|
@@ -964,6 +964,12 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
|
|
|
964
964
|
return _regenerator.default.wrap(function (_context4) {
|
|
965
965
|
while (1) switch (_context4.prev = _context4.next) {
|
|
966
966
|
case 0:
|
|
967
|
+
captureCompletionTime = function _captureCompletionTim(promise, onResolved) {
|
|
968
|
+
return promise.then(function (value) {
|
|
969
|
+
onResolved(performance.now());
|
|
970
|
+
return value;
|
|
971
|
+
});
|
|
972
|
+
};
|
|
967
973
|
if (!(!engine || destroyed)) {
|
|
968
974
|
_context4.next = 1;
|
|
969
975
|
break;
|
|
@@ -983,12 +989,6 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
|
|
|
983
989
|
lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
|
|
984
990
|
semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
|
|
985
991
|
arcticInput = wrapForArctic(semanticText);
|
|
986
|
-
captureCompletionTime = function captureCompletionTime(promise, onResolved) {
|
|
987
|
-
return promise.then(function (value) {
|
|
988
|
-
onResolved(performance.now());
|
|
989
|
-
return value;
|
|
990
|
-
});
|
|
991
|
-
};
|
|
992
992
|
if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
|
|
993
993
|
// eslint-disable-next-line no-console
|
|
994
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;');
|
|
@@ -29,6 +29,8 @@ var _debugMode = require("./debug-mode");
|
|
|
29
29
|
|
|
30
30
|
// eslint-disable-next-line require-unicode-regexp
|
|
31
31
|
var WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
|
|
32
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
33
|
+
var TRAILING_SLASH_REGEX = /\/$/;
|
|
32
34
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -80,8 +82,7 @@ var createSlowLaneClient = exports.createSlowLaneClient = function createSlowLan
|
|
|
80
82
|
}
|
|
81
83
|
return _context.abrupt("return");
|
|
82
84
|
case 1:
|
|
83
|
-
|
|
84
|
-
url = "".concat(baseUrl.replace(/\/$/, '')).concat(endpoint);
|
|
85
|
+
url = "".concat(baseUrl.replace(TRAILING_SLASH_REGEX, '')).concat(endpoint);
|
|
85
86
|
payload = {
|
|
86
87
|
text: text,
|
|
87
88
|
session_id: sessionId
|
|
@@ -42,6 +42,12 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
42
42
|
|
|
43
43
|
// eslint-disable-next-line require-unicode-regexp
|
|
44
44
|
var PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
|
|
45
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
46
|
+
var WHITESPACE_SPLIT_REGEX = /\s+/;
|
|
47
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
48
|
+
var SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
|
|
49
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
50
|
+
var TRAILING_WHITESPACE_REGEX = /\s$/;
|
|
45
51
|
var MIN_PREFIX_LENGTH = 3;
|
|
46
52
|
var MAX_CANDIDATES = 200;
|
|
47
53
|
var CONTEXT_WORDS = 10;
|
|
@@ -312,8 +318,8 @@ var getContextVectorForScoring = function getContextVectorForScoring(textBefore)
|
|
|
312
318
|
};
|
|
313
319
|
var tokenize = function tokenize(text) {
|
|
314
320
|
var tokens = [];
|
|
315
|
-
// eslint-disable-next-line
|
|
316
|
-
var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(
|
|
321
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
322
|
+
var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)),
|
|
317
323
|
_step7;
|
|
318
324
|
try {
|
|
319
325
|
for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
|
|
@@ -333,12 +339,9 @@ var tokenize = function tokenize(text) {
|
|
|
333
339
|
};
|
|
334
340
|
var extractPreviousWord = function extractPreviousWord(text) {
|
|
335
341
|
// Only consider the current sentence/line the user is typing in.
|
|
336
|
-
|
|
337
|
-
var sentences = text.split(/[\n.?!]+/);
|
|
342
|
+
var sentences = text.split(SENTENCE_BOUNDARY_REGEX);
|
|
338
343
|
var currentSentence = sentences[sentences.length - 1];
|
|
339
|
-
|
|
340
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
341
|
-
var words = currentSentence.trimEnd().split(/\s+/);
|
|
344
|
+
var words = currentSentence.trimEnd().split(WHITESPACE_SPLIT_REGEX);
|
|
342
345
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
343
346
|
};
|
|
344
347
|
|
|
@@ -477,8 +480,7 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
477
480
|
// }
|
|
478
481
|
|
|
479
482
|
// ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
|
|
480
|
-
|
|
481
|
-
if (textBefore.length > 0 && /\s$/.test(textBefore)) {
|
|
483
|
+
if (textBefore.length > 0 && TRAILING_WHITESPACE_REGEX.test(textBefore)) {
|
|
482
484
|
return null;
|
|
483
485
|
}
|
|
484
486
|
var trimmed = textBefore.trimEnd();
|
|
@@ -694,7 +696,7 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
694
696
|
* array and a sparse numeric-keyed object are emitted identically as named
|
|
695
697
|
* exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
|
|
696
698
|
*/
|
|
697
|
-
|
|
699
|
+
function unwrapJsonModule(mod, shape) {
|
|
698
700
|
if (mod == null || (0, _typeof2.default)(mod) !== 'object') {
|
|
699
701
|
return null;
|
|
700
702
|
}
|
|
@@ -730,7 +732,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
|
|
|
730
732
|
return namespace.default;
|
|
731
733
|
}
|
|
732
734
|
return null;
|
|
733
|
-
}
|
|
735
|
+
}
|
|
734
736
|
var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
|
|
735
737
|
var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
|
|
736
738
|
var _options$isLocalLLM;
|
|
@@ -24,6 +24,11 @@ const MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
24
24
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
25
25
|
// on every word boundary for the plugin's lifetime.
|
|
26
26
|
const MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
29
|
+
const TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
30
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
31
|
+
const WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
|
|
27
32
|
const hasDestroy = client => 'destroy' in client && typeof client.destroy === 'function';
|
|
28
33
|
const createInitialState = () => ({
|
|
29
34
|
ghostText: '',
|
|
@@ -213,9 +218,7 @@ const getTypedLengthForPrediction = textBefore => {
|
|
|
213
218
|
if (isWordBoundary(textBefore)) {
|
|
214
219
|
return 0;
|
|
215
220
|
}
|
|
216
|
-
|
|
217
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
218
|
-
const trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(/([^\s.,;:!?]*)$/)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
|
|
221
|
+
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$ : '';
|
|
219
222
|
return trailingToken.length;
|
|
220
223
|
};
|
|
221
224
|
const isEnglishLocale = locale => {
|
|
@@ -237,6 +240,7 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
237
240
|
* user has already typed several words before the promise resolved.
|
|
238
241
|
*/
|
|
239
242
|
let currentView = null;
|
|
243
|
+
let unsubscribeFromContextUpdates;
|
|
240
244
|
/**
|
|
241
245
|
* Set after accepting a suggestion so the next doc-change update
|
|
242
246
|
* skips scheduling a new prediction for the just-inserted text.
|
|
@@ -522,16 +526,14 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
522
526
|
return;
|
|
523
527
|
}
|
|
524
528
|
const lastChar = newText[newText.length - 1];
|
|
525
|
-
|
|
526
|
-
if (!/[\s.,;:!?]/.test(lastChar)) {
|
|
529
|
+
if (!WORD_BOUNDARY_CHARS_REGEX.test(lastChar)) {
|
|
527
530
|
return;
|
|
528
531
|
}
|
|
529
532
|
|
|
530
533
|
// Only fire if the previous state did not already end on a boundary,
|
|
531
534
|
// so we don't double-count when multiple boundary chars are inserted.
|
|
532
535
|
const prevLastChar = prevText[prevText.length - 1];
|
|
533
|
-
|
|
534
|
-
if (prevLastChar && /[\s.,;:!?]/.test(prevLastChar)) {
|
|
536
|
+
if (prevLastChar && WORD_BOUNDARY_CHARS_REGEX.test(prevLastChar)) {
|
|
535
537
|
return;
|
|
536
538
|
}
|
|
537
539
|
const beforeBoundary = newText.slice(0, -1).trimEnd();
|
|
@@ -657,59 +659,80 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
657
659
|
}
|
|
658
660
|
}
|
|
659
661
|
},
|
|
660
|
-
view:
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
currentView = view;
|
|
666
|
-
if (!prevState.doc.eq(view.state.doc)) {
|
|
667
|
-
if (justAccepted) {
|
|
668
|
-
justAccepted = false;
|
|
662
|
+
view: editorView => {
|
|
663
|
+
// Capture up front so a subscription notification before the first PM
|
|
664
|
+
// transaction can still drive slowLaneClient.updateContext (gated on currentView).
|
|
665
|
+
currentView = editorView;
|
|
669
666
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
667
|
+
// Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
|
|
668
|
+
if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
|
|
669
|
+
unsubscribeFromContextUpdates = options.subscribeToContextUpdates(() => {
|
|
670
|
+
// Bypass throttle for freshness; the in-flight guard prevents overlap.
|
|
671
|
+
refreshContext({
|
|
672
|
+
source: 'subscription',
|
|
673
|
+
allowThrottle: false
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
update: (view, prevState) => {
|
|
679
|
+
if (!isAutocompleteEnabled) {
|
|
676
680
|
return;
|
|
677
681
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
682
|
+
currentView = view;
|
|
683
|
+
if (!prevState.doc.eq(view.state.doc)) {
|
|
684
|
+
if (justAccepted) {
|
|
685
|
+
justAccepted = false;
|
|
686
|
+
|
|
687
|
+
// Snapshot the post-acceptance text so follow-up transactions hit
|
|
688
|
+
// the dismissedContext guard and abort until the user types again.
|
|
689
|
+
dismissedContext = getTextBeforeCursor(view.state);
|
|
690
|
+
if (debounceTimer) {
|
|
691
|
+
clearTimeout(debounceTimer);
|
|
692
|
+
}
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
maybeUpdateSessionFrequency(view, prevState);
|
|
696
|
+
const textBefore = getTextBeforeCursor(view.state);
|
|
697
|
+
if (isWordBoundary(textBefore)) {
|
|
698
|
+
var _resolvedContext;
|
|
699
|
+
slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
|
|
683
700
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
701
|
+
// Context may not have resolved on first focus (e.g. comment
|
|
702
|
+
// thread still loading). Retry on word boundaries until we have
|
|
703
|
+
// the parent comment, throttled so we don't refetch constantly
|
|
704
|
+
// and capped so non-comment editors stop retrying entirely.
|
|
705
|
+
// Skipped for push-channel hosts (subscribeToContextUpdates): they
|
|
706
|
+
// never grow parentCommentContent, so polling only burns the budget.
|
|
707
|
+
if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
|
|
708
|
+
// Only count the attempt when a fetch actually started, so an
|
|
709
|
+
// in-flight or throttled no-op doesn't burn the retry budget.
|
|
710
|
+
if (refreshContext({
|
|
711
|
+
source: 'word-boundary'
|
|
712
|
+
})) {
|
|
713
|
+
wordBoundaryRefreshAttempts++;
|
|
714
|
+
}
|
|
695
715
|
}
|
|
696
716
|
}
|
|
717
|
+
schedulePrediction(view);
|
|
697
718
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
slowLaneClient
|
|
719
|
+
},
|
|
720
|
+
destroy: () => {
|
|
721
|
+
var _unsubscribeFromConte;
|
|
722
|
+
destroyed = true;
|
|
723
|
+
currentView = null;
|
|
724
|
+
(_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 ? void 0 : _unsubscribeFromConte();
|
|
725
|
+
unsubscribeFromContextUpdates = undefined;
|
|
726
|
+
if (debounceTimer) {
|
|
727
|
+
clearTimeout(debounceTimer);
|
|
728
|
+
}
|
|
729
|
+
if (hasDestroy(slowLaneClient)) {
|
|
730
|
+
slowLaneClient.destroy();
|
|
731
|
+
}
|
|
732
|
+
ingestedContextTexts.clear();
|
|
733
|
+
setDefaultSlowLaneClient(null);
|
|
709
734
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
})
|
|
735
|
+
};
|
|
736
|
+
}
|
|
714
737
|
});
|
|
715
738
|
};
|
|
@@ -60,6 +60,8 @@ import { isWordBoundary } from './slow-lane-client';
|
|
|
60
60
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
61
61
|
|
|
62
62
|
const DEFAULT_DEBOUNCE_MS = 300;
|
|
63
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
64
|
+
const OOM_REGEX = /\boom\b/;
|
|
63
65
|
export const LOCAL_MLC_CAUSAL_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -247,7 +249,7 @@ let bePayloadDataPromise;
|
|
|
247
249
|
* :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
|
|
248
250
|
* :returns: The parsed JSON value, or `null` if neither interop mode applies.
|
|
249
251
|
*/
|
|
250
|
-
|
|
252
|
+
function unwrapJsonModule(mod, shape) {
|
|
251
253
|
if (mod == null || typeof mod !== 'object') {
|
|
252
254
|
return null;
|
|
253
255
|
}
|
|
@@ -288,7 +290,7 @@ const unwrapJsonModule = (mod, shape) => {
|
|
|
288
290
|
return namespace.default;
|
|
289
291
|
}
|
|
290
292
|
return null;
|
|
291
|
-
}
|
|
293
|
+
}
|
|
292
294
|
|
|
293
295
|
/**
|
|
294
296
|
* Lazily load and build the BE-parity lookup tables from their JSON payloads.
|
|
@@ -587,9 +589,7 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
587
589
|
if (lower.includes('loading chunk') || lower.includes('dynamically imported module') || lower.includes('dynamic import')) {
|
|
588
590
|
return 'module_load_failed';
|
|
589
591
|
}
|
|
590
|
-
if (lower.includes('out of memory') ||
|
|
591
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
592
|
-
/\boom\b/.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
592
|
+
if (lower.includes('out of memory') || OOM_REGEX.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
593
593
|
return 'insufficient_memory';
|
|
594
594
|
}
|
|
595
595
|
// Pre-flight already returns missing_shader_f16 when the feature is absent,
|
|
@@ -764,10 +764,12 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
764
764
|
const lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
|
|
765
765
|
const semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
|
|
766
766
|
const arcticInput = wrapForArctic(semanticText);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
767
|
+
function captureCompletionTime(promise, onResolved) {
|
|
768
|
+
return promise.then(value => {
|
|
769
|
+
onResolved(performance.now());
|
|
770
|
+
return value;
|
|
771
|
+
});
|
|
772
|
+
}
|
|
771
773
|
if (isAutocompleteDebugEnabled()) {
|
|
772
774
|
// eslint-disable-next-line no-console
|
|
773
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;');
|
|
@@ -20,6 +20,8 @@ import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
|
20
20
|
|
|
21
21
|
// eslint-disable-next-line require-unicode-regexp
|
|
22
22
|
const WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
|
|
23
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
24
|
+
const TRAILING_SLASH_REGEX = /\/$/;
|
|
23
25
|
const DEFAULT_DEBOUNCE_MS = 300;
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -61,9 +63,7 @@ export const createSlowLaneClient = config => {
|
|
|
61
63
|
if (!text || text.trim().length === 0) {
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
66
|
-
const url = `${baseUrl.replace(/\/$/, '')}${endpoint}`;
|
|
66
|
+
const url = `${baseUrl.replace(TRAILING_SLASH_REGEX, '')}${endpoint}`;
|
|
67
67
|
const payload = {
|
|
68
68
|
text,
|
|
69
69
|
session_id: sessionId
|
|
@@ -29,6 +29,12 @@ import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
|
29
29
|
|
|
30
30
|
// eslint-disable-next-line require-unicode-regexp
|
|
31
31
|
const PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
|
|
32
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
33
|
+
const WHITESPACE_SPLIT_REGEX = /\s+/;
|
|
34
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
35
|
+
const SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
|
|
36
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
37
|
+
const TRAILING_WHITESPACE_REGEX = /\s$/;
|
|
32
38
|
const MIN_PREFIX_LENGTH = 3;
|
|
33
39
|
const MAX_CANDIDATES = 200;
|
|
34
40
|
const CONTEXT_WORDS = 10;
|
|
@@ -235,8 +241,8 @@ const getContextVectorForScoring = textBefore => {
|
|
|
235
241
|
};
|
|
236
242
|
const tokenize = text => {
|
|
237
243
|
const tokens = [];
|
|
238
|
-
// eslint-disable-next-line
|
|
239
|
-
for (const raw of text.toLowerCase().split(
|
|
244
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
245
|
+
for (const raw of text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)) {
|
|
240
246
|
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
241
247
|
const clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
|
|
242
248
|
if (clean.length >= 2) {
|
|
@@ -247,12 +253,9 @@ const tokenize = text => {
|
|
|
247
253
|
};
|
|
248
254
|
const extractPreviousWord = text => {
|
|
249
255
|
// Only consider the current sentence/line the user is typing in.
|
|
250
|
-
|
|
251
|
-
const sentences = text.split(/[\n.?!]+/);
|
|
256
|
+
const sentences = text.split(SENTENCE_BOUNDARY_REGEX);
|
|
252
257
|
const currentSentence = sentences[sentences.length - 1];
|
|
253
|
-
|
|
254
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
255
|
-
const words = currentSentence.trimEnd().split(/\s+/);
|
|
258
|
+
const words = currentSentence.trimEnd().split(WHITESPACE_SPLIT_REGEX);
|
|
256
259
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
257
260
|
};
|
|
258
261
|
|
|
@@ -373,8 +376,7 @@ export const predict = textBefore => {
|
|
|
373
376
|
// }
|
|
374
377
|
|
|
375
378
|
// ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
|
|
376
|
-
|
|
377
|
-
if (textBefore.length > 0 && /\s$/.test(textBefore)) {
|
|
379
|
+
if (textBefore.length > 0 && TRAILING_WHITESPACE_REGEX.test(textBefore)) {
|
|
378
380
|
return null;
|
|
379
381
|
}
|
|
380
382
|
const trimmed = textBefore.trimEnd();
|
|
@@ -560,7 +562,7 @@ export const predict = textBefore => {
|
|
|
560
562
|
* array and a sparse numeric-keyed object are emitted identically as named
|
|
561
563
|
* exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
|
|
562
564
|
*/
|
|
563
|
-
|
|
565
|
+
function unwrapJsonModule(mod, shape) {
|
|
564
566
|
if (mod == null || typeof mod !== 'object') {
|
|
565
567
|
return null;
|
|
566
568
|
}
|
|
@@ -585,7 +587,7 @@ const unwrapJsonModule = (mod, shape) => {
|
|
|
585
587
|
return namespace.default;
|
|
586
588
|
}
|
|
587
589
|
return null;
|
|
588
|
-
}
|
|
590
|
+
}
|
|
589
591
|
export const loadVectorsAsync = async options => {
|
|
590
592
|
var _options$isLocalLLM;
|
|
591
593
|
if (vectorStore || vectorsLoadStarted) {
|
|
@@ -31,6 +31,11 @@ var MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
31
31
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
32
32
|
// on every word boundary for the plugin's lifetime.
|
|
33
33
|
var MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
36
|
+
var TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
37
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
38
|
+
var WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
|
|
34
39
|
var hasDestroy = function hasDestroy(client) {
|
|
35
40
|
return 'destroy' in client && typeof client.destroy === 'function';
|
|
36
41
|
};
|
|
@@ -218,9 +223,7 @@ var getTypedLengthForPrediction = function getTypedLengthForPrediction(textBefor
|
|
|
218
223
|
if (isWordBoundary(textBefore)) {
|
|
219
224
|
return 0;
|
|
220
225
|
}
|
|
221
|
-
|
|
222
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
223
|
-
var trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(/([^\s.,;:!?]*)$/)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
|
|
226
|
+
var 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$ : '';
|
|
224
227
|
return trailingToken.length;
|
|
225
228
|
};
|
|
226
229
|
var isEnglishLocale = function isEnglishLocale(locale) {
|
|
@@ -242,6 +245,7 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
242
245
|
* user has already typed several words before the promise resolved.
|
|
243
246
|
*/
|
|
244
247
|
var currentView = null;
|
|
248
|
+
var unsubscribeFromContextUpdates;
|
|
245
249
|
/**
|
|
246
250
|
* Set after accepting a suggestion so the next doc-change update
|
|
247
251
|
* skips scheduling a new prediction for the just-inserted text.
|
|
@@ -531,16 +535,14 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
531
535
|
return;
|
|
532
536
|
}
|
|
533
537
|
var lastChar = newText[newText.length - 1];
|
|
534
|
-
|
|
535
|
-
if (!/[\s.,;:!?]/.test(lastChar)) {
|
|
538
|
+
if (!WORD_BOUNDARY_CHARS_REGEX.test(lastChar)) {
|
|
536
539
|
return;
|
|
537
540
|
}
|
|
538
541
|
|
|
539
542
|
// Only fire if the previous state did not already end on a boundary,
|
|
540
543
|
// so we don't double-count when multiple boundary chars are inserted.
|
|
541
544
|
var prevLastChar = prevText[prevText.length - 1];
|
|
542
|
-
|
|
543
|
-
if (prevLastChar && /[\s.,;:!?]/.test(prevLastChar)) {
|
|
545
|
+
if (prevLastChar && WORD_BOUNDARY_CHARS_REGEX.test(prevLastChar)) {
|
|
544
546
|
return;
|
|
545
547
|
}
|
|
546
548
|
var beforeBoundary = newText.slice(0, -1).trimEnd();
|
|
@@ -663,7 +665,21 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
663
665
|
}
|
|
664
666
|
}
|
|
665
667
|
},
|
|
666
|
-
view: function view() {
|
|
668
|
+
view: function view(editorView) {
|
|
669
|
+
// Capture up front so a subscription notification before the first PM
|
|
670
|
+
// transaction can still drive slowLaneClient.updateContext (gated on currentView).
|
|
671
|
+
currentView = editorView;
|
|
672
|
+
|
|
673
|
+
// Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
|
|
674
|
+
if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
|
|
675
|
+
unsubscribeFromContextUpdates = options.subscribeToContextUpdates(function () {
|
|
676
|
+
// Bypass throttle for freshness; the in-flight guard prevents overlap.
|
|
677
|
+
refreshContext({
|
|
678
|
+
source: 'subscription',
|
|
679
|
+
allowThrottle: false
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
}
|
|
667
683
|
return {
|
|
668
684
|
update: function update(view, prevState) {
|
|
669
685
|
if (!isAutocompleteEnabled) {
|
|
@@ -692,7 +708,9 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
692
708
|
// thread still loading). Retry on word boundaries until we have
|
|
693
709
|
// the parent comment, throttled so we don't refetch constantly
|
|
694
710
|
// and capped so non-comment editors stop retrying entirely.
|
|
695
|
-
|
|
711
|
+
// Skipped for push-channel hosts (subscribeToContextUpdates): they
|
|
712
|
+
// never grow parentCommentContent, so polling only burns the budget.
|
|
713
|
+
if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
|
|
696
714
|
// Only count the attempt when a fetch actually started, so an
|
|
697
715
|
// in-flight or throttled no-op doesn't burn the retry budget.
|
|
698
716
|
if (refreshContext({
|
|
@@ -706,8 +724,11 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
706
724
|
}
|
|
707
725
|
},
|
|
708
726
|
destroy: function destroy() {
|
|
727
|
+
var _unsubscribeFromConte;
|
|
709
728
|
destroyed = true;
|
|
710
729
|
currentView = null;
|
|
730
|
+
(_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 || _unsubscribeFromConte();
|
|
731
|
+
unsubscribeFromContextUpdates = undefined;
|
|
711
732
|
if (debounceTimer) {
|
|
712
733
|
clearTimeout(debounceTimer);
|
|
713
734
|
}
|
|
@@ -72,6 +72,8 @@ import { isWordBoundary } from './slow-lane-client';
|
|
|
72
72
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
73
73
|
|
|
74
74
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
75
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
76
|
+
var OOM_REGEX = /\boom\b/;
|
|
75
77
|
export var LOCAL_MLC_CAUSAL_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
76
78
|
|
|
77
79
|
/**
|
|
@@ -258,7 +260,7 @@ var bePayloadDataPromise;
|
|
|
258
260
|
* :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
|
|
259
261
|
* :returns: The parsed JSON value, or `null` if neither interop mode applies.
|
|
260
262
|
*/
|
|
261
|
-
|
|
263
|
+
function unwrapJsonModule(mod, shape) {
|
|
262
264
|
if (mod == null || _typeof(mod) !== 'object') {
|
|
263
265
|
return null;
|
|
264
266
|
}
|
|
@@ -310,7 +312,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
|
|
|
310
312
|
return namespace.default;
|
|
311
313
|
}
|
|
312
314
|
return null;
|
|
313
|
-
}
|
|
315
|
+
}
|
|
314
316
|
|
|
315
317
|
/**
|
|
316
318
|
* Lazily load and build the BE-parity lookup tables from their JSON payloads.
|
|
@@ -756,9 +758,7 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
|
|
|
756
758
|
if (lower.includes('loading chunk') || lower.includes('dynamically imported module') || lower.includes('dynamic import')) {
|
|
757
759
|
return 'module_load_failed';
|
|
758
760
|
}
|
|
759
|
-
if (lower.includes('out of memory') ||
|
|
760
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
761
|
-
/\boom\b/.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
761
|
+
if (lower.includes('out of memory') || OOM_REGEX.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
762
762
|
return 'insufficient_memory';
|
|
763
763
|
}
|
|
764
764
|
// Pre-flight already returns missing_shader_f16 when the feature is absent,
|
|
@@ -952,6 +952,12 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
|
|
|
952
952
|
return _regeneratorRuntime.wrap(function (_context4) {
|
|
953
953
|
while (1) switch (_context4.prev = _context4.next) {
|
|
954
954
|
case 0:
|
|
955
|
+
captureCompletionTime = function _captureCompletionTim(promise, onResolved) {
|
|
956
|
+
return promise.then(function (value) {
|
|
957
|
+
onResolved(performance.now());
|
|
958
|
+
return value;
|
|
959
|
+
});
|
|
960
|
+
};
|
|
955
961
|
if (!(!engine || destroyed)) {
|
|
956
962
|
_context4.next = 1;
|
|
957
963
|
break;
|
|
@@ -971,12 +977,6 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
|
|
|
971
977
|
lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
|
|
972
978
|
semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
|
|
973
979
|
arcticInput = wrapForArctic(semanticText);
|
|
974
|
-
captureCompletionTime = function captureCompletionTime(promise, onResolved) {
|
|
975
|
-
return promise.then(function (value) {
|
|
976
|
-
onResolved(performance.now());
|
|
977
|
-
return value;
|
|
978
|
-
});
|
|
979
|
-
};
|
|
980
980
|
if (isAutocompleteDebugEnabled()) {
|
|
981
981
|
// eslint-disable-next-line no-console
|
|
982
982
|
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;');
|
|
@@ -23,6 +23,8 @@ import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
|
23
23
|
|
|
24
24
|
// eslint-disable-next-line require-unicode-regexp
|
|
25
25
|
var WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
|
|
26
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
27
|
+
var TRAILING_SLASH_REGEX = /\/$/;
|
|
26
28
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -74,8 +76,7 @@ export var createSlowLaneClient = function createSlowLaneClient(config) {
|
|
|
74
76
|
}
|
|
75
77
|
return _context.abrupt("return");
|
|
76
78
|
case 1:
|
|
77
|
-
|
|
78
|
-
url = "".concat(baseUrl.replace(/\/$/, '')).concat(endpoint);
|
|
79
|
+
url = "".concat(baseUrl.replace(TRAILING_SLASH_REGEX, '')).concat(endpoint);
|
|
79
80
|
payload = {
|
|
80
81
|
text: text,
|
|
81
82
|
session_id: sessionId
|
|
@@ -38,6 +38,12 @@ import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
|
38
38
|
|
|
39
39
|
// eslint-disable-next-line require-unicode-regexp
|
|
40
40
|
var PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
|
|
41
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
42
|
+
var WHITESPACE_SPLIT_REGEX = /\s+/;
|
|
43
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
44
|
+
var SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
|
|
45
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
46
|
+
var TRAILING_WHITESPACE_REGEX = /\s$/;
|
|
41
47
|
var MIN_PREFIX_LENGTH = 3;
|
|
42
48
|
var MAX_CANDIDATES = 200;
|
|
43
49
|
var CONTEXT_WORDS = 10;
|
|
@@ -308,8 +314,8 @@ var getContextVectorForScoring = function getContextVectorForScoring(textBefore)
|
|
|
308
314
|
};
|
|
309
315
|
var tokenize = function tokenize(text) {
|
|
310
316
|
var tokens = [];
|
|
311
|
-
// eslint-disable-next-line
|
|
312
|
-
var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(
|
|
317
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
318
|
+
var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)),
|
|
313
319
|
_step7;
|
|
314
320
|
try {
|
|
315
321
|
for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
|
|
@@ -329,12 +335,9 @@ var tokenize = function tokenize(text) {
|
|
|
329
335
|
};
|
|
330
336
|
var extractPreviousWord = function extractPreviousWord(text) {
|
|
331
337
|
// Only consider the current sentence/line the user is typing in.
|
|
332
|
-
|
|
333
|
-
var sentences = text.split(/[\n.?!]+/);
|
|
338
|
+
var sentences = text.split(SENTENCE_BOUNDARY_REGEX);
|
|
334
339
|
var currentSentence = sentences[sentences.length - 1];
|
|
335
|
-
|
|
336
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
337
|
-
var words = currentSentence.trimEnd().split(/\s+/);
|
|
340
|
+
var words = currentSentence.trimEnd().split(WHITESPACE_SPLIT_REGEX);
|
|
338
341
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
339
342
|
};
|
|
340
343
|
|
|
@@ -473,8 +476,7 @@ export var predict = function predict(textBefore) {
|
|
|
473
476
|
// }
|
|
474
477
|
|
|
475
478
|
// ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
|
|
476
|
-
|
|
477
|
-
if (textBefore.length > 0 && /\s$/.test(textBefore)) {
|
|
479
|
+
if (textBefore.length > 0 && TRAILING_WHITESPACE_REGEX.test(textBefore)) {
|
|
478
480
|
return null;
|
|
479
481
|
}
|
|
480
482
|
var trimmed = textBefore.trimEnd();
|
|
@@ -690,7 +692,7 @@ export var predict = function predict(textBefore) {
|
|
|
690
692
|
* array and a sparse numeric-keyed object are emitted identically as named
|
|
691
693
|
* exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
|
|
692
694
|
*/
|
|
693
|
-
|
|
695
|
+
function unwrapJsonModule(mod, shape) {
|
|
694
696
|
if (mod == null || _typeof(mod) !== 'object') {
|
|
695
697
|
return null;
|
|
696
698
|
}
|
|
@@ -726,7 +728,7 @@ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
|
|
|
726
728
|
return namespace.default;
|
|
727
729
|
}
|
|
728
730
|
return null;
|
|
729
|
-
}
|
|
731
|
+
}
|
|
730
732
|
export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
731
733
|
var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(options) {
|
|
732
734
|
var _options$isLocalLLM;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ export interface AutocompletePluginOptions {
|
|
|
40
40
|
* Use this to serve vectors from a CDN or media service in production.
|
|
41
41
|
*/
|
|
42
42
|
getVectorsBinaryUrl?: () => Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Subscription for hosts with live external context (e.g. Rovo chat). The plugin
|
|
45
|
+
* re-fetches via getContext() on each notification and unsubscribes on destroy.
|
|
46
|
+
* Only meaningful alongside `getContext`; without it each notification is a no-op.
|
|
47
|
+
*/
|
|
48
|
+
subscribeToContextUpdates?: (onContextUpdated: () => void) => () => void;
|
|
43
49
|
/**
|
|
44
50
|
* User locale used to determine whether autocomplete should run.
|
|
45
51
|
* Defaults to browser locale when omitted.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-autocomplete",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.5",
|
|
4
4
|
"description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"wink-nlp": "^2.4.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@atlaskit/editor-common": "^116.
|
|
30
|
+
"@atlaskit/editor-common": "^116.17.0",
|
|
31
31
|
"@atlaskit/editor-plugin-analytics": "^12.0.0",
|
|
32
32
|
"react": "^18.2.0"
|
|
33
33
|
},
|
package/src/index.ts
CHANGED
|
@@ -48,6 +48,11 @@ const MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
48
48
|
// on every word boundary for the plugin's lifetime.
|
|
49
49
|
const MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
50
50
|
|
|
51
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
52
|
+
const TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
53
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
54
|
+
const WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
|
|
55
|
+
|
|
51
56
|
const hasDestroy = (
|
|
52
57
|
client: ReturnType<typeof createSlowLaneClient> | LocalSlowLaneClient,
|
|
53
58
|
): client is LocalSlowLaneClient => 'destroy' in client && typeof client.destroy === 'function';
|
|
@@ -214,6 +219,12 @@ export interface AutocompletePluginOptions {
|
|
|
214
219
|
* Use this to serve vectors from a CDN or media service in production.
|
|
215
220
|
*/
|
|
216
221
|
getVectorsBinaryUrl?: () => Promise<string>;
|
|
222
|
+
/**
|
|
223
|
+
* Subscription for hosts with live external context (e.g. Rovo chat). The plugin
|
|
224
|
+
* re-fetches via getContext() on each notification and unsubscribes on destroy.
|
|
225
|
+
* Only meaningful alongside `getContext`; without it each notification is a no-op.
|
|
226
|
+
*/
|
|
227
|
+
subscribeToContextUpdates?: (onContextUpdated: () => void) => () => void;
|
|
217
228
|
/**
|
|
218
229
|
* User locale used to determine whether autocomplete should run.
|
|
219
230
|
* Defaults to browser locale when omitted.
|
|
@@ -299,8 +310,7 @@ const getTypedLengthForPrediction = (textBefore: string): number => {
|
|
|
299
310
|
return 0;
|
|
300
311
|
}
|
|
301
312
|
|
|
302
|
-
|
|
303
|
-
const trailingToken = textBefore.match(/([^\s.,;:!?]*)$/)?.[1] ?? '';
|
|
313
|
+
const trailingToken = textBefore.match(TRAILING_NON_BOUNDARY_TOKEN_REGEX)?.[1] ?? '';
|
|
304
314
|
return trailingToken.length;
|
|
305
315
|
};
|
|
306
316
|
|
|
@@ -329,6 +339,7 @@ export const createAutocompletePlugin = (
|
|
|
329
339
|
* user has already typed several words before the promise resolved.
|
|
330
340
|
*/
|
|
331
341
|
let currentView: EditorView | null = null;
|
|
342
|
+
let unsubscribeFromContextUpdates: (() => void) | undefined;
|
|
332
343
|
/**
|
|
333
344
|
* Set after accepting a suggestion so the next doc-change update
|
|
334
345
|
* skips scheduling a new prediction for the just-inserted text.
|
|
@@ -582,7 +593,9 @@ export const createAutocompletePlugin = (
|
|
|
582
593
|
// nodeAfter correctly handles inline atoms (mentions, emojis) where
|
|
583
594
|
// parentOffset and textContent indices diverge.
|
|
584
595
|
const nodeAfter = selection.$from.nodeAfter;
|
|
585
|
-
const charAfterCursor = nodeAfter?.isText
|
|
596
|
+
const charAfterCursor = nodeAfter?.isText
|
|
597
|
+
? getLeadingTextCharacter(nodeAfter.text)
|
|
598
|
+
: undefined;
|
|
586
599
|
// Only suppress for Unicode letters, digits, and underscore — hyphens,
|
|
587
600
|
// apostrophes, and similar punctuation are valid left-edge boundaries
|
|
588
601
|
// and should not block suggestions (e.g. cursor before '-' in compound-word).
|
|
@@ -638,16 +651,14 @@ export const createAutocompletePlugin = (
|
|
|
638
651
|
}
|
|
639
652
|
|
|
640
653
|
const lastChar = newText[newText.length - 1];
|
|
641
|
-
|
|
642
|
-
if (!/[\s.,;:!?]/.test(lastChar)) {
|
|
654
|
+
if (!WORD_BOUNDARY_CHARS_REGEX.test(lastChar)) {
|
|
643
655
|
return;
|
|
644
656
|
}
|
|
645
657
|
|
|
646
658
|
// Only fire if the previous state did not already end on a boundary,
|
|
647
659
|
// so we don't double-count when multiple boundary chars are inserted.
|
|
648
660
|
const prevLastChar = prevText[prevText.length - 1];
|
|
649
|
-
|
|
650
|
-
if (prevLastChar && /[\s.,;:!?]/.test(prevLastChar)) {
|
|
661
|
+
if (prevLastChar && WORD_BOUNDARY_CHARS_REGEX.test(prevLastChar)) {
|
|
651
662
|
return;
|
|
652
663
|
}
|
|
653
664
|
|
|
@@ -790,66 +801,85 @@ export const createAutocompletePlugin = (
|
|
|
790
801
|
},
|
|
791
802
|
},
|
|
792
803
|
|
|
793
|
-
view: () =>
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
currentView = view;
|
|
800
|
-
if (!prevState.doc.eq(view.state.doc)) {
|
|
801
|
-
if (justAccepted) {
|
|
802
|
-
justAccepted = false;
|
|
804
|
+
view: (editorView: EditorView) => {
|
|
805
|
+
// Capture up front so a subscription notification before the first PM
|
|
806
|
+
// transaction can still drive slowLaneClient.updateContext (gated on currentView).
|
|
807
|
+
currentView = editorView;
|
|
803
808
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
809
|
+
// Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
|
|
810
|
+
if (isAutocompleteEnabled && options?.subscribeToContextUpdates) {
|
|
811
|
+
unsubscribeFromContextUpdates = options.subscribeToContextUpdates(() => {
|
|
812
|
+
// Bypass throttle for freshness; the in-flight guard prevents overlap.
|
|
813
|
+
refreshContext({ source: 'subscription', allowThrottle: false });
|
|
814
|
+
});
|
|
815
|
+
}
|
|
807
816
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
817
|
+
return {
|
|
818
|
+
update: (view: EditorView, prevState: EditorState) => {
|
|
819
|
+
if (!isAutocompleteEnabled) {
|
|
811
820
|
return;
|
|
812
821
|
}
|
|
813
822
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
// and capped so non-comment editors stop retrying entirely.
|
|
826
|
-
if (
|
|
827
|
-
!resolvedContext?.parentCommentContent &&
|
|
828
|
-
wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS
|
|
829
|
-
) {
|
|
830
|
-
// Only count the attempt when a fetch actually started, so an
|
|
831
|
-
// in-flight or throttled no-op doesn't burn the retry budget.
|
|
832
|
-
if (refreshContext({ source: 'word-boundary' })) {
|
|
833
|
-
wordBoundaryRefreshAttempts++;
|
|
823
|
+
currentView = view;
|
|
824
|
+
if (!prevState.doc.eq(view.state.doc)) {
|
|
825
|
+
if (justAccepted) {
|
|
826
|
+
justAccepted = false;
|
|
827
|
+
|
|
828
|
+
// Snapshot the post-acceptance text so follow-up transactions hit
|
|
829
|
+
// the dismissedContext guard and abort until the user types again.
|
|
830
|
+
dismissedContext = getTextBeforeCursor(view.state);
|
|
831
|
+
|
|
832
|
+
if (debounceTimer) {
|
|
833
|
+
clearTimeout(debounceTimer);
|
|
834
834
|
}
|
|
835
|
+
return;
|
|
835
836
|
}
|
|
836
|
-
}
|
|
837
837
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
838
|
+
maybeUpdateSessionFrequency(view, prevState);
|
|
839
|
+
|
|
840
|
+
const textBefore = getTextBeforeCursor(view.state);
|
|
841
|
+
if (isWordBoundary(textBefore)) {
|
|
842
|
+
slowLaneClient.updateContext(
|
|
843
|
+
buildSlowLaneText(view.state.doc.textContent, resolvedContext),
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
// Context may not have resolved on first focus (e.g. comment
|
|
847
|
+
// thread still loading). Retry on word boundaries until we have
|
|
848
|
+
// the parent comment, throttled so we don't refetch constantly
|
|
849
|
+
// and capped so non-comment editors stop retrying entirely.
|
|
850
|
+
// Skipped for push-channel hosts (subscribeToContextUpdates): they
|
|
851
|
+
// never grow parentCommentContent, so polling only burns the budget.
|
|
852
|
+
if (
|
|
853
|
+
!options?.subscribeToContextUpdates &&
|
|
854
|
+
!resolvedContext?.parentCommentContent &&
|
|
855
|
+
wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS
|
|
856
|
+
) {
|
|
857
|
+
// Only count the attempt when a fetch actually started, so an
|
|
858
|
+
// in-flight or throttled no-op doesn't burn the retry budget.
|
|
859
|
+
if (refreshContext({ source: 'word-boundary' })) {
|
|
860
|
+
wordBoundaryRefreshAttempts++;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
schedulePrediction(view);
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
destroy: () => {
|
|
869
|
+
destroyed = true;
|
|
870
|
+
currentView = null;
|
|
871
|
+
unsubscribeFromContextUpdates?.();
|
|
872
|
+
unsubscribeFromContextUpdates = undefined;
|
|
873
|
+
if (debounceTimer) {
|
|
874
|
+
clearTimeout(debounceTimer);
|
|
875
|
+
}
|
|
876
|
+
if (hasDestroy(slowLaneClient)) {
|
|
877
|
+
slowLaneClient.destroy();
|
|
878
|
+
}
|
|
879
|
+
ingestedContextTexts.clear();
|
|
880
|
+
setDefaultSlowLaneClient(null);
|
|
881
|
+
},
|
|
882
|
+
};
|
|
883
|
+
},
|
|
854
884
|
});
|
|
855
885
|
};
|
|
@@ -174,6 +174,8 @@ interface MinimalGpu {
|
|
|
174
174
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
175
175
|
|
|
176
176
|
const DEFAULT_DEBOUNCE_MS = 300;
|
|
177
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
178
|
+
const OOM_REGEX = /\boom\b/;
|
|
177
179
|
|
|
178
180
|
export const LOCAL_MLC_CAUSAL_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
179
181
|
|
|
@@ -369,7 +371,7 @@ let bePayloadDataPromise: Promise<void> | undefined;
|
|
|
369
371
|
* :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
|
|
370
372
|
* :returns: The parsed JSON value, or `null` if neither interop mode applies.
|
|
371
373
|
*/
|
|
372
|
-
|
|
374
|
+
function unwrapJsonModule<T>(mod: unknown, shape: 'object' | 'array'): T | null {
|
|
373
375
|
if (mod == null || typeof mod !== 'object') {
|
|
374
376
|
return null;
|
|
375
377
|
}
|
|
@@ -411,7 +413,7 @@ const unwrapJsonModule = <T>(mod: unknown, shape: 'object' | 'array'): T | null
|
|
|
411
413
|
}
|
|
412
414
|
|
|
413
415
|
return null;
|
|
414
|
-
}
|
|
416
|
+
}
|
|
415
417
|
|
|
416
418
|
/**
|
|
417
419
|
* Lazily load and build the BE-parity lookup tables from their JSON payloads.
|
|
@@ -769,8 +771,7 @@ export const createLocalSlowLaneClient = (
|
|
|
769
771
|
}
|
|
770
772
|
if (
|
|
771
773
|
lower.includes('out of memory') ||
|
|
772
|
-
|
|
773
|
-
/\boom\b/.test(lower) ||
|
|
774
|
+
OOM_REGEX.test(lower) ||
|
|
774
775
|
lower.includes('allocation') ||
|
|
775
776
|
lower.includes('exceeds') ||
|
|
776
777
|
lower.includes('buffer size') ||
|
|
@@ -1004,14 +1005,15 @@ export const createLocalSlowLaneClient = (
|
|
|
1004
1005
|
const lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
|
|
1005
1006
|
const semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
|
|
1006
1007
|
const arcticInput = wrapForArctic(semanticText);
|
|
1007
|
-
|
|
1008
|
+
function captureCompletionTime<T>(
|
|
1008
1009
|
promise: Promise<T>,
|
|
1009
1010
|
onResolved: (resolvedAt: number) => void,
|
|
1010
|
-
): Promise<T>
|
|
1011
|
-
promise.then((value: T) => {
|
|
1011
|
+
): Promise<T> {
|
|
1012
|
+
return promise.then((value: T) => {
|
|
1012
1013
|
onResolved(performance.now());
|
|
1013
1014
|
return value;
|
|
1014
1015
|
});
|
|
1016
|
+
}
|
|
1015
1017
|
|
|
1016
1018
|
if (isAutocompleteDebugEnabled()) {
|
|
1017
1019
|
// eslint-disable-next-line no-console
|
|
@@ -29,6 +29,8 @@ export interface TypeaheadEncodingsResponse {
|
|
|
29
29
|
|
|
30
30
|
// eslint-disable-next-line require-unicode-regexp
|
|
31
31
|
const WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
|
|
32
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
33
|
+
const TRAILING_SLASH_REGEX = /\/$/;
|
|
32
34
|
const DEFAULT_DEBOUNCE_MS = 300;
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -94,8 +96,7 @@ export const createSlowLaneClient = (
|
|
|
94
96
|
return;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
const url = `${baseUrl.replace(/\/$/, '')}${endpoint}`;
|
|
99
|
+
const url = `${baseUrl.replace(TRAILING_SLASH_REGEX, '')}${endpoint}`;
|
|
99
100
|
const payload: TypeaheadEncodingsRequest = {
|
|
100
101
|
text,
|
|
101
102
|
session_id: sessionId,
|
|
@@ -29,6 +29,12 @@ import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
|
29
29
|
|
|
30
30
|
// eslint-disable-next-line require-unicode-regexp
|
|
31
31
|
const PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
|
|
32
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
33
|
+
const WHITESPACE_SPLIT_REGEX = /\s+/;
|
|
34
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
35
|
+
const SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
|
|
36
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
37
|
+
const TRAILING_WHITESPACE_REGEX = /\s$/;
|
|
32
38
|
|
|
33
39
|
const MIN_PREFIX_LENGTH = 3;
|
|
34
40
|
const MAX_CANDIDATES = 200;
|
|
@@ -275,8 +281,8 @@ const getContextVectorForScoring = (textBefore: string): Float32Array | null =>
|
|
|
275
281
|
|
|
276
282
|
const tokenize = (text: string): string[] => {
|
|
277
283
|
const tokens: string[] = [];
|
|
278
|
-
// eslint-disable-next-line
|
|
279
|
-
for (const raw of text.toLowerCase().split(
|
|
284
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
285
|
+
for (const raw of text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)) {
|
|
280
286
|
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
281
287
|
const clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
|
|
282
288
|
if (clean.length >= 2) {
|
|
@@ -288,12 +294,10 @@ const tokenize = (text: string): string[] => {
|
|
|
288
294
|
|
|
289
295
|
const extractPreviousWord = (text: string): string => {
|
|
290
296
|
// Only consider the current sentence/line the user is typing in.
|
|
291
|
-
|
|
292
|
-
const sentences = text.split(/[\n.?!]+/);
|
|
297
|
+
const sentences = text.split(SENTENCE_BOUNDARY_REGEX);
|
|
293
298
|
const currentSentence = sentences[sentences.length - 1];
|
|
294
299
|
|
|
295
|
-
|
|
296
|
-
const words = currentSentence.trimEnd().split(/\s+/);
|
|
300
|
+
const words = currentSentence.trimEnd().split(WHITESPACE_SPLIT_REGEX);
|
|
297
301
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
298
302
|
};
|
|
299
303
|
|
|
@@ -442,8 +446,7 @@ export const predict = (textBefore: string): string | null => {
|
|
|
442
446
|
// }
|
|
443
447
|
|
|
444
448
|
// ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
|
|
445
|
-
|
|
446
|
-
if (textBefore.length > 0 && /\s$/.test(textBefore)) {
|
|
449
|
+
if (textBefore.length > 0 && TRAILING_WHITESPACE_REGEX.test(textBefore)) {
|
|
447
450
|
return null;
|
|
448
451
|
}
|
|
449
452
|
|
|
@@ -715,7 +718,7 @@ interface VocabularyJson {
|
|
|
715
718
|
* array and a sparse numeric-keyed object are emitted identically as named
|
|
716
719
|
* exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
|
|
717
720
|
*/
|
|
718
|
-
|
|
721
|
+
function unwrapJsonModule<T>(mod: unknown, shape: 'object' | 'array'): T | null {
|
|
719
722
|
if (mod == null || typeof mod !== 'object') {
|
|
720
723
|
return null;
|
|
721
724
|
}
|
|
@@ -743,7 +746,7 @@ const unwrapJsonModule = <T>(mod: unknown, shape: 'object' | 'array'): T | null
|
|
|
743
746
|
}
|
|
744
747
|
|
|
745
748
|
return null;
|
|
746
|
-
}
|
|
749
|
+
}
|
|
747
750
|
|
|
748
751
|
export const loadVectorsAsync = async (options?: {
|
|
749
752
|
getBinaryUrl?: () => Promise<string>;
|