arcane-os 0.2.2 → 0.3.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +462 -384
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +639 -98
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +470 -0
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. package/src/workspace.mjs +1 -1
@@ -2,42 +2,602 @@ import ArcaneCommunicationBridge from './ArcaneCommunicationBridge.js';
2
2
  import CommunicationHub from './CommunicationHub.js?v=2';
3
3
  import CommunicationPreferences from './CommunicationPreferences.js';
4
4
  import {loadAndApplyTheme} from './ThemeManager.js';
5
- import {inspectMessageRecords,unavailableMessageInspection} from './MessageAdvisory.js?v=3';
5
+ import {
6
+ inspectMessageRecords,
7
+ unavailableMessageInspection
8
+ } from './MessageAdvisory.js?v=3';
6
9
 
7
- function defaults(services){return Object.fromEntries(services.map(item=>[item.id,{enabled:Boolean(item.defaultEnabled),endpoint:item.defaultEndpoint||'http://127.0.0.1:8020',accountLabel:'',status:item.defaultStatus||'Disconnected'}]));}
8
- function ready(element,event){return element.ready?Promise.resolve(element):new Promise(resolve=>element.addEventListener(event,()=>resolve(element),{once:true}));}
10
+ export const COMMUNICATION_APP_CONTROLLER_ERROR_CODES=Object.freeze({
11
+ destroyed:'ARCANE_COMMUNICATION_APP_CONTROLLER_DESTROYED'
12
+ });
13
+
14
+ function communicationAbortError(reason){
15
+ if(
16
+ reason instanceof Error
17
+ &&reason.name==='AbortError'
18
+ &&reason.code===COMMUNICATION_APP_CONTROLLER_ERROR_CODES.destroyed
19
+ ){
20
+ return reason;
21
+ }
22
+
23
+ const error=new Error('The communication application controller has been destroyed.');
24
+ error.name='AbortError';
25
+ error.code=COMMUNICATION_APP_CONTROLLER_ERROR_CODES.destroyed;
26
+ if(reason!==undefined){
27
+ error.cause=reason;
28
+ }
29
+ return error;
30
+ }
31
+
32
+ function defaults(services){
33
+ return Object.fromEntries(
34
+ services.map(function normalizeDefaultService(item){
35
+ return [
36
+ item.id,
37
+ {
38
+ enabled:Boolean(item.defaultEnabled),
39
+ endpoint:item.defaultEndpoint||'http://127.0.0.1:8020',
40
+ accountLabel:'',
41
+ status:item.defaultStatus||'Disconnected'
42
+ }
43
+ ];
44
+ })
45
+ );
46
+ }
47
+
48
+ function ready(element,event,signal){
49
+ if(element.ready){
50
+ return Promise.resolve(element);
51
+ }
52
+ if(signal.aborted){
53
+ return Promise.reject(communicationAbortError(signal.reason));
54
+ }
55
+
56
+ return new Promise(function waitForCommunicationComponent(resolve,reject){
57
+ function cleanup(){
58
+ element.removeEventListener(event,complete);
59
+ signal.removeEventListener('abort',abortWait);
60
+ }
61
+
62
+ function complete(){
63
+ cleanup();
64
+ resolve(element);
65
+ }
66
+
67
+ function abortWait(){
68
+ cleanup();
69
+ reject(communicationAbortError(signal.reason));
70
+ }
71
+
72
+ element.addEventListener(event,complete,{once:true});
73
+ signal.addEventListener('abort',abortWait,{once:true});
74
+ });
75
+ }
9
76
 
