arcane-os 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
  4. package/browser-runtime/ai/browser-speech-providers.mjs +1 -0
  5. package/docs/architecture.md +2 -2
  6. package/docs/reference/README.md +79 -13
  7. package/docs/reference/ai/browser-speech.md +336 -0
  8. package/docs/reference/ai/browser-wasm.md +207 -82
  9. package/docs/reference/availability-and-normalization.md +33 -4
  10. package/docs/reference/behavioral-testing.md +4 -1
  11. package/docs/reference/cli.md +29 -10
  12. package/docs/reference/core/arcane-ai-contracts.md +43 -9
  13. package/docs/reference/inventory/package-api.json +110 -14
  14. package/docs/reference/inventory/runtime-components.json +42 -14
  15. package/docs/reference/inventory/runtime-modules.json +118 -13
  16. package/docs/reference/protocols.md +264 -38
  17. package/docs/reference/runtime-components.md +177 -21
  18. package/docs/reference/runtime-modules.md +449 -13
  19. package/docs/reference/sdk-api.md +626 -85
  20. package/package.json +1 -1
  21. package/runtime/ARCANE_RUNTIME_RELEASE.json +19 -19
  22. package/runtime/arcane/components/chat.html +17 -52
  23. package/runtime/arcane/components/speech.html +109 -29
  24. package/runtime/arcane/components/voice-transcription.html +437 -68
  25. package/runtime/arcane/modules/AI.js +556 -128
  26. package/runtime/arcane/modules/AIProviderRuntime.js +299 -30
  27. package/runtime/arcane/modules/AIRuntimeState.js +23 -4
  28. package/runtime/arcane/modules/ComponentContracts.js +272 -0
  29. package/runtime/arcane/modules/LocalAIReadinessController.js +1 -1
  30. package/schemas/arcane-lock.schema.json +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "builder": "arcane-sdk-runtime-v1",
4
- "sdkVersion": "0.2.1",
4
+ "sdkVersion": "0.2.3",
5
5
  "source": {
6
6
  "authority": "sdk-canonical",
7
7
  "repository": "https://github.com/TheWizardNexus/arcane-os-sdk.git",
@@ -14,8 +14,8 @@
14
14
  }
15
15
  },
16
16
  "fileCount": 160,
