arcane-os 0.2.3 → 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 +17 -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 +20 -20
  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 +2 -2
  21. package/docs/reference/behavioral-testing.md +28 -5
  22. package/docs/reference/cli.md +115 -7
  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 +86 -37
  27. package/docs/reference/inventory/runtime-modules.json +126 -49
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +143 -33
  30. package/docs/reference/runtime-components.md +192 -80
  31. package/docs/reference/runtime-modules.md +584 -70
  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 +453 -131
  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 +244 -72
  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 +235 -37
  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
@@ -60,7 +60,29 @@
60
60
  </section>
61
61
 
62
62
  <script type="module">
63
+ const {
64
+ createArcaneEventSource,
65
+ projectArcaneDOMEvent
66
+ }=await import('arcane-os/event-manager');
67
+
63
68
  const host=this;
69
+ const events=createArcaneEventSource(
70
+ host,
71
+ {
72
+ source:'arcane.component.voice-transcription',
73
+ eventTypes:[
74
+ 'voice-transcription-ready',
75
+ 'voice-transcription-state',
76
+ 'voice-transcription-segment',
77
+ 'voice-transcription-change',
78
+ 'voice-transcription-complete',
79
+ 'speech-transcription-complete',
80
+ 'speech-transcription-cancelled',
81
+ 'speech-stt-activation-request',
82
+ 'speech-stt-activation-error'
83
+ ]
84
+ }
85
+ );
64
86
  const componentURL=new URL(
65
87
  host.getAttribute('href')||'./arcane/components/voice-transcription.html',
66
88
  document.baseURI
@@ -94,9 +116,72 @@
94
116
  let transcript='';
95
117
  let sessionGeneration=0;
96
118
  let transcriptionAbortController=null;
119
+ let eventOperationSequence=0;
120
+ let voiceOperationId=null;
97
121
  let destroyed=false;
98
122
  let options=normalizeVoiceOptions(optionsFromDataset());
99
123
 
124
+ function nextVoiceOperationId(kind){
125
+ eventOperationSequence+=1;
126
+ return `${events.descriptor.instanceId}:${kind}:${eventOperationSequence}`;
127
+ }
128
+
129
+ function currentVoiceOperationId(kind='transcription'){
130
+ voiceOperationId=voiceOperationId||nextVoiceOperationId(kind);
131
+ return voiceOperationId;
132
+ }
133
+
134
+ function canonicalSTTCancellationReason(reason){
135
+ if(reason==='runtime-unready'){
136
+ return 'stt-role-unready';
137
+ }
138
+ if(reason==='stt-provider-request-cancelled'){
139
+ return 'stt-provider-transcription-cancelled';
140
+ }
141
+ return String(reason||'stt-transcription-cancelled');
142
+ }
143
+
144
+ function dispatchVoiceEvent(
145
+ type,
146
+ compatibilityDetail={},
147
+ {
148
+ publicDetail={},
149
+ operationId=null,
150
+ beginOperation=null,
151
+ bubbles=false,
152
+ composed=false,
153
+ cancelable=false
154
+ }={}
155
+ ){
156
+ if(destroyed&&type!=='speech-transcription-cancelled'){
157
+ return false;
158
+ }
159
+ if(beginOperation!==null){
160
+ voiceOperationId=nextVoiceOperationId(beginOperation);
161
+ }
162
+ const publication=events.dispatch(
163
+ type,
164
+ compatibilityDetail,
165
+ {
166
+ publicDetail,
167
+ operationId:operationId||currentVoiceOperationId(),
168
+ cancelable
169
+ }
170
+ );
171
+ if(!publication.accepted){
172
+ return false;
173
+ }
174
+ return projectArcaneDOMEvent(
175
+ host,
176
+ publication.occurrence,
177
+ {
178
+ bubbles,
179
+ composed,
180
+ cancelable
181
+ }
182
+ );
183
+ }
184
+
100
185
  host.clear=clear;
101
186
  host.completeTranscription=completeStream;
102
187
  host.configure=configure;
@@ -118,16 +203,24 @@
118
203
  }