10
77
  export default class CommunicationAppController{
11
- constructor({appId,services,channels,labels={},inspectMessage=null,prepareMessageInspection=null,onAdvisoryAction=null}){this.appId=appId;this.services=services;this.channels=channels;this.labels=labels;this.inspectMessage=typeof inspectMessage==='function'?inspectMessage:null;this.prepareMessageInspection=typeof prepareMessageInspection==='function'?prepareMessageInspection:null;this.onAdvisoryAction=typeof onAdvisoryAction==='function'?onAdvisoryAction:null;this.selectionVersion=0;this.preferences=new CommunicationPreferences(appId);this.values={};this.hub=null;this.active=null;this.elements={inbox:document.querySelector('#inbox'),conversation:document.querySelector('#conversation'),settings:document.querySelector('#integrationSettings'),panel:document.querySelector('#settingsPanel'),status:document.querySelector('#appStatus')};}
78
+ constructor({
79
+ appId,
80
+ services,
81
+ channels,
82
+ labels={},
83
+ inspectMessage=null,
84
+ prepareMessageInspection=null,
85
+ onAdvisoryAction=null
86
+ }){
87
+ this.appId=appId;
88
+ this.services=services;
89
+ this.channels=channels;
90
+ this.labels=labels;
91
+ this.inspectMessage=typeof inspectMessage==='function'
92
+ ?inspectMessage
93
+ :null;
94
+ this.prepareMessageInspection=typeof prepareMessageInspection==='function'
95
+ ?prepareMessageInspection
96
+ :null;
97
+ this.onAdvisoryAction=typeof onAdvisoryAction==='function'
98
+ ?onAdvisoryAction
99
+ :null;
100
+ this.refreshVersion=0;
101
+ this.selectionVersion=0;
102
+ this.preferences=new CommunicationPreferences(appId);
103
+ this.values={};
104
+ this.hub=null;
105
+ this.active=null;
106
+ this.destroyed=false;
107
+ this.bound=false;
108
+ this.lifecycleController=new AbortController();
109
+ this.elements={
110
+ inbox:document.querySelector('#inbox'),
111
+ conversation:document.querySelector('#conversation'),
112
+ settings:document.querySelector('#integrationSettings'),
113
+ panel:document.querySelector('#settingsPanel'),
114
+ status:document.querySelector('#appStatus')
115
+ };
116
+ }
117
+
118
+ assertActive(){
119
+ if(this.destroyed||this.lifecycleController.signal.aborted){
120
+ throw communicationAbortError(this.lifecycleController.signal.reason);
121
+ }
122
+ }
123
+
12
124
  async start(){
13
- loadAndApplyTheme().catch(()=>{});await Promise.all([ready(this.elements.inbox,'unified-inbox-ready'),ready(this.elements.conversation,'conversation-view-ready'),ready(this.elements.settings,'integration-settings-ready')]);
14
- this.values=await this.preferences.load(defaults(this.services));this.bind();this.configure();await this.refresh();
125
+ this.assertActive();
126
+ loadAndApplyTheme().catch(function ignoreCommunicationThemeLoadFailure(){});
127
+ await Promise.all(
128
+ [
129
+ ready(
130
+ this.elements.inbox,
131
+ 'unified-inbox-ready',
132
+ this.lifecycleController.signal
133
+ ),
134
+ ready(
135
+ this.elements.conversation,
136
+ 'conversation-view-ready',
137
+ this.lifecycleController.signal
138
+ ),
139
+ ready(
140
+ this.elements.settings,
141
+ 'integration-settings-ready',
142
+ this.lifecycleController.signal
143
+ )
144
+ ]
145
+ );
146
+ this.assertActive();
147
+ this.values=await this.preferences.load(defaults(this.services));
148
+ this.assertActive();
149
+ this.bind();
150
+ this.configure();
151
+ await this.refresh();
15
152
  }
153
+
16
154
  bind(){
17
- document.querySelector('#openSettings').addEventListener('click',()=>this.openSettings());
18
- this.elements.settings.addEventListener('integration-settings-close',()=>this.closeSettings());
19
- this.elements.settings.addEventListener('integration-settings-save',event=>this.saveSettings(event.detail.values));
20
- this.elements.settings.addEventListener('integration-action',event=>this.action(event.detail.service));
21
- this.elements.inbox.addEventListener('inbox-refresh',()=>this.refresh());
22
- this.elements.inbox.addEventListener('thread-select',event=>this.select(event.detail.thread));
23
- this.elements.conversation.addEventListener('communication-send',event=>this.send(event.detail));
24
- this.elements.conversation.addEventListener('communication-advisory-action',event=>this.onAdvisoryAction?.(event.detail));
25
- this.elements.panel.addEventListener('click',event=>{if(event.target===this.elements.panel)this.closeSettings()});
26
- document.addEventListener('keydown',event=>{if(event.key==='Escape'&&!this.elements.panel.hidden)this.closeSettings()});
155
+ if(this.bound){
156
+ return;
157
+ }
158
+ this.assertActive();
159
+ this.bound=true;
160
+ const listenerOptions={signal:this.lifecycleController.signal};
161
+
162
+ document.querySelector('#openSettings').addEventListener(
163
+ 'click',
164
+ this.handleOpenSettings.bind(this),
165
+ listenerOptions
166
+ );
167
+ this.elements.settings.addEventListener(
168
+ 'integration-settings-close',
169
+ this.handleCloseSettings.bind(this),
170
+ listenerOptions
171
+ );
172
+ this.elements.settings.addEventListener(
173
+ 'integration-settings-save',
174
+ this.handleSaveSettings.bind(this),
175
+ listenerOptions
176
+ );
177
+ this.elements.settings.addEventListener(
178
+ 'integration-action',
179
+ this.handleIntegrationAction.bind(this),
180
+ listenerOptions
181
+ );
182
+ this.elements.inbox.addEventListener(
183
+ 'inbox-refresh',
184
+ this.handleInboxRefresh.bind(this),
185
+ listenerOptions
186
+ );
187
+ this.elements.inbox.addEventListener(
188
+ 'thread-select',
189
+ this.handleThreadSelect.bind(this),
190
+ listenerOptions
191
+ );
192
+ this.elements.conversation.addEventListener(
193
+ 'communication-send',
194
+ this.handleCommunicationSend.bind(this),
195
+ listenerOptions
196
+ );
197
+ this.elements.conversation.addEventListener(
198
+ 'communication-advisory-action',
199
+ this.handleAdvisoryAction.bind(this),
200
+ listenerOptions
201
+ );
202
+ this.elements.panel.addEventListener(
203
+ 'click',
204
+ this.handleSettingsBackdropClick.bind(this),
205
+ listenerOptions
206
+ );
207
+ document.addEventListener(
208
+ 'keydown',
209
+ this.handleDocumentKeyDown.bind(this),
210
+ listenerOptions
211
+ );
212
+ globalThis.addEventListener?.(
213
+ 'pagehide',
214
+ this.handlePageHide.bind(this),
215
+ {
216
+ once:true,
217
+ signal:this.lifecycleController.signal
218
+ }
219
+ );
220
+ }
221
+
222
+ handleOpenSettings(){
223
+ this.openSettings();
224
+ }
225
+
226
+ handleCloseSettings(){
227
+ this.closeSettings();
228
+ }
229
+
230
+ handleSaveSettings(event){
231
+ void this.saveSettings(event.detail.values);
232
+ }
233
+
234
+ handleIntegrationAction(event){
235
+ this.action(event.detail.service);
236
+ }
237
+
238
+ handleInboxRefresh(){
239
+ void this.refresh();
240
+ }
241
+
242
+ handleThreadSelect(event){
243
+ void this.select(event.detail.thread);
244
+ }
245
+
246
+ handleCommunicationSend(event){
247
+ void this.send(event.detail);
248
+ }
249
+
250
+ handleAdvisoryAction(event){
251
+ this.onAdvisoryAction?.(event.detail);
252
+ }
253
+
254
+ handleSettingsBackdropClick(event){
255
+ if(event.target===this.elements.panel){
256
+ this.closeSettings();
257
+ }
258
+ }
259
+
260
+ handleDocumentKeyDown(event){
261
+ if(event.key==='Escape'&&!this.elements.panel.hidden){
262
+ this.closeSettings();
263
+ }
264
+ }
265
+
266
+ handlePageHide(){
267
+ this.destroy();
27
268
  }
269
+
28
270
  configure(){
29
- const providers=this.services.map(item=>({id:item.id,label:item.label}));
30
- this.elements.inbox.configure({channels:this.channels,providers,threads:[]});
31
- this.elements.settings.configure({title:this.labels.settingsTitle||'Connected services',description:this.labels.settingsDescription||'Choose which services appear in this application.',services:this.services,values:this.values});
271
+ this.assertActive();
272
+ const providers=this.services.map(function presentCommunicationService(item){
273
+ return {id:item.id,label:item.label};
274
+ });
275
+ this.elements.inbox.configure(
276
+ {
277
+ channels:this.channels,
278
+ providers,
279
+ threads:[]
280
+ }
281
+ );
282
+ this.elements.settings.configure(
283
+ {
284
+ title:this.labels.settingsTitle||'Connected services',
285
+ description:this.labels.settingsDescription
286
+ ||'Choose which services appear in this application.',
287
+ services:this.services,
288
+ values:this.values
289
+ }
290
+ );
32
291
  this.rebuildHub();
33
292
  }
34
- rebuildHub(){const enabled=this.services.filter(item=>this.values[item.id]?.enabled&&item.unified!==false);const providers=enabled.map(item=>typeof item.providerFactory==='function'?item.providerFactory({service:item,value:this.values[item.id]}):new ArcaneCommunicationBridge({id:item.id,label:item.label,channels:item.channels,endpoint:this.values[item.id].endpoint||item.defaultEndpoint}));this.hub=new CommunicationHub({providers,enabledProviderIds:enabled.map(item=>item.id)});}
35
- setStatus(message,tone='muted'){this.elements.status.textContent=String(message||'');this.elements.status.dataset.tone=tone;}
36
- async refresh(){this.elements.inbox.setLoading(true);this.setStatus('Refreshing…');try{const {threads,errors}=await this.hub.refresh();this.elements.inbox.setThreads(threads);if(errors.length)this.setStatus(`${threads.length} conversations · ${errors.length} service${errors.length===1?'':'s'} need attention`,'warning');else this.setStatus(`${threads.length} conversation${threads.length===1?'':'s'}`,'success');}catch(error){this.setStatus(error.message,'error');}finally{this.elements.inbox.setLoading(false)}}
37
- async select(thread){const selection=++this.selectionVersion;this.active=thread;this.elements.inbox.setActive(thread.id);this.elements.conversation.setStatus('Loading…');try{const messages=await this.hub.messages(thread);let inspection;try{inspection=await inspectMessageRecords(messages,(message,context)=>this.inspectMessage?.(message,thread,context),{prepare:this.prepareMessageInspection?records=>this.prepareMessageInspection(records,thread):null});}catch{inspection=unavailableMessageInspection(messages);}if(selection!==this.selectionVersion)return;const {advisories,failures}=inspection;this.elements.conversation.setConversation(thread,messages,{advisories});const warnings=advisories.size-failures;this.elements.conversation.setStatus(failures?`${failures} message safety check${failures===1?' is':'s are'} unavailable. Review manually before replying.`:warnings?`${warnings} message${warnings===1?' has':'s have'} a safety warning. Review before replying.`:'');}catch(error){if(selection!==this.selectionVersion)return;this.elements.conversation.setConversation(thread,[]);this.elements.conversation.setStatus(error.message);}}
38
- async send({thread,body}){this.elements.conversation.setBusy(true);this.elements.conversation.setStatus('Sending…');try{await this.hub.send({providerId:thread.providerId,threadId:thread.id,channel:thread.channel,body});this.elements.conversation.clearComposer();await this.select(thread);const service=this.services.find(item=>item.id===thread.providerId);this.elements.conversation.setStatus(service?.simulated?'Simulated locally. Nothing left this device.':'Sent.');}catch(error){this.elements.conversation.setStatus(error.message);}finally{this.elements.conversation.setBusy(false)}}
39
- openSettings(){this.elements.settings.configure({title:this.labels.settingsTitle,description:this.labels.settingsDescription,services:this.services,values:this.values});this.elements.panel.hidden=false;document.body.classList.add('modal-open');}
40
- closeSettings(){this.elements.panel.hidden=true;document.body.classList.remove('modal-open');}
41
- async saveSettings(values){try{this.values=await this.preferences.save(values);this.rebuildHub();this.elements.settings.setStatus('Services saved.');this.closeSettings();await this.refresh();}catch(error){this.elements.settings.setStatus(error.message)}}
42
- action(service){if(service.externalUrl){const opened=globalThis.open(service.externalUrl,'_blank','noopener,noreferrer');if(!opened)this.elements.settings.setStatus(`Open ${service.externalUrl} to continue.`);return}this.elements.settings.setStatus(`${service.label} connects through the Arcane communications bridge.`)}
293
+
294
+ rebuildHub(){
295
+ this.assertActive();
296
+ const enabled=this.services.filter(
297
+ function selectEnabledCommunicationService(item){
298
+ return this.values[item.id]?.enabled&&item.unified!==false;
299
+ },
300
+ this
301
+ );
302
+ const providers=enabled.map(
303
+ function createCommunicationProvider(item){
304
+ return typeof item.providerFactory==='function'
305
+ ?item.providerFactory(
306
+ {
307
+ service:item,
308
+ value:this.values[item.id]
309
+ }
310
+ )
311
+ :new ArcaneCommunicationBridge(
312
+ {
313
+ id:item.id,
314
+ label:item.label,
315
+ channels:item.channels,
316
+ endpoint:this.values[item.id].endpoint
317
+ ||item.defaultEndpoint
318
+ }
319
+ );
320
+ },
321
+ this
322
+ );
323
+ const replacement=new CommunicationHub(
324
+ {
325
+ providers,
326
+ enabledProviderIds:enabled.map(
327
+ function communicationProviderId(item){
328
+ return item.id;
329
+ }
330
+ )
331
+ }
332
+ );
333
+ this.elements.conversation.setBusy(false);
334
+ const previous=this.hub;
335
+ this.hub=replacement;
336
+ this.active=null;
337
+ this.refreshVersion+=1;
338
+ this.selectionVersion+=1;
339
+ this.disposeHubInstance(previous);
340
+ return replacement;
341
+ }
342
+
343
+ disposeHub(){
344
+ const hub=this.hub;
345
+ this.hub=null;
346
+ this.active=null;
347
+ this.refreshVersion+=1;
348
+ this.selectionVersion+=1;
349
+ this.disposeHubInstance(hub);
350
+ }
351
+
352
+ disposeHubInstance(hub){
353
+ if(!hub){
354
+ return;
355
+ }
356
+ try{
357
+ if(typeof hub.destroy==='function'){
358
+ hub.destroy();
359
+ }else if(typeof hub.dispose==='function'){
360
+ hub.dispose();
361
+ }
362
+ }catch(error){
363
+ console.error('The communication hub could not be disposed cleanly.',error);
364
+ }
365
+ }
366
+
367
+ setStatus(message,tone='muted'){
368
+ if(this.destroyed){
369
+ return;
370
+ }
371
+ this.elements.status.textContent=String(message||'');
372
+ this.elements.status.dataset.tone=tone;
373
+ }
374
+
375
+ async refresh(){
376
+ this.assertActive();
377
+ const hub=this.hub;
378
+ const refresh=++this.refreshVersion;
379
+ this.elements.inbox.setLoading(true);
380
+ this.setStatus('Refreshing…');
381
+ try{
382
+ const {threads,errors}=await hub.refresh(
383
+ {signal:this.lifecycleController.signal}
384
+ );
385
+ this.assertActive();
386
+ if(this.hub!==hub||refresh!==this.refreshVersion){
387
+ return false;
388
+ }
389
+ this.elements.inbox.setThreads(threads);
390
+ if(errors.length){
391
+ this.setStatus(
392
+ `${threads.length} conversations · ${errors.length} service${errors.length===1?'':'s'} need attention`,
393
+ 'warning'
394
+ );
395
+ }else{
396
+ this.setStatus(
397
+ `${threads.length} conversation${threads.length===1?'':'s'}`,
398
+ 'success'
399
+ );
400
+ }
401
+ return true;
402
+ }catch(error){
403
+ if(
404
+ error?.name!=='AbortError'
405
+ &&!this.destroyed
406
+ &&this.hub===hub
407
+ &&refresh===this.refreshVersion
408
+ ){
409
+ this.setStatus(error.message,'error');
410
+ }
411
+ return false;
412
+ }finally{
413
+ if(
414
+ !this.destroyed
415
+ &&this.hub===hub
416
+ &&refresh===this.refreshVersion
417
+ ){
418
+ this.elements.inbox.setLoading(false);
419
+ }
420
+ }
421
+ }
422
+
423
+ async select(thread){
424
+ this.assertActive();
425
+ const hub=this.hub;
426
+ const selection=++this.selectionVersion;
427
+ this.active=thread;
428
+ this.elements.inbox.setActive(thread.id);
429
+ this.elements.conversation.setStatus('Loading…');
430
+ try{
431
+ const messages=await hub.messages(thread);
432
+ this.assertActive();
433
+ let inspection;
434
+ try{
435
+ inspection=await inspectMessageRecords(
436
+ messages,
437
+ function inspectCommunicationMessage(message,context){
438
+ return this.inspectMessage?.(message,thread,context);
439
+ }.bind(this),
440
+ {
441
+ prepare:this.prepareMessageInspection
442
+ ?function prepareCommunicationMessages(records){
443
+ return this.prepareMessageInspection(records,thread);
444
+ }.bind(this)
445
+ :null
446
+ }
447
+ );
448
+ }catch{
449
+ inspection=unavailableMessageInspection(messages);
450
+ }
451
+ this.assertActive();
452
+ if(selection!==this.selectionVersion||this.hub!==hub){
453
+ return false;
454
+ }
455
+ const {advisories,failures}=inspection;
456
+ this.elements.conversation.setConversation(
457
+ thread,
458
+ messages,
459
+ {advisories}
460
+ );
461
+ const warnings=advisories.size-failures;
462
+ this.elements.conversation.setStatus(
463
+ failures
464
+ ?`${failures} message safety check${failures===1?' is':'s are'} unavailable. Review manually before replying.`
465
+ :warnings
466
+ ?`${warnings} message${warnings===1?' has':'s have'} a safety warning. Review before replying.`
467
+ :''
468
+ );
469
+ return true;
470
+ }catch(error){
471
+ if(
472
+ error?.name!=='AbortError'
473
+ &&!this.destroyed
474
+ &&selection===this.selectionVersion
475
+ &&this.hub===hub
476
+ ){
477
+ this.elements.conversation.setConversation(thread,[]);
478
+ this.elements.conversation.setStatus(error.message);
479
+ }
480
+ return false;
481
+ }
482
+ }
483
+
484
+ async send({thread,body}){
485
+ this.assertActive();
486
+ const hub=this.hub;
487
+ this.elements.conversation.setBusy(true);
488
+ this.elements.conversation.setStatus('Sending…');
489
+ try{
490
+ await hub.send(
491
+ {
492
+ providerId:thread.providerId,
493
+ threadId:thread.id,
494
+ channel:thread.channel,
495
+ body
496
+ }
497
+ );
498
+ this.assertActive();
499
+ if(this.hub!==hub){
500
+ return false;
501
+ }
502
+ this.elements.conversation.clearComposer();
503
+ await this.select(thread);
504
+ this.assertActive();
505
+ if(this.hub!==hub){
506
+ return false;
507
+ }
508
+ const service=this.services.find(
509
+ function matchSentCommunicationService(item){
510
+ return item.id===thread.providerId;
511
+ }
512
+ );
513
+ this.elements.conversation.setStatus(
514
+ service?.simulated
515
+ ?'Simulated locally. Nothing left this device.'
516
+ :'Sent.'
517
+ );
518
+ return true;
519
+ }catch(error){
520
+ if(error?.name!=='AbortError'&&!this.destroyed&&this.hub===hub){
521
+ this.elements.conversation.setStatus(error.message);
522
+ }
523
+ return false;
524
+ }finally{
525
+ if(!this.destroyed&&this.hub===hub){
526
+ this.elements.conversation.setBusy(false);
527
+ }
528
+ }
529
+ }
530
+
531
+ openSettings(){
532
+ if(this.destroyed){
533
+ return;
534
+ }
535
+ this.elements.settings.configure(
536
+ {
537
+ title:this.labels.settingsTitle,
538
+ description:this.labels.settingsDescription,
539
+ services:this.services,
540
+ values:this.values
541
+ }
542
+ );
543
+ this.elements.panel.hidden=false;
544
+ document.body.classList.add('modal-open');
545
+ }
546
+
547
+ closeSettings(){
548
+ this.elements.panel.hidden=true;
549
+ document.body.classList.remove('modal-open');
550
+ }
551
+
552
+ async saveSettings(values){
553
+ try{
554
+ this.assertActive();
555
+ this.values=await this.preferences.save(values);
556
+ this.assertActive();
557
+ this.rebuildHub();
558
+ this.elements.settings.setStatus('Services saved.');
559
+ this.closeSettings();
560
+ await this.refresh();
561
+ }catch(error){
562
+ if(error?.name!=='AbortError'&&!this.destroyed){
563
+ this.elements.settings.setStatus(error.message);
564
+ }
565
+ }
566
+ }
567
+
568
+ action(service){
569
+ if(this.destroyed){
570
+ return;
571
+ }
572
+ if(service.externalUrl){
573
+ const opened=globalThis.open(
574
+ service.externalUrl,
575
+ '_blank',
576
+ 'noopener,noreferrer'
577
+ );
578
+ if(!opened){
579
+ this.elements.settings.setStatus(
580
+ `Open ${service.externalUrl} to continue.`
581
+ );
582
+ }
583
+ return;
584
+ }
585
+ this.elements.settings.setStatus(
586
+ `${service.label} connects through the Arcane communications bridge.`
587
+ );
588
+ }
589
+
590
+ destroy(){
591
+ if(this.destroyed){
592
+ return false;
593
+ }
594
+ const reason=communicationAbortError();
595
+ this.destroyed=true;
596
+ this.lifecycleController.abort(reason);
597
+ this.disposeHub();
598
+ this.bound=false;
599
+ this.elements.panel.hidden=true;
600
+ document.body.classList.remove('modal-open');
601
+ return true;
602
+ }
43
603
  }