17
- "totalBytes": 3605154,
18
- "contentSha256": "5b921d50b6a0cf36a13f7a7dedf96cd3a68104a322e5139136fd4197aa1ca7cb",
17
+ "totalBytes": 3655536,
18
+ "contentSha256": "f96a27bd79a809201708f73ca729bc60ab02696daf619365f77267a28b47b4b3",
19
19
  "files": [
20
20
  {
21
21
  "path": "arcane/components/app-bar.html",
@@ -39,8 +39,8 @@
39
39
  },
40
40
  {
41
41
  "path": "arcane/components/chat.html",
42
- "bytes": 61650,
43
- "sha256": "d644848fbc3ccc85d491db7ada75231f393a371dd3a0ccd92f223c172571e47b"
42
+ "bytes": 60478,
43
+ "sha256": "2f3a7f454c1e3da64c9eaeedb0869d49f04636b20ff59e5116a671e0d0a17973"
44
44
  },
45
45
  {
46
46
  "path": "arcane/components/conversation-view.html",
@@ -159,8 +159,8 @@
159
159
  },
160
160
  {
161
161
  "path": "arcane/components/speech.html",
162
- "bytes": 38217,
163
- "sha256": "61786685b7f4f257da77db2cbbe8230b379c56292ad635081f5b0763d5f48e1e"
162
+ "bytes": 41300,
163
+ "sha256": "0ed5dd29c37747702f114114ad66bedd419fd39bed35fc12362c63c6357a41d9"
164
164
  },
165
165
  {
166
166
  "path": "arcane/components/summary-strip.html",
@@ -199,8 +199,8 @@
199
199
  },
200
200
  {
201
201
  "path": "arcane/components/voice-transcription.html",
202
- "bytes": 14471,
203
- "sha256": "7fab0ca767c252f661f7a3a5e09cf7325692b64a1d03e5731dc5d4f83c80c124"
202
+ "bytes": 26991,
203
+ "sha256": "8140b918791df5043be9254d52bf7864fe91759ab6e5e60af2364d6b4a8fcb7b"
204
204
  },
205
205
  {
206
206
  "path": "arcane/components/weather-widget.html",
@@ -374,8 +374,8 @@
374
374
  },
375
375
  {
376
376
  "path": "arcane/modules/AI.js",
377
- "bytes": 103733,
378
- "sha256": "3a051d67b1e895b7c6f14af36a34daf1d92fe70e02c95c1c4ffd80cc7c622561"
377
+ "bytes": 120607,
378
+ "sha256": "703a5342ed0c0758cccdfd8469fbae6d86e91de8c43ec01c893619daf97c2d34"
379
379
  },
380
380
  {
381
381
  "path": "arcane/modules/AIPreferenceRuntime.js",
@@ -389,8 +389,8 @@
389
389
  },
390
390
  {
391
391
  "path": "arcane/modules/AIProviderRuntime.js",
392
- "bytes": 83261,
393
- "sha256": "32aa65b464ea9a9317d47f382063fc052e1f47e63e6c65c28e58898e0ab4911a"
392
+ "bytes": 93507,
393
+ "sha256": "d1b33f678431128cefe6f43daabe177794e758c770394098871cf4ac726926b8"
394
394
  },
395
395
  {
396
396
  "path": "arcane/modules/AIResponseLength.js",
@@ -404,8 +404,8 @@
404
404
  },
405
405
  {
406
406
  "path": "arcane/modules/AIRuntimeState.js",
407
- "bytes": 24783,
408
- "sha256": "8b9bfaf208eb44455b581d8e700cec785044f81595dcf839dcdfa3e06ed0f5fa"
407
+ "bytes": 25379,
408
+ "sha256": "492162cec5e6a8b9e9b437833172905b2cb4e352ca1ef30104c65ec29cf3fa29"
409
409
  },
410
410
  {
411
411
  "path": "arcane/modules/AnsiText.js",
@@ -494,8 +494,8 @@
494
494
  },
495
495
  {
496
496
  "path": "arcane/modules/ComponentContracts.js",
497
- "bytes": 20507,
498
- "sha256": "4115d4ce8081e1dbaeb0a1a2fa3201ecddabf05a471f76e4deb7a071f9c31d20"
497
+ "bytes": 28736,
498
+ "sha256": "f5ba011094835b45258ee833a90bfa17980e860ef9ceac88aa56ecad3abe9d53"
499
499
  },
500
500
  {
501
501
  "path": "arcane/modules/ConfiguredAIChatSession.js",
@@ -599,8 +599,8 @@
599
599
  },
600
600
  {
601
601
  "path": "arcane/modules/LocalAIReadinessController.js",
602
- "bytes": 4334,
603
- "sha256": "32d4ce2639557b89a283754a67435226777b56df696ce7a76231ec6c0274b25c"
602
+ "bytes": 4340,
603
+ "sha256": "f80d51d0ad499664f265b4f2d52a46ede521a78d56714fecb0e13b939b261007"
604
604
  },
605
605
  {
606
606
  "path": "arcane/modules/MD.js",
@@ -719,8 +719,7 @@
719
719
  ?speech.initialMuted
720
720
  :true;
721
721
  let initialSpeechMuteListenerInstalled=false;
722
- let pendingSpeechAvailability=null;
723
- let speechAvailabilityListenerInstalled=false;
722
+ let latestAIRuntimeRoles=null;
724
723
  const hostSubmissionBarrier=new ConversationSubmissionBarrier();
725
724
  const aiRuntimeStateAbortController=new AbortController();
726
725
  let programmaticSubmissionQueue=Promise.resolve(true);
@@ -910,36 +909,32 @@
910
909
  }
911
910
 
912
911
  function setAIAvailability(input={}){
913
- const explicitSpeechAvailability={};
914
912
  const availability={
915
- llm:Object.prototype.hasOwnProperty.call(input,'llm')
913
+ llm:hasSelectedAIRuntimeRole('llm')
914
+ ?latestAIRuntimeRoles.llm.state==='ready'
915
+ :Object.prototype.hasOwnProperty.call(input,'llm')
916
916
  ?Boolean(input.llm)
917
917
  :host.aiAvailability.llm,
918
- stt:Object.prototype.hasOwnProperty.call(input,'stt')
919
- ?Boolean(input.stt)
918
+ stt:latestAIRuntimeRoles
919
+ ?latestAIRuntimeRoles.stt.state==='ready'
920
920
  :host.aiAvailability.stt,
921
- tts:Object.prototype.hasOwnProperty.call(input,'tts')
922
- ?Boolean(input.tts)
921
+ tts:latestAIRuntimeRoles
922
+ ?latestAIRuntimeRoles.tts.state==='ready'
923
923
  :host.aiAvailability.tts
924
924
  };
925
925
 
926
- if(Object.prototype.hasOwnProperty.call(input,'stt')){
927
- explicitSpeechAvailability.stt=availability.stt;
928
- }
929
- if(Object.prototype.hasOwnProperty.call(input,'tts')){
930
- explicitSpeechAvailability.tts=availability.tts;
931
- }
932
926
  applyAIAvailability(availability);
933
- if(Object.keys(explicitSpeechAvailability).length){
934
- pendingSpeechAvailability={
935
- ...(pendingSpeechAvailability||{}),
936
- ...explicitSpeechAvailability
937
- };
938
- synchronizeSpeechCompatibilityAvailability();
939
- }
940
927
  return {...availability};
941
928
  }
942
929
 
930
+ function hasSelectedAIRuntimeRole(role){
931
+ const record=latestAIRuntimeRoles?.[role];
932
+ return typeof record?.providerId==='string'
933
+ &&record.providerId.length>0
934
+ &&typeof record?.modelId==='string'
935
+ &&record.modelId.length>0;
936
+ }
937
+
943
938
  function applyAIAvailability(availability){
944
939
  host.aiAvailability={...availability};
945
940
  send.disabled=host.conversationComplete||!availability.llm;
@@ -1131,6 +1126,7 @@
1131
1126
  }
1132
1127
 
1133
1128
  function synchronizeAIRuntimeState(snapshot){
1129
+ latestAIRuntimeRoles=snapshot.roles;
1134
1130
  aiActivationController.synchronize(snapshot.roles.llm);
1135
1131
  applyAIAvailability({
1136
1132
  llm:snapshot.roles.llm.state==='ready',
@@ -1139,37 +1135,6 @@
1139
1135
  });
1140
1136
  }
1141
1137
 
1142
- function synchronizeSpeechCompatibilityAvailability(){
1143
- if(!pendingSpeechAvailability){
1144
- return;
1145
- }
1146
- if(speech.componentReady){
1147
- const availability=pendingSpeechAvailability;
1148
- pendingSpeechAvailability=null;
1149
- speech.setAvailability?.({...availability});
1150
- return;
1151
- }
1152
- if(speechAvailabilityListenerInstalled){
1153
- return;
1154
- }
1155
-
1156
- speechAvailabilityListenerInstalled=true;
1157
- speech.addEventListener(
1158
- 'speech-ready',
1159
- applyPendingSpeechAvailability,
1160
- {once:true}
1161
- );
1162
- }
1163
-
1164
- function applyPendingSpeechAvailability(){
1165
- speechAvailabilityListenerInstalled=false;
1166
- if(pendingSpeechAvailability){
1167
- const availability=pendingSpeechAvailability;
1168
- pendingSpeechAvailability=null;
1169
- speech.setAvailability?.({...availability});
1170
- }
1171
- }
1172
-
1173
1138
  function formatConversationTime(milliseconds=0){
1174
1139
  const totalSeconds=Math.max(0,Math.floor(milliseconds/1000));
1175
1140
  const hours=Math.floor(totalSeconds/3600);
@@ -63,6 +63,13 @@
63
63
  text-align: center;
64
64
  }
65
65
 
66
+ #sttActivationButton {
67
+ grid-column: 1;
68
+ max-width: 8rem;
69
+ background: var(--arcane-action, var(--primary-color));
70
+ color: var(--arcane-action-text, var(--button-text-color, var(--text-color)));
71
+ }
72
+
66
73
  #recordButton:active,
