@atlaskit/editor-plugin-autocomplete 3.4.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,12 @@ import type { AutocompletePlugin } from '../autocompletePluginType';
16
16
 
17
17
  import { isAutocompleteDebugEnabled } from './debug-mode';
18
18
  import { createGhostTextDecorationSet } from './ghost-text-decoration';
19
- import { createLocalSlowLaneClient, type LocalSlowLaneClient } from './local-slow-lane-client';
19
+ import {
20
+ createLocalSlowLaneClient,
21
+ type LocalSlowLaneClient,
22
+ type LocalSlowLaneLoadError,
23
+ type LocalSlowLaneLoadSuccess,
24
+ } from './local-slow-lane-client';
20
25
  import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
21
26
  import {
22
27
  predict,
@@ -298,6 +303,43 @@ export const createAutocompletePlugin = (
298
303
  });
299
304
  };
300
305
 
306
+ const fireLocalModelLoadedAnalytics = (info: LocalSlowLaneLoadSuccess): void => {
307
+ api?.analytics?.actions.fireAnalyticsEvent({
308
+ action: ACTION.LOCAL_MODEL_LOADED,
309
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
310
+ eventType: EVENT_TYPE.TRACK,
311
+ attributes: {
312
+ modelId: info.modelId,
313
+ embeddingModelId: info.embeddingModelId,
314
+ loadDurationMs: info.loadDurationMs,
315
+ gpuVendor: info.capabilities.vendor,
316
+ gpuArchitecture: info.capabilities.architecture,
317
+ },
318
+ });
319
+ };
320
+
321
+ const fireLocalModelLoadFailedAnalytics = (error: LocalSlowLaneLoadError): void => {
322
+ const { capabilities } = error;
323
+ api?.analytics?.actions.fireAnalyticsEvent({
324
+ action: ACTION.LOCAL_MODEL_LOAD_FAILED,
325
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
326
+ eventType: EVENT_TYPE.TRACK,
327
+ attributes: {
328
+ reason: error.reason,
329
+ message: error.message,
330
+ modelId: error.modelId,
331
+ embeddingModelId: error.embeddingModelId,
332
+ webgpuAvailable: capabilities.available,
333
+ adapterAvailable: capabilities.adapterAvailable,
334
+ shaderF16Supported: capabilities.shaderF16Supported,
335
+ maxBufferSizeMB: capabilities.maxBufferSizeMB,
336
+ maxStorageBufferBindingSizeMB: capabilities.maxStorageBufferBindingSizeMB,
337
+ gpuVendor: capabilities.vendor,
338
+ gpuArchitecture: capabilities.architecture,
339
+ },
340
+ });
341
+ };
342
+
301
343
  const fireSuggestionInsertedAnalytics = (ghostText: string): void => {
302
344
  const typedLength = lastSuggestionTypedLength;
303
345
  const suggestionLength = lastSuggestionLength || typedLength + ghostText.length;
@@ -319,6 +361,8 @@ export const createAutocompletePlugin = (
319
361
  const slowLaneClient = options?.useLocalModel
320
362
  ? createLocalSlowLaneClient({
321
363
  debounceMs: LOCAL_SLOW_LANE_DEBOUNCE_MS,
364
+ onLoadSuccess: fireLocalModelLoadedAnalytics,
365
+ onLoadError: fireLocalModelLoadFailedAnalytics,
322
366
  })
323
367
  : createSlowLaneClient({
324
368
  baseUrl: '',
@@ -1,18 +1,65 @@
1
1
  /**
2
2
  * Contextual Typeahead Completions (CTC) debug logging utility.
3
3
  *
4
- * Logs are silent by default. To enable in any environment (dev, staging, prod):
4
+ * Logs are silent by default. Enable in any environment (dev, staging, prod):
5
5
  *
6
- * localStorage.setItem('atl-ctc-dbg', '1')
6
+ * // Live, for the current session (no reload required):
7
+ * __atlCtcDebug__.enable()
7
8
  *
8
- * Then reload the page. To disable:
9
+ * // To disable:
10
+ * __atlCtcDebug__.disable()
9
11
  *
10
- * localStorage.removeItem('atl-ctc-dbg')
12
+ * // From initial page load (survives reload) — append to the URL:
13
+ * ?atlCtcDebug=1
14
+ *
15
+ * Storage-free by design: avoids browser-storage consent controls (BSC), which can
16
+ * block uncategorized localStorage/sessionStorage/cookie writes in some products.
11
17
  */
12
- export const isAutocompleteDebugEnabled = (): boolean => {
18
+
19
+ declare global {
20
+ interface Window {
21
+ __atlCtcDebug__?: {
22
+ enable: () => void;
23
+ disable: () => void;
24
+ isEnabled: () => boolean;
25
+ };
26
+ }
27
+ }
28
+
29
+ const hasUrlDebugFlag = (): boolean => {
30
+ if (typeof window === 'undefined') {
31
+ return false;
32
+ }
13
33
  try {
14
- return typeof localStorage !== 'undefined' && localStorage.getItem('atl-ctc-dbg') === '1';
34
+ return new URLSearchParams(window.location.search).get('atlCtcDebug') === '1';
15
35
  } catch {
16
36
  return false;
17
37
  }
18
38
  };
39
+
40
+ // State lives on the window object (not a module closure) so duplicate copies of this
41
+ // module across separate bundles/realms share one source of truth and the console API
42
+ // controls them all. In-memory only; persisted across reload via the URL flag.
43
+ const getDebugApi = (): Window['__atlCtcDebug__'] => {
44
+ if (typeof window === 'undefined') {
45
+ return undefined;
46
+ }
47
+ if (!window.__atlCtcDebug__) {
48
+ let debugEnabled = hasUrlDebugFlag();
49
+ window.__atlCtcDebug__ = {
50
+ enable: () => {
51
+ debugEnabled = true;
52
+ },
53
+ disable: () => {
54
+ debugEnabled = false;
55
+ },
56
+ isEnabled: () => debugEnabled,
57
+ };
58
+ }
59
+ return window.__atlCtcDebug__;
60
+ };
61
+
62
+ export const isAutocompleteDebugEnabled = (): boolean => getDebugApi()?.isEnabled() ?? false;
63
+
64
+ // Eagerly install so the console API is available on load, regardless of call order.
65
+ getDebugApi();
@@ -71,6 +71,10 @@ export interface LocalSlowLaneClientConfig {
71
71
  * `customModelConfig` with the model URL and WASM library URL.
72
72
  */
73
73
  modelId?: string;
74
+ /** Callback fired when the engine fails to load/start. */
75
+ onLoadError?: (error: LocalSlowLaneLoadError) => void;
76
+ /** Callback fired when the engine successfully loads and is ready. */
77
+ onLoadSuccess?: (info: LocalSlowLaneLoadSuccess) => void;
74
78
  /** Callback fired with status messages (model loading progress, etc.). */
75
79
  onStatus?: (message: string) => void;
76
80
  /** Callback fired when inference returns new results. */
@@ -91,6 +95,80 @@ export interface LocalSlowLaneClient {
91
95
  updateContext: (text: string) => void;
92
96
  }
93
97
 
98
+ /**
99
+ * Why the local engine failed to load/start.
100
+ *
101
+ * The first three are user-machine limitations (WebGPU missing, no compatible
102
+ * GPU adapter, GPU lacks the `shader-f16` feature the model needs);
103
+ * `insufficient_memory` is hit when weights don't fit in VRAM. The rest cover
104
+ * delivery/runtime failures unrelated to hardware.
105
+ */
106
+ export type LocalSlowLaneLoadErrorReason =
107
+ | 'webgpu_unavailable'
108
+ | 'webgpu_no_adapter'
109
+ | 'missing_shader_f16'
110
+ | 'insufficient_memory'
111
+ | 'model_download_failed'
112
+ | 'module_load_failed'
113
+ | 'init_failed';
114
+
115
+ /** Snapshot of the machine's WebGPU support, used to explain hardware limits. */
116
+ export interface WebGpuCapabilities {
117
+ /** GPU architecture reported by the adapter (e.g. "metal-3", "rdna2"). */
118
+ architecture?: string;
119
+ /** Whether `navigator.gpu.requestAdapter()` returned a usable adapter. */
120
+ adapterAvailable?: boolean;
121
+ /** Whether `navigator.gpu` exists at all. */
122
+ available: boolean;
123
+ /** Largest single GPU buffer the adapter allows, in MB. */
124
+ maxBufferSizeMB?: number;
125
+ /** Largest storage-buffer binding the adapter allows, in MB. */
126
+ maxStorageBufferBindingSizeMB?: number;
127
+ /** Whether the adapter exposes the `shader-f16` feature the model requires. */
128
+ shaderF16Supported?: boolean;
129
+ /** GPU vendor reported by the adapter (e.g. "apple", "intel"). */
130
+ vendor?: string;
131
+ }
132
+
133
+ export interface LocalSlowLaneLoadError {
134
+ /** WebGPU support snapshot — explains hardware limitations behind the failure. */
135
+ capabilities: WebGpuCapabilities;
136
+ /** Semantic embedder loaded alongside the causal LM (loads atomically). */
137
+ embeddingModelId: string;
138
+ /** Canonical, controlled failure description (never raw error text). */
139
+ message: string;
140
+ /** Causal LM identifier that failed to load. */
141
+ modelId: string;
142
+ /** Coarse, privacy-safe failure category. */
143
+ reason: LocalSlowLaneLoadErrorReason;
144
+ }
145
+
146
+ export interface LocalSlowLaneLoadSuccess {
147
+ /** WebGPU support snapshot for the machine that loaded the model. */
148
+ capabilities: WebGpuCapabilities;
149
+ /** Semantic embedder loaded alongside the causal LM (loads atomically). */
150
+ embeddingModelId: string;
151
+ /** Model engine load time in ms (excludes the WebGPU capability probe). */
152
+ loadDurationMs: number;
153
+ /** Causal LM identifier that loaded. */
154
+ modelId: string;
155
+ }
156
+
157
+ // Minimal WebGPU shape: lib.dom types aren't guaranteed in this build target.
158
+ interface MinimalGpuAdapterInfo {
159
+ architecture?: string;
160
+ vendor?: string;
161
+ }
162
+ interface MinimalGpuAdapter {
163
+ features: { has: (feature: string) => boolean };
164
+ info?: MinimalGpuAdapterInfo;
165
+ limits?: { maxBufferSize?: number; maxStorageBufferBindingSize?: number };
166
+ requestAdapterInfo?: () => Promise<MinimalGpuAdapterInfo>;
167
+ }
168
+ interface MinimalGpu {
169
+ requestAdapter: () => Promise<MinimalGpuAdapter | null>;
170
+ }
171
+
94
172
  // ─── Constants ───────────────────────────────────────────────────────────────
95
173
 
96
174
  const DEFAULT_DEBOUNCE_MS = 300;
@@ -584,6 +662,8 @@ export const createLocalSlowLaneClient = (
584
662
  debounceMs = DEFAULT_DEBOUNCE_MS,
585
663
  onUpdate,
586
664
  onStatus,
665
+ onLoadError,
666
+ onLoadSuccess,
587
667
  modelId = LOCAL_MLC_CAUSAL_MODEL_ID,
588
668
  customModelConfig,
589
669
  } = config;
@@ -641,7 +721,142 @@ export const createLocalSlowLaneClient = (
641
721
  onStatus?.(message);
642
722
  };
643
723
 
724
+ const bytesToMB = (bytes?: number): number | undefined =>
725
+ typeof bytes === 'number' ? Math.round(bytes / (1024 * 1024)) : undefined;
726
+
727
+ /**
728
+ * Inspect the machine's WebGPU support so a load failure can be attributed
729
+ * to a concrete hardware/browser limitation rather than a generic error.
730
+ */
731
+ const probeWebGpuCapabilities = async (): Promise<WebGpuCapabilities> => {
732
+ const gpu = (navigator as Navigator & { gpu?: MinimalGpu }).gpu;
733
+ if (!gpu) {
734
+ return { available: false };
735
+ }
736
+ try {
737
+ const adapter = await gpu.requestAdapter();
738
+ if (!adapter) {
739
+ return { available: true, adapterAvailable: false };
740
+ }
741
+ let vendor: string | undefined;
742
+ let architecture: string | undefined;
743
+ try {
744
+ const info = adapter.info ?? (await adapter.requestAdapterInfo?.());
745
+ vendor = info?.vendor || undefined;
746
+ architecture = info?.architecture || undefined;
747
+ } catch {
748
+ // adapter info is best-effort
749
+ }
750
+ return {
751
+ available: true,
752
+ adapterAvailable: true,
753
+ shaderF16Supported: adapter.features.has('shader-f16'),
754
+ maxBufferSizeMB: bytesToMB(adapter.limits?.maxBufferSize),
755
+ maxStorageBufferBindingSizeMB: bytesToMB(adapter.limits?.maxStorageBufferBindingSize),
756
+ vendor,
757
+ architecture,
758
+ };
759
+ } catch {
760
+ return { available: true, adapterAvailable: false };
761
+ }
762
+ };
763
+
764
+ /** Map an MLC/WebLLM engine-creation error message to a coarse reason. */
765
+ const classifyEngineError = (message: string): LocalSlowLaneLoadErrorReason => {
766
+ const lower = message.toLowerCase();
767
+ if (
768
+ lower.includes('loading chunk') ||
769
+ lower.includes('dynamically imported module') ||
770
+ lower.includes('dynamic import')
771
+ ) {
772
+ return 'module_load_failed';
773
+ }
774
+ if (
775
+ lower.includes('out of memory') ||
776
+ /\boom\b/u.test(lower) ||
777
+ lower.includes('allocation') ||
778
+ lower.includes('exceeds') ||
779
+ lower.includes('buffer size') ||
780
+ lower.includes('not enough memory')
781
+ ) {
782
+ return 'insufficient_memory';
783
+ }
784
+ // Pre-flight already returns missing_shader_f16 when the feature is absent,
785
+ // so only match the exact feature token here — not bare 'shader' (compile
786
+ // errors) or bare 'f16' (present in model ids like q0f16-MLC).
787
+ if (lower.includes('shader-f16') || lower.includes('shader_f16')) {
788
+ return 'missing_shader_f16';
789
+ }
790
+ if (
791
+ lower.includes('fetch') ||
792
+ lower.includes('network') ||
793
+ lower.includes('download') ||
794
+ lower.includes('http') ||
795
+ lower.includes('cache')
796
+ ) {
797
+ return 'model_download_failed';
798
+ }
799
+ return 'init_failed';
800
+ };
801
+
802
+ // Canonical, controlled failure descriptions. We never emit the raw engine
803
+ // error into analytics — it can embed customer-context URLs/paths (HOT-120175)
804
+ // — so the analytics `message` is always one of these fixed strings.
805
+ const LOAD_FAILURE_MESSAGE: Record<LocalSlowLaneLoadErrorReason, string> = {
806
+ webgpu_unavailable: 'WebGPU is not available in this browser',
807
+ webgpu_no_adapter: 'No compatible WebGPU adapter found',
808
+ missing_shader_f16: 'GPU does not support the shader-f16 feature',
809
+ insufficient_memory: 'Insufficient GPU memory to load the model',
810
+ model_download_failed: 'Failed to download model assets',
811
+ module_load_failed: 'Failed to load the web-llm runtime module',
812
+ init_failed: 'Model engine failed to initialise',
813
+ };
814
+
815
+ const handleLoadFailure = (
816
+ reason: LocalSlowLaneLoadErrorReason,
817
+ capabilities: WebGpuCapabilities,
818
+ // Raw engine error — local debug logging only, never sent to analytics.
819
+ debugDetail?: string,
820
+ ): void => {
821
+ ready = false;
822
+ const message = LOAD_FAILURE_MESSAGE[reason];
823
+ if (isAutocompleteDebugEnabled()) {
824
+ // eslint-disable-next-line no-console
825
+ console.log(
826
+ `[LocalSlowLane] Engine initialisation failed (${reason}): ${debugDetail ?? message}`,
827
+ );
828
+ }
829
+ onStatus?.(`Engine initialisation failed: ${message}`);
830
+ onLoadError?.({
831
+ reason,
832
+ message,
833
+ modelId,
834
+ embeddingModelId: LOCAL_MLC_EMBEDDING_MODEL_ID,
835
+ capabilities,
836
+ });
837
+ engineInitPromise = null;
838
+ initFailed = true;
839
+ };
840
+
644
841
  const initEngine = async (): Promise<void> => {
842
+ const capabilities = await probeWebGpuCapabilities();
843
+
844
+ // ── Pre-flight: machine limitations short-circuit before the expensive load ──
845
+ if (!capabilities.available) {
846
+ handleLoadFailure('webgpu_unavailable', capabilities);
847
+ return;
848
+ }
849
+ if (capabilities.adapterAvailable === false) {
850
+ handleLoadFailure('webgpu_no_adapter', capabilities);
851
+ return;
852
+ }
853
+ if (capabilities.shaderF16Supported === false) {
854
+ handleLoadFailure('missing_shader_f16', capabilities);
855
+ return;
856
+ }
857
+
858
+ const startTime = performance.now();
859
+
645
860
  try {
646
861
  if (isAutocompleteDebugEnabled()) {
647
862
  // eslint-disable-next-line no-console
@@ -653,10 +868,6 @@ export const createLocalSlowLaneClient = (
653
868
  }
654
869
  onStatus?.(`Initialising models: ${modelId} + ${LOCAL_MLC_EMBEDDING_MODEL_ID}…`);
655
870
 
656
- if (!('gpu' in navigator)) {
657
- throw new Error('WebGPU not supported');
658
- }
659
-
660
871
  // Fetch the web-llm runtime and the BE-parity lookup tables in parallel;
661
872
  // both are dynamically imported so they stay out of the main editor chunk.
662
873
  const [{ MLCEngine: MLCEngineCtor, prebuiltAppConfig }] = await Promise.all([
@@ -714,6 +925,7 @@ export const createLocalSlowLaneClient = (
714
925
 
715
926
  engine = newEngine;
716
927
  ready = true;
928
+ const loadDurationMs = Math.round(performance.now() - startTime);
717
929
 
718
930
  if (isAutocompleteDebugEnabled()) {
719
931
  // eslint-disable-next-line no-console
@@ -740,16 +952,15 @@ export const createLocalSlowLaneClient = (
740
952
  );
741
953
  }
742
954
  onStatus?.('Model loaded and ready.');
955
+ onLoadSuccess?.({
956
+ modelId,
957
+ embeddingModelId: LOCAL_MLC_EMBEDDING_MODEL_ID,
958
+ loadDurationMs,
959
+ capabilities,
960
+ });
743
961
  } catch (err) {
744
962
  const errorMsg = err instanceof Error ? err.message : String(err);
745
- ready = false;
746
- if (isAutocompleteDebugEnabled()) {
747
- // eslint-disable-next-line no-console
748
- console.log(`[LocalSlowLane] Engine initialisation failed: ${errorMsg}`);
749
- }
750
- onStatus?.(`Engine initialisation failed: ${errorMsg}`);
751
- engineInitPromise = null;
752
- initFailed = true;
963
+ handleLoadFailure(classifyEngineError(errorMsg), capabilities, errorMsg);
753
964
  }
754
965
  };
755
966