119
204
  );
120
205
 
121
- startButton.addEventListener('click',startRecording);
122
- stopButton.addEventListener('click',stopRecording);
123
- completeButton.addEventListener('click',completeStream);
124
- window.addEventListener('pagehide',destroy,{once:true});
206
+ const lifecycleListenerOptions=Object.freeze(
207
+ {signal:runtimeStateAbortController.signal}
208
+ );
209
+ startButton.addEventListener('click',startRecording,lifecycleListenerOptions);
210
+ stopButton.addEventListener('click',stopRecording,lifecycleListenerOptions);
211
+ completeButton.addEventListener('click',completeStream,lifecycleListenerOptions);
212
+ window.addEventListener(
213
+ 'pagehide',
214
+ destroy,
215
+ {once:true,signal:runtimeStateAbortController.signal}
216
+ );
125
217
 
126
218
  const sttActivationController=createSTTActivationController(
127
219
  {
128
220
  host,
129
221
  button:sttActivationButton,
130
- onChange:renderState
222
+ onChange:renderState,
223
+ eventSource:events
131
224
  }
132
225
  );
133
226
 
@@ -178,8 +271,20 @@
178
271
  }
179
272
 
180
273
  function configure(input={}){
274
+ if(destroyed){
275
+ return getOptions();
276
+ }
181
277
  const setInitial=Object.prototype.hasOwnProperty.call(input||{},'initialValue');
182
- options=normalizeVoiceOptions(input,options);
278
+ const nextOptions=normalizeVoiceOptions(input,options);
279
+ if(!setInitial){
280
+ if(supersedeForConfigurationReplacement()){
281
+ reportSTTCancellation(
282
+ 'configuration-replaced',
283
+ 'Transcription configuration replaced.'
284
+ );
285
+ }
286
+ }
287
+ options=nextOptions;
183
288
  renderOptions();
184
289
  if(setInitial){
185
290
  setValue(options.initialValue);
@@ -203,7 +308,9 @@
203
308
 
204
309
  const generation=++sessionGeneration;
205
310
  chunks=[];
206
- setState('starting',options.messages.requesting);
311
+ setState('starting',options.messages.requesting,{
312
+ beginOperation:'transcription'
313
+ });
207
314
  if(!recordingStartIsCurrent(generation,'starting')){
208
315
  return rejectRecordingStart(generation);
209
316
  }
@@ -293,7 +400,7 @@
293
400
  }
294
401
 
295
402
  function stopRecording(){
296
- if(state!=='recording'||recorder?.state!=='recording'){
403
+ if(destroyed||state!=='recording'||recorder?.state!=='recording'){
297
404
  return false;
298
405
  }
299
406
  const generation=sessionGeneration;
@@ -321,6 +428,15 @@
321
428
  setState('error',options.messages.emptyAudio);
322
429
  return false;
323
430
  }
431
+ if(sttRole.state!=='ready'||sttRole.busy){
432
+ reportSTTCancellation(
433
+ sttRole.busy?'stt-role-busy':'runtime-unready',
434
+ sttRole.busy
435
+ ?'Transcription stopped because the selected service is busy.'
436
+ :'Transcription stopped because the selected service became unavailable.'
437
+ );
438
+ return false;
439
+ }
324
440
 
325
441
  try{
326
442
  controller=new AbortController();
@@ -345,6 +461,7 @@
345
461
  if(controller.signal.aborted
346
462
  ||generation!==sessionGeneration
347
463
  ||sttRole.state!=='ready'
464
+ ||sttRole.busy
348
465
  ||destroyed){
349
466
  return false;
350
467
  }
@@ -393,7 +510,8 @@
393
510
  detail,
394
511
  options.persist
395
512
  ?options.messages.saved
396
- :options.messages.transcribed
513
+ :options.messages.transcribed,
514
+ currentVoiceOperationId()
397
515
  );
398
516
  }catch(error){
399
517
  if(generation!==sessionGeneration||destroyed){
@@ -419,25 +537,40 @@
419
537
  generation,
420
538
  expectedState,
421
539
  detail,
422
- message
540
+ message,
541
+ operationId=null
423
542
  ){
424
543
  if(!isCurrentVoiceOperation(generation,expectedState)){
425
544
  return false;
426
545
  }
427
- host.dispatchEvent(
428
- new CustomEvent(
429
- 'speech-transcription-complete',
430
- {bubbles:true,composed:true,detail}
431
- )
546
+ dispatchVoiceEvent(
547
+ 'speech-transcription-complete',
548
+ detail,
549
+ {
550
+ bubbles:true,
551
+ composed:true,
552
+ operationId,
553
+ publicDetail:{
554
+ segmentCharacterCount:detail.text.length,
555
+ transcriptCharacterCount:detail.transcript.length
556
+ }
557
+ }
432
558
  );
433
559
  if(!isCurrentVoiceOperation(generation,expectedState)){
434
560
  return false;
435
561
  }
436
- host.dispatchEvent(
437
- new CustomEvent(
438
- 'voice-transcription-segment',
439
- {bubbles:true,composed:true,detail}
440
- )
562
+ dispatchVoiceEvent(
563
+ 'voice-transcription-segment',
564
+ detail,
565
+ {
566
+ bubbles:true,
567
+ composed:true,
568
+ operationId,
569
+ publicDetail:{
570
+ segmentCharacterCount:detail.text.length,
571
+ transcriptCharacterCount:detail.transcript.length
572
+ }
573
+ }
441
574
  );
442
575
  if(!isCurrentVoiceOperation(generation,expectedState)){
443
576
  return false;
@@ -459,10 +592,12 @@
459
592
  return options.transcribe(file,context);
460
593
  }
461
594
  if(typeof globalThis.ai?.fetchSTT!=='function'){
462
- await import(moduleURL('AI.js?v=7'));
595
+ await import(moduleURL('AI.js'));
463
596
  }
464
597
  if(typeof globalThis.ai?.fetchSTT!=='function'){
465
- throw new Error('Speech transcription is not configured.');
598
+ const error=new Error('Speech transcription is not configured.');
599
+ error.code='ARCANE_VOICE_TRANSCRIPTION_STT_RUNTIME_METHOD_UNAVAILABLE';
600
+ throw error;
466
601
  }
467
602
  return globalThis.ai.fetchSTT(file,undefined,signal);
468
603
  }
@@ -510,7 +645,9 @@
510
645
  const complete=options.onComplete||(
511
646
  typeof host.complete==='function'?host.complete.bind(host):null
512
647
  );
513
- setState('completing',options.messages.completing);
648
+ setState('completing',options.messages.completing,{
649
+ beginOperation:'completion'
650
+ });
514
651
  if(!isCurrentVoiceOperation(generation,'completing')){
515
652
  return false;
516
653
  }
@@ -521,15 +658,16 @@
521
658
  if(!isCurrentVoiceOperation(generation,'completing')){
522
659
  return false;
523
660
  }
524
- host.dispatchEvent(
525
- new CustomEvent(
526
- 'voice-transcription-complete',
527
- {
528
- bubbles:true,
529
- composed:true,
530
- detail:{transcript:completionTranscript}
661
+ dispatchVoiceEvent(
662
+ 'voice-transcription-complete',
663
+ {transcript:completionTranscript},
664
+ {
665
+ bubbles:true,
666
+ composed:true,
667
+ publicDetail:{
668
+ transcriptCharacterCount:completionTranscript.length
531
669
  }
532
- )
670
+ }
533
671
  );
534
672
  if(!isCurrentVoiceOperation(generation,'completing')){
535
673
  return false;
@@ -563,7 +701,28 @@
563
701
  return true;
564
702
  }
565
703
 
704
+ function supersedeForConfigurationReplacement(){
705
+ const hadActiveOperation=[
706
+ 'starting',
707
+ 'recording',
708
+ 'transcribing',
709
+ 'saving',
710
+ 'completing'
711
+ ].includes(state)||Boolean(transcriptionAbortController);
712
+ if(!hadActiveOperation){
713
+ return false;
714
+ }
715
+ sessionGeneration+=1;
716
+ transcriptionAbortController?.abort();
717
+ transcriptionAbortController=null;
718
+ releaseMicrophone();
719
+ return true;
720
+ }
721
+
566
722
  function setValue(value=''){
723
+ if(destroyed){
724
+ return transcript;
725
+ }
567
726
  const operationSuperseded=
568
727
  supersedeForTranscriptReplacement();
569
728
  transcript=`${value??''}`;
@@ -576,24 +735,23 @@
576
735
  }else if(state!=='recording'){
577
736
  setState('idle',options.messages.ready);
578
737
  }
579
- if(destroyed){
580
- return transcript;
581
- }
582
- host.dispatchEvent(
583
- new CustomEvent(
584
- 'voice-transcription-change',
585
- {
586
- bubbles:true,
587
- composed:true,
588
- detail:{transcript}
738
+ dispatchVoiceEvent(
739
+ 'voice-transcription-change',
740
+ {transcript},
741
+ {
742
+ beginOperation:'transcript-change',
743
+ bubbles:true,
744
+ composed:true,
745
+ publicDetail:{
746
+ transcriptCharacterCount:transcript.length
589
747
  }
590
- )
748
+ }
591
749
  );
592
750
  return transcript;
593
751
  }
594
752
 
595
753
  function clear(){
596
- if(!['complete','error','idle'].includes(state)){
754
+ if(destroyed||!['complete','error','idle'].includes(state)){
597
755
  return false;
598
756
  }
599
757
  transcript='';
@@ -646,10 +804,17 @@
646
804
  }
647
805
  }
648
806
 
649
- function setState(nextState,message=''){
807
+ function setState(nextState,message='',{beginOperation=null}={}){
808
+ if(destroyed){
809
+ return false;
810
+ }
811
+ if(beginOperation!==null){
812
+ voiceOperationId=nextVoiceOperationId(beginOperation);
813
+ }
650
814
  state=nextState;
651
815
  stateMessage=message||nextState;
652
816
  renderState();
817
+ return true;
653
818
  }
654
819
 
655
820
  function synchronizeAIRuntimeState(snapshot){
@@ -683,18 +848,18 @@
683
848
  return true;
684
849
  }
685
850
 
686
- function reportSTTCancellation(reason,message){
851
+ function reportSTTCancellation(reason,message,operationId=null){
687
852
  state='idle';
688
853
  stateMessage=message;
689
- host.dispatchEvent(
690
- new CustomEvent(
691
- 'speech-transcription-cancelled',
692
- {
693
- bubbles:true,
694
- composed:true,
695
- detail:Object.freeze({reason})
696
- }
697
- )
854
+ dispatchVoiceEvent(
855
+ 'speech-transcription-cancelled',
856
+ Object.freeze({reason}),
857
+ {
858
+ bubbles:true,
859
+ composed:true,
860
+ operationId:operationId||currentVoiceOperationId(),
861
+ publicDetail:{reason:canonicalSTTCancellationReason(reason)}
862
+ }
698
863
  );
699
864
  if(!destroyed){
700
865
  renderState();
@@ -738,21 +903,23 @@
738
903
  ||['loading','unloading'].includes(sttRole.state)
739
904
  )
740
905
  );
741
- host.dispatchEvent(
742
- new CustomEvent(
743
- 'voice-transcription-state',
906
+ dispatchVoiceEvent(
907
+ 'voice-transcription-state',
908
+ Object.freeze(
744
909
  {
745
- bubbles:true,
746
- composed:true,
747
- detail:Object.freeze(
748
- {
749
- message:status.innerText,
750
- state,
751
- stt:sttRole
752
- }
753
- )
910
+ message:status.innerText,
911
+ state,
912
+ stt:sttRole
754
913
  }
755
- )
914
+ ),
915
+ {
916
+ bubbles:true,
917
+ composed:true,
918
+ publicDetail:{
919
+ state,
920
+ sttState:sttRole.state
921
+ }
922
+ }
756
923
  );
757
924
  }
758
925
 
@@ -820,6 +987,7 @@
820
987
  stateMessage='Transcription unavailable.';
821
988
  host.ready=false;
822
989
  renderState();
990
+ events.dispose();
823
991
  return true;
824
992
  }
825
993
 
@@ -835,11 +1003,15 @@
835
1003
  }
836
1004
  if(!destroyed){
837
1005
  host.ready=true;
838
- host.dispatchEvent(
839
- new CustomEvent(
840
- 'voice-transcription-ready',
841
- {bubbles:true,composed:true}
842
- )
1006
+ dispatchVoiceEvent(
1007
+ 'voice-transcription-ready',
1008
+ {},
1009
+ {
1010
+ beginOperation:'ready',
1011
+ bubbles:true,
1012
+ composed:true,
1013
+ publicDetail:{ready:true}
1014
+ }
843
1015
  );
844
1016
  }
845
1017
  </script>
@@ -1,8 +1,10 @@
1
1
  <link rel="stylesheet" href="./arcane/css/theme.css?v=1"><link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
2
  <style>:host{display:block}.widget{display:grid;gap:var(--arcane-space-4)}.current{align-items:center;display:grid;gap:var(--arcane-space-4);grid-template-columns:auto 1fr auto}.symbol{font-size:clamp(2.5rem,8vw,5rem);line-height:1}.temperature{font-size:clamp(2.2rem,7vw,4.5rem);font-weight:760;line-height:1}.location h2,.location p{margin:0}.location p,.details{color:var(--arcane-muted)}.details{display:grid;gap:var(--arcane-space-1);text-align:right}.forecast{display:grid;gap:var(--arcane-space-2);grid-template-columns:repeat(7,minmax(5.5rem,1fr));overflow:auto}.day{display:grid;gap:var(--arcane-space-2);padding:var(--arcane-space-3);text-align:center;background:var(--arcane-surface-muted);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-medium)}.day strong{white-space:nowrap}.day-symbol{font-size:1.8rem}.empty{padding:var(--arcane-space-6);text-align:center;color:var(--arcane-muted)}.footer{display:flex;align-items:center;justify-content:space-between;gap:var(--arcane-space-3)}@media(max-width:36rem){.current{grid-template-columns:auto 1fr}.details{grid-column:1/-1;text-align:left;grid-template-columns:repeat(3,1fr)}}</style>
3
3
  <section class="widget arcane-card"><div id="empty" class="empty"><strong>Weather is ready for a location</strong><p>Search for a city or postal code to load current conditions and the forecast.</p></div><div id="content" hidden><div class="arcane-card__body current"><span id="symbol" class="symbol" aria-hidden="true"></span><div class="location"><h2 id="location" class="arcane-section-heading"></h2><p id="condition"></p><div><span id="temperature" class="temperature"></span></div></div><div class="details"><span id="feels"></span><span id="humidity"></span><span id="wind"></span></div></div><div id="forecast" class="arcane-card__body forecast"></div><footer class="arcane-card__footer footer"><span id="source" class="arcane-help"></span><button id="refresh" class="arcane-button arcane-button--secondary" type="button">Refresh</button></footer></div></section>
4
- <script type="module">const host=this,root=host.shadowRoot,empty=root.querySelector('#empty'),content=root.querySelector('#content'),forecast=root.querySelector('#forecast');let weather=null;const conditions=new Map([[0,['Clear','☀️']],[1,['Mostly clear','🌤️']],[2,['Partly cloudy','⛅']],[3,['Overcast','☁️']],[45,['Fog','🌫️']],[48,['Freezing fog','🌫️']],[51,['Light drizzle','🌦️']],[53,['Drizzle','🌦️']],[55,['Heavy drizzle','🌧️']],[61,['Light rain','🌦️']],[63,['Rain','🌧️']],[65,['Heavy rain','🌧️']],[71,['Light snow','🌨️']],[73,['Snow','🌨️']],[75,['Heavy snow','❄️']],[80,['Rain showers','🌦️']],[81,['Rain showers','🌧️']],[82,['Heavy showers','⛈️']],[95,['Thunderstorm','⛈️']],[96,['Thunderstorm with hail','⛈️']],[99,['Severe thunderstorm','⛈️']]]);
5
- host.setWeather=setWeather;host.clear=()=>{weather=null;empty.hidden=false;content.hidden=true};root.querySelector('#refresh').addEventListener('click',()=>host.dispatchEvent(new CustomEvent('weather-refresh',{bubbles:true,composed:true,detail:{weather}})));
4
+ <script type="module">const host=this,root=host.shadowRoot,empty=root.querySelector('#empty'),content=root.querySelector('#content'),forecast=root.querySelector('#forecast');const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.weather-widget',eventTypes:['weather-refresh','weather-widget-ready']});let weather=null,destroyed=false;const conditions=new Map([[0,['Clear','☀️']],[1,['Mostly clear','🌤️']],[2,['Partly cloudy','⛅']],[3,['Overcast','☁️']],[45,['Fog','🌫️']],[48,['Freezing fog','🌫️']],[51,['Light drizzle','🌦️']],[53,['Drizzle','🌦️']],[55,['Heavy drizzle','🌧️']],[61,['Light rain','🌦️']],[63,['Rain','🌧️']],[65,['Heavy rain','🌧️']],[71,['Light snow','🌨️']],[73,['Snow','🌨️']],[75,['Heavy snow','❄️']],[80,['Rain showers','🌦️']],[81,['Rain showers','🌧️']],[82,['Heavy showers','⛈️']],[95,['Thunderstorm','⛈️']],[96,['Thunderstorm with hail','⛈️']],[99,['Severe thunderstorm','⛈️']]]);
5
+ function emit(type,detail={},publicDetail={}){if(destroyed)return false;const publication=events.dispatch(type,detail,{publicDetail});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
6
+ host.destroy=()=>{if(destroyed)return false;destroyed=true;host.ready=false;events.dispose();return true};
7
+ host.setWeather=setWeather;host.clear=()=>{weather=null;empty.hidden=false;content.hidden=true};root.querySelector('#refresh').addEventListener('click',()=>emit('weather-refresh',{weather},{hasWeather:Boolean(weather)}));
6
8
  function info(code,isDay=true){const value=conditions.get(Number(code))||['Conditions unavailable','🌡️'];if(!isDay&&Number(code)<=1)return ['Clear night','🌙'];return value}
7
9
  function setWeather(value){weather=value;if(!value)return host.clear();empty.hidden=true;content.hidden=false;const current=value.current,[label,symbol]=info(current.weatherCode,current.isDay);root.querySelector('#symbol').textContent=symbol;root.querySelector('#location').textContent=[value.location.name,value.location.region,value.location.country].filter(Boolean).join(', ');root.querySelector('#condition').textContent=label;root.querySelector('#temperature').textContent=`${Math.round(current.temperature)}${current.temperatureUnit}`;root.querySelector('#feels').textContent=`Feels ${Math.round(current.apparentTemperature)}${current.temperatureUnit}`;root.querySelector('#humidity').textContent=`Humidity ${Math.round(current.humidity)}%`;root.querySelector('#wind').textContent=`Wind ${Math.round(current.windSpeed)} ${current.windUnit}`;root.querySelector('#source').textContent=`${value.source||'Weather source'} · updated ${new Intl.DateTimeFormat(undefined,{timeStyle:'short'}).format(new Date(value.fetchedAt))}`;forecast.replaceChildren(...Array.from(value.daily||[]).map(day=>{const section=document.createElement('section'),name=document.createElement('strong'),icon=document.createElement('span'),temperatures=document.createElement('span'),rain=document.createElement('span');section.className='day';name.textContent=new Intl.DateTimeFormat(undefined,{weekday:'short'}).format(new Date(day.date));icon.className='day-symbol';icon.textContent=info(day.weatherCode)[1];temperatures.textContent=`${Math.round(day.temperatureMax)}° / ${Math.round(day.temperatureMin)}°`;rain.textContent=`${Math.round(day.precipitationProbability)}% rain`;section.append(name,icon,temperatures,rain);return section}));return weather}
8
- host.ready=true;host.dispatchEvent(new CustomEvent('weather-widget-ready',{bubbles:true,composed:true}));</script>
10
+ host.ready=true;emit('weather-widget-ready',{}, {ready:true});</script>
@@ -61,6 +61,8 @@ button{min-width:2.5rem}
61
61
  <script type="module">
62
62
  const host=this;
63
63
  const root=host.shadowRoot;
64
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
65
+ const events=createArcaneEventSource(host,{source:'arcane.component.web-navigator',eventTypes:['web-navigation-blocked','web-navigate','web-open-external','web-navigator-ready']});
64
66
  const address=root.querySelector('#address');
65
67
  const frame=root.querySelector('#frame');
66
68
  const blocked=root.querySelector('#blocked');
@@ -72,6 +74,20 @@ let history=[];
72
74
  let position=-1;
73
75
  let navigationGuard=null;
74
76
  let navigationSequence=0;
77
+ let eventOperationSequence=0;
78
+ let destroyed=false;
79
+
80
+ function emit(type,detail={},publicDetail={}){
81
+ if(destroyed)return false;
82
+ const operationId=
83
+ `${events.instanceId}:${type}:${(++eventOperationSequence).toString(36)}`;
84
+ const publication=events.dispatch(
85
+ type,
86
+ detail,
87
+ {operationId,publicDetail:{...publicDetail,reason:type}}
88
+ );
89
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
90
+ }
75
91
 
76
92
  function normalize(value){
77
93
  let text=String(value||'').trim();
@@ -143,7 +159,7 @@ function showBlocked(url,decision){
143
159
  root.querySelector('#blockedEvidence').textContent=[decision.ruleId&&`Rule ${decision.ruleId}`,decision.policyGeneration&&`Policy generation ${decision.policyGeneration}`,decision.decisionId&&`Decision ${decision.decisionId}`,decision.decidedAt].filter(Boolean).join(' · ');
144
160
  status.textContent='Blocked by the Arcane global deny policy.';
145
161
  blocked.focus();
146
- host.dispatchEvent(new CustomEvent('web-navigation-blocked',{bubbles:true,composed:true,detail:{url,decision}}));
162
+ emit('web-navigation-blocked',{url,decision},{blocked:true,ruleId:String(decision.ruleId||''),policyGeneration:decision.policyGeneration??null});
147
163
  }
148
164
 
149
165
  function showFrame(url){
@@ -151,17 +167,18 @@ function showFrame(url){
151
167
  frame.hidden=false;
152
168
  frame.src=url;
153
169
  status.textContent='Loading…';
154
- host.dispatchEvent(new CustomEvent('web-navigate',{bubbles:true,composed:true,detail:{url}}));
170
+ emit('web-navigate',{url},{intent:'embedded'});
155
171
  }
156
172
 
157
173
  async function navigate(value,{record=true}={}){
174
+ if(destroyed)return '';
158
175
  const sequence=++navigationSequence;
159
176
  try{
160
177
  const url=normalize(value);
161
178
  address.value=url;
162
179
  status.textContent='Checking the global deny policy…';
163
180
  const decision=await evaluate(url,'embedded');
164
- if(sequence!==navigationSequence)return'';
181
+ if(destroyed||sequence!==navigationSequence)return'';
165
182
  recordHistory(url,record);
166
183
  updateButtons();
167
184
  if(decision.blocked){
@@ -177,26 +194,29 @@ async function navigate(value,{record=true}={}){
177
194
  }
178
195
 
179
196
  async function openExternal(){
197
+ if(destroyed)return false;
180
198
  const sequence=++navigationSequence;
181
199
  try{
182
200
  const url=normalize(currentUrl()||address.value||homeUrl);
183
201
  status.textContent='Checking the global deny policy…';
184
202
  const decision=await evaluate(url,'external');
185
- if(sequence!==navigationSequence)return;
203
+ if(destroyed||sequence!==navigationSequence)return false;
186
204
  if(decision.blocked){
187
205
  showBlocked(url,decision);
188
206
  return;
189
207
  }
190
208
  status.textContent='Handing the allowed address to the system browser…';
191
- host.dispatchEvent(new CustomEvent('web-open-external',{bubbles:true,composed:true,detail:{url}}));
209
+ emit('web-open-external',{url},{intent:'external'});
210
+ return true;
192
211
  }catch(error){
193
- if(sequence===navigationSequence)status.textContent=error.message;
212
+ if(!destroyed&&sequence===navigationSequence)status.textContent=error.message;
213
+ return false;
194
214
  }
195
215
  }
196
216
 
197
217
  function ownNavigation(operation){
198
218
  operation.catch(function reportUnexpectedNavigationFailure(error){
199
- status.textContent=error.message;
219
+ if(!destroyed)status.textContent=error.message;
200
220
  });
201
221
  }
202
222
 
@@ -208,6 +228,7 @@ function move(amount){
208
228
  }
209
229
 
210
230
  function configure(options){
231
+ if(destroyed)return false;
211
232
  if(options?.homeUrl)homeUrl=normalize(options.homeUrl);
212
233
  navigationGuard=typeof options?.navigationGuard==='function'?options.navigationGuard:null;
213
234
  if(options?.initialUrl)ownNavigation(navigate(options.initialUrl));
@@ -223,7 +244,26 @@ function handleBackClick(){move(-1);}
223
244
  function handleForwardClick(){move(1);}
224
245
  function handleFrameLoad(){if(blocked.hidden)status.textContent='Loaded. If the page is blank, its site does not permit embedded viewing.';}
225
246
 
247
+ function destroy(){
248
+ if(destroyed)return false;
249
+ destroyed=true;
250
+ navigationSequence+=1;
251
+ navigationGuard=null;
252
+ host.ready=false;
253
+ root.querySelector('#addressForm').removeEventListener('submit',handleAddressSubmit);
254
+ root.querySelector('#home').removeEventListener('click',handleHomeClick);
255
+ root.querySelector('#reload').removeEventListener('click',handleReloadClick);
256
+ root.querySelector('#external').removeEventListener('click',handleExternalClick);
257
+ back.removeEventListener('click',handleBackClick);
258
+ forward.removeEventListener('click',handleForwardClick);
259
+ frame.removeEventListener('load',handleFrameLoad);
260
+ frame.removeAttribute('src');
261
+ events.dispose();
262
+ return true;
263
+ }
264
+
226
265
  host.configure=configure;
266
+ host.destroy=destroy;
227
267
  host.navigate=navigate;
228
268
  host.currentUrl=currentUrl;
229
269
  root.querySelector('#addressForm').addEventListener('submit',handleAddressSubmit);
@@ -235,5 +275,5 @@ forward.addEventListener('click',handleForwardClick);
235
275
  frame.addEventListener('load',handleFrameLoad);
236
276
  updateButtons();
237
277
  host.ready=true;
238
- host.dispatchEvent(new CustomEvent('web-navigator-ready',{bubbles:true,composed:true}));
278
+ emit('web-navigator-ready',{}, {ready:true});
239
279
  </script>
@@ -1,6 +1,6 @@
1
1
  import Is from '../../node_modules/strong-type/index.js';
2
2
  import '../modules/DBOPFS.js';
3
- import '../modules/AI.js?v=8';
3
+ import '../modules/AI.js';
4
4
  import {hasUserEntry} from '../modules/ChatRecords.js';
5
5
  import {normalizeMemoryContent} from '../modules/MemoryRecords.js';
6
6