67
74
  #recordButton.pressed {
68
75
  opacity: .82;
@@ -108,7 +115,8 @@
108
115
  grid-template-columns: auto minmax(0, 1fr) auto;
109
116
  }
110
117
 
111
- #recordButton {
118
+ #recordButton,
119
+ #sttActivationButton {
112
120
  grid-row: 1 / span 2;
113
121
  }
114
122
 
@@ -131,6 +139,7 @@
131
139
 
132
140
  <p class="hidden">Will auto send after X seconds of silence set in code.</p>
133
141
  <button id="recordButton" type="button" aria-label="Transcription unavailable" disabled>Hold to talk</button>
142
+ <button id="sttActivationButton" type="button" hidden disabled>Start transcription</button>
134
143
  <button id="stopButton" type="button" class="hidden" disabled>Send</button>
135
144
  <p id="speechStatus" class="speech_status" role="status" aria-live="polite"></p>
136
145
  <button id="endButton" type="button" class="end">End</button>
@@ -147,10 +156,14 @@
147
156
  requestAIRuntimeIntent,
148
157
  subscribeAIRuntimeState
149
158
  } = await import('../modules/AIRuntimeState.js');
159
+ const {
160
+ createSTTActivationController
161
+ } = await import('../modules/ComponentContracts.js');
150
162
 
151
163
  const host = this;
152
164
  const shadowRoot = host.shadowRoot;
153
165
  const recordButton = shadowRoot.querySelector('#recordButton');
166
+ const sttActivationButton = shadowRoot.querySelector('#sttActivationButton');
154
167
  const stopButton = shadowRoot.querySelector('#stopButton');
155
168
  const endButton = shadowRoot.querySelector('#endButton');
156
169
  const muteButton = shadowRoot.querySelector('#muteButton');
@@ -167,11 +180,13 @@
167
180
  let recordingGeneration = 0;
168
181
  let transcriptionGeneration = 0;
169
182
  let transcriptionActive = false;
183
+ let transcriptionAbortController = null;
170
184
  let silenceTimer = null;
171
185
  let activePointerId = null;
172
186
  let keyboardRecording = false;
173
187
  let localStatus = null;
174
188
  let pendingTTSIntent = null;
189
+ let ttsIntentGeneration = 0;
175
190
  let destroyed = false;
176
191
 
177
192
  const initialMutedWasHydrated = typeof host.initialMuted === 'boolean';
@@ -180,6 +195,10 @@
180
195
  host.configure = configure;
181
196
  host.destroy = destroy;
182
197
  host.reportTTSError = reportTTSError;
198
+ host.requestSTTActivation = host.requestSTTActivation
199
+ || function requestSTTActivation(intent) {
200
+ return requestAIRuntimeIntent(intent);
201
+ };
183
202
  host.setAvailability = setAvailability;
184
203
  host.setMuted = setMuted;
185
204
  host.availability = {
@@ -190,6 +209,13 @@
190
209
  stt: false,
191
210
  tts: false
192
211
  };
212
+ const sttActivationController = createSTTActivationController(
213
+ {
214
+ host,
215
+ button: sttActivationButton,
216
+ onChange: renderSTTActivationState
217
+ }
218
+ );
193
219
 
194
220
  recordButton.addEventListener('pointerdown', handleRecordPointerDown);
195
221
  recordButton.addEventListener('pointerup', handleRecordPointerUp);
@@ -248,14 +274,11 @@
248
274
  );
249
275
  }
250
276
 
251
- function compatibilityRole(role, ready) {
252
- return Object.freeze(
253
- {
254
- ...unavailableRole(role),
255
- state: ready ? 'ready' : 'unavailable',
256
- loaded: ready
257
- }
258
- );
277
+ function selectedRole(role) {
278
+ return typeof role?.providerId === 'string'
279
+ && role.providerId.length > 0
280
+ && typeof role?.modelId === 'string'
281
+ && role.modelId.length > 0;
259
282
  }
260
283
 
261
284
  function configure(options = {}) {
@@ -288,11 +311,15 @@
288
311
  if (Object.prototype.hasOwnProperty.call(input, 'microphone')) {
289
312
  host.availability.microphone = Boolean(input.microphone);
290
313
  }
291
- if (Object.prototype.hasOwnProperty.call(input, 'stt')) {
292
- sttRole = compatibilityRole('stt', Boolean(input.stt));
314
+ if (Object.prototype.hasOwnProperty.call(input, 'stt')
315
+ && !Boolean(input.stt)
316
+ && !selectedRole(sttRole)) {
317
+ sttRole = unavailableRole('stt');
293
318
  }
294
- if (Object.prototype.hasOwnProperty.call(input, 'tts')) {
295
- ttsRole = compatibilityRole('tts', Boolean(input.tts));
319
+ if (Object.prototype.hasOwnProperty.call(input, 'tts')
320
+ && !Boolean(input.tts)
321
+ && !selectedRole(ttsRole)) {
322
+ ttsRole = unavailableRole('tts');
296
323
  }
297
324
 
298
325
  if (microphoneWasAvailable && !host.availability.microphone) {
@@ -337,6 +364,7 @@
337
364
  }
338
365
 
339
366
  function applyRoleTransitions(previousSTTRole, previousTTSRole) {
367
+ sttActivationController.synchronize(sttRole);
340
368
  const sttBecameUnready = previousSTTRole.state === 'ready'
341
369
  && sttRole.state !== 'ready';
342
370
  const ttsBecameUnready = previousTTSRole.state === 'ready'
@@ -362,6 +390,11 @@
362
390
  renderStatus();
363
391
  }
364
392
 
393
+ function renderSTTActivationState() {
394
+ renderControls();
395
+ renderStatus();
396
+ }
397
+
365
398
  function clearSettledTTSIntent() {
366
399
  if (!pendingTTSIntent) {
367
400
  return;
@@ -383,6 +416,8 @@
383
416
  function renderControls() {
384
417
  const transcriptionReady = sttRole.state === 'ready';
385
418
  const microphoneReady = host.availability.microphone;
419
+ const activationVisible = sttActivationController.visible;
420
+ recordButton.hidden = activationVisible;
386
421
  recordButton.disabled = !transcriptionReady
387
422
  || !microphoneReady
388
423
  || sttRole.busy
@@ -395,6 +430,24 @@
395
430
  'aria-pressed',
396
431
  String(recordingRequested || Boolean(captureSession))
397
432
  );
433
+ sttActivationButton.hidden = !activationVisible;
434
+ sttActivationButton.disabled = destroyed
435
+ || sttActivationController.pending
436
+ || sttRole.state === 'unloading'
437
+ || sttActivationController.action === null;
438
+ sttActivationButton.textContent = sttActivationController.label;
439
+ sttActivationButton.title = sttActivationController.title;
440
+ sttActivationButton.setAttribute(
441
+ 'aria-label',
442
+ sttActivationButton.title
443
+ );
444
+ sttActivationButton.setAttribute(
445
+ 'aria-busy',
446
+ String(
447
+ sttActivationController.pending
448
+ || ['loading', 'unloading'].includes(sttRole.state)
449
+ )
450
+ );
398
451
  renderTTSControl();
399
452
  }
400
453
 
@@ -492,31 +545,21 @@
492
545
  'aria-busy',
493
546
  String(
494
547
  sttRole.state === 'loading'
548
+ || sttRole.state === 'unloading'
495
549
  || ttsRole.state === 'loading'
496
550
  || sttRole.busy
497
551
  || ttsRole.busy
552
+ || sttActivationController.pending
498
553
  || transcriptionActive
499
554
  )
500
555
  );
501
556
  }
502
557
 
503
558
  function describeSTTRole() {
504
- if (sttRole.state === 'ready') {
505
- if (!host.availability.microphone) {
506
- return 'Microphone unavailable.';
507
- }
508
- return sttRole.busy ? 'Transcription busy.' : 'Transcription ready.';
509
- }
510
- if (sttRole.state === 'loading') {
511
- return `Transcription ${formatRoleProgress(sttRole.progress, 'loading')}.`;
512
- }
513
- if (sttRole.state === 'error') {
514
- return `Transcription error: ${visibleErrorMessage(sttRole.error, 'Unknown error')}.`;
559
+ if (sttRole.state === 'ready' && !host.availability.microphone) {
560
+ return 'Microphone unavailable.';
515
561
  }
516
- if (sttRole.state === 'disposed') {
517
- return 'Transcription disposed.';
518
- }
519
- return 'Transcription unavailable.';
562
+ return sttActivationController.status;
520
563
  }
521
564
 
522
565
  function describeTTSRole() {
@@ -748,10 +791,15 @@
748
791
  }
749
792
 
750
793
  function requestTTSIntent(action, reason) {
794
+ const generation = ++ttsIntentGeneration;
751
795
  pendingTTSIntent = action === 'load' && ttsRole.state === 'ready'
752
796
  ? null
753
797
  : action;
798
+ let lifecycle = null;
754
799
  try {
800
+ if (typeof globalThis.ai?.setSpeechMuted === 'function') {
801
+ lifecycle = globalThis.ai.setSpeechMuted(action === 'unload');
802
+ }
755
803
  requestAIRuntimeIntent(
756
804
  {
757
805
  role: 'tts',
@@ -761,8 +809,27 @@
761
809
  );
762
810
  } catch (error) {
763
811
  pendingTTSIntent = null;
812
+ if (action === 'load') {
813
+ host.muted = true;
814
+ stopTTSPlayback();
815
+ }
764
816
  reportTTSLifecycleError(error, action);
817
+ return false;
765
818
  }
819
+ void Promise.resolve(lifecycle).catch(
820
+ function reportCurrentTTSSpeechLifecycleFailure(error) {
821
+ if (destroyed || generation !== ttsIntentGeneration) {
822
+ return;
823
+ }
824
+ pendingTTSIntent = null;
825
+ if (action === 'load') {
826
+ host.muted = true;
827
+ stopTTSPlayback();
828
+ }
829
+ reportTTSLifecycleError(error, action);
830
+ }
831
+ );
832
+ return true;
766
833
  }
767
834
 
768
835
  function reportTTSLifecycleError(error, action) {
@@ -1051,6 +1118,9 @@
1051
1118
 
1052
1119
  async function transcribeAudio(audioFile) {
1053
1120
  const generation = ++transcriptionGeneration;
1121
+ const controller = new AbortController();
1122
+ transcriptionAbortController?.abort();
1123
+ transcriptionAbortController = controller;
1054
1124
  transcriptionActive = true;
1055
1125
  localStatus = {
1056
1126
  message: 'Transcribing speech.',
@@ -1065,7 +1135,11 @@
1065
1135
  throw new Error('The selected transcription service is unavailable.');
1066
1136
  }
1067
1137
 
1068
- const transcription = await aiRuntime.fetchSTT(audioFile);
1138
+ const transcription = await aiRuntime.fetchSTT(
1139
+ audioFile,
1140
+ undefined,
1141
+ controller.signal
1142
+ );
1069
1143
  if (
1070
1144
  generation !== transcriptionGeneration
1071
1145
  || sttRole.state !== 'ready'
@@ -1086,6 +1160,9 @@
1086
1160
  }
1087
1161
  return false;
1088
1162
  } finally {
1163
+ if (transcriptionAbortController === controller) {
1164
+ transcriptionAbortController = null;
1165
+ }
1089
1166
  if (generation === transcriptionGeneration) {
1090
1167
  transcriptionActive = false;
1091
1168
  renderControls();
@@ -1126,6 +1203,8 @@
1126
1203
  || transcriptionActive;
1127
1204
  recordingGeneration += 1;
1128
1205
  transcriptionGeneration += 1;
1206
+ transcriptionAbortController?.abort();
1207
+ transcriptionAbortController = null;
1129
1208
  recordingRequested = false;
1130
1209
  transcriptionActive = false;
1131
1210
  clearSilenceTimer();
@@ -1199,6 +1278,7 @@
1199
1278
 
1200
1279
  destroyed = true;
1201
1280
  runtimeStateAbortController.abort();
1281
+ sttActivationController.destroy();
1202
1282
  window.removeEventListener('pagehide', destroy);
1203
1283
  microphonePermissionStatus?.removeEventListener?.(
1204
1284
  'change',