arcane-os 0.1.2 → 0.2.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 (52) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/NOTICE +5 -3
  3. package/README.md +73 -24
  4. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +67 -18
  5. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +16 -5
  6. package/browser-runtime/ai/browser-kokoro-worker.mjs +3 -0
  7. package/browser-runtime/ai/browser-speech-artifacts.mjs +1108 -0
  8. package/browser-runtime/ai/browser-speech-providers.mjs +475 -0
  9. package/browser-runtime/ai/browser-speech.mjs +9 -0
  10. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1537 -167
  11. package/browser-runtime/ai/browser-wasm.mjs +46 -1
  12. package/browser-runtime/ai/browser-whisper-worker.mjs +3 -0
  13. package/browser-runtime/ai/browser-wllama-runtime.mjs +677 -132
  14. package/browser-runtime/ai/model-controller.mjs +138 -12
  15. package/browser-runtime/ai/speech-worker-client.mjs +207 -0
  16. package/browser-runtime/ai/speech-worker-runtime.mjs +516 -0
  17. package/browser-runtime/ai/wllama/index.mjs +389 -0
  18. package/docs/architecture.md +132 -22
  19. package/docs/reference/README.md +1 -1
  20. package/docs/reference/ai/browser-wasm.md +101 -42
  21. package/docs/reference/availability-and-normalization.md +19 -5
  22. package/docs/reference/behavioral-testing.md +18 -5
  23. package/docs/reference/cli.md +2 -2
  24. package/docs/reference/inventory/package-api.json +14 -14
  25. package/docs/reference/protocols.md +4 -4
  26. package/docs/reference/sdk-api.md +68 -38
  27. package/docs/work-amplification.md +8 -4
  28. package/package.json +7 -3
  29. package/runtime/ARCANE_RUNTIME_RELEASE.json +50 -20
  30. package/runtime/arcane/components/chat.html +280 -62
  31. package/runtime/arcane/components/speech.html +1113 -265
  32. package/runtime/arcane/entities/Chat.js +246 -43
  33. package/runtime/arcane/modules/AI.js +713 -162
  34. package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
  35. package/runtime/arcane/modules/AIRuntimeState.js +872 -0
  36. package/runtime/arcane/modules/ConfiguredAIChatSession.js +293 -27
  37. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +682 -0
  38. package/runtime/arcane/modules/DocumentLexicalSearch.js +292 -0
  39. package/runtime/arcane/modules/PersistentAIChatSession.js +268 -0
  40. package/runtime/arcane/modules/StaticDocumentCatalog.js +25 -206
  41. package/schemas/arcane-lock.schema.json +6 -4
  42. package/src/cli/main.mjs +14 -2
  43. package/src/constants.mjs +1 -1
  44. package/src/dev-server.mjs +244 -13
  45. package/src/import-map.mjs +59 -1
  46. package/src/packager/core.mjs +2 -2
  47. package/src/runtime.mjs +14 -4
  48. package/src/sdk-browser-runtime.mjs +28 -75
  49. package/src/templates/workspace-template.mjs +4 -4
  50. package/src/toolchain.mjs +3 -0
  51. package/src/workspace-runtime.mjs +1 -1
  52. package/src/workspace.mjs +1 -1
@@ -1,11 +1,25 @@
1
1
  <link rel="stylesheet" href="./arcane/css/layout.css?v=3" />
2
2
  <style>
3
+ :host {
4
+ display: block;
5
+ height: 100%;
6
+ min-width: 0;
7
+ min-height: 0;
8
+ --arcane-chat-gap: var(--arcane-space-3, .75rem);
9
+ --arcane-chat-radius: var(--arcane-radius-large, 1rem);
10
+ }
11
+
3
12
  .chat_area {
4
13
  display: flex;
5
14
  flex-direction: column;
6
15
  justify-content: flex-end;
7
- padding: 1em;
8
- height: calc(100% - 2em);
16
+ box-sizing: border-box;
17
+ width: min(100%, 72rem);
18
+ height: 100%;
19
+ min-height: 0;
20
+ margin-inline: auto;
21
+ padding: clamp(.65rem, 1.5vw, 1.25rem);
22
+ gap: var(--arcane-chat-gap);
9
23
  overflow: hidden;
10
24
  }
11
25
 
@@ -114,80 +128,128 @@
114
128
  border:0;
115
129
  }
116
130
 
117
- .chat_area ul {
131
+ .chat_output {
118
132
  flex:1 1 auto;
133
+ box-sizing: border-box;
134
+ width: 100%;
119
135
  min-height:0;
136
+ margin: 0;
137
+ padding: clamp(1rem, 2vw, 1.5rem);
120
138
  list-style: none;
121
139
  overflow-y: auto;
122
- padding: 0 1em;
140
+ scrollbar-gutter: stable;
141
+ border: 1px solid var(--arcane-border, var(--border-color));
142
+ border-radius: var(--arcane-chat-radius);
143
+ background: var(--arcane-surface-raised, var(--modal-background));
144
+ box-shadow: var(--arcane-shadow-medium, 0 .65rem 1.8rem var(--shadow-color));
123
145
  }
124
146
 
125
- .chat_area li {
126
- padding: 0.5em;
127
- margin: 0 0 1em;
147
+ .chat_output > li {
148
+ box-sizing: border-box;
128
149
  width: auto;
129
- max-width: 95%;
150
+ max-width: min(92%, 48rem);
151
+ margin: 0 0 .85rem;
152
+ padding: .85rem 1rem;
130
153
  clear: both;
154
+ border: 1px solid var(--arcane-border, var(--border-color));
155
+ border-radius: calc(var(--arcane-chat-radius) - .1rem);
156
+ overflow-wrap: anywhere;
131
157
  }
132
158
 
133
- .chat_area li.ai {
134
- width: 90%;
159
+ .chat_output > li.ai {
160
+ width: auto;
135
161
  float: left;
136
- padding: 10px; /* Ensures padding around the content */
162
+ background: var(--arcane-surface, var(--background));
163
+ border-start-start-radius: var(--arcane-radius-small, .3rem);
137
164
  }
138
165
 
139
-
140
- .chat_area li.user {
141
- text-align: right;
166
+ .chat_output > li.user {
142
167
  float: right;
168
+ background: var(--arcane-action, var(--primary-color));
169
+ color: var(--arcane-action-text, var(--button-text-color, var(--text-color)));
170
+ border-color: var(--arcane-action, var(--primary-color));
171
+ border-start-end-radius: var(--arcane-radius-small, .3rem);
172
+ text-align: right;
173
+ }
174
+
175
+ .chat_output li ul,
176
+ .chat_output li ol {
177
+ display: block;
178
+ min-height: auto;
179
+ margin: .55rem 0 .55rem 1.35rem;
180
+ padding: 0;
181
+ overflow: visible;
182
+ list-style: revert;
183
+ }
184
+
185
+ .chat_output li li {
186
+ float: none;
187
+ width: auto;
188
+ max-width: none;
189
+ margin: .28rem 0;
190
+ padding: 0;
191
+ border: 0;
192
+ border-radius: 0;
193
+ background: transparent;
194
+ color: inherit;
195
+ text-align: start;
143
196
  }
144
197
 
145
198
  .chat_input_area {
146
199
  display: grid;
147
- padding-bottom: 2em;
148
- grid-template-columns: 1fr auto auto;
149
- column-gap: 0.5em;
200
+ box-sizing: border-box;
201
+ width: 100%;
202
+ padding: var(--arcane-space-3, .7rem);
203
+ grid-template-columns: minmax(0, 1fr) auto auto;
204
+ align-items: center;
205
+ gap: var(--arcane-space-2, .6rem);
206
+ border: 1px solid var(--arcane-border, var(--border-color));
207
+ border-radius: var(--arcane-chat-radius);
208
+ background: var(--arcane-surface-raised, var(--modal-background));
209
+ box-shadow: var(--arcane-shadow-small, 0 .35rem 1rem var(--shadow-color));
150
210
  }
151
211
 
152
212
  .chat_input {
153
- width: calc(100% - 1em);
154
- min-height: 48px;
155
- max-height: 160px;
156
- padding: 1.1em 1em;
157
- margin: 0;
158
- border:2px solid var(--border-color);
159
- border-radius: 0.5em;
160
- font-family: Arial, sans-serif;
161
- font-size: 16px;
162
- line-height: 20px;
163
213
  box-sizing: border-box;
214
+ width: 100%;
215
+ min-height: 3rem;
216
+ max-height: 10rem;
217
+ margin: 0;
218
+ padding: .82rem .9rem;
219
+ border: 1px solid var(--arcane-border, var(--border-color));
220
+ border-radius: var(--arcane-radius-medium, .7rem);
221
+ background: var(--arcane-surface, var(--background));
222
+ color: var(--text-color);
223
+ font: inherit;
224
+ font-size: 1rem;
225
+ line-height: 1.35;
164
226
  resize: none;
165
227
  overflow-y: auto;
166
228
  }
167
229
 
168
- .chat_input:focus {
169
- outline:.18em solid var(--focus-color);
170
- outline-offset:.12em;
171
- border-color:var(--focus-color);
230
+ .chat_input:focus-visible {
231
+ outline: .18em solid var(--arcane-focus, var(--focus-color));
232
+ outline-offset: .12em;
233
+ border-color: var(--arcane-focus, var(--focus-color));
172
234
  }
173
235
 
174
236
  .chat_submit,
175
237
  button.chat_upload {
176
238
  display:grid;
177
239
  place-items:center;
178
- width:3.75em;
179
- height:3.75em;
240
+ width:3rem;
241
+ height:3rem;
180
242
  padding:0;
181
243
  margin:0;
182
- background-color:var(--primary-color);
183
- border:.2em solid var(--primary-color);
184
- border-radius:.8em;
244
+ background-color:var(--arcane-action, var(--primary-color));
245
+ border:.2em solid var(--arcane-action, var(--primary-color));
246
+ border-radius:var(--arcane-radius-medium, .7rem);
185
247
  cursor: pointer;
186
248
  outline: none;
187
- box-shadow:0 .18em .45em var(--shadow-color);
188
- align-self: end;
249
+ box-shadow:var(--arcane-shadow-small, 0 .18em .45em var(--shadow-color));
250
+ align-self:center;
189
251
  transition:background-color 180ms,border-color 180ms,box-shadow 180ms,transform 120ms;
190
- color:var(--button-text-color,var(--text-color));
252
+ color:var(--arcane-action-text, var(--button-text-color,var(--text-color)));
191
253
  }
192
254
 
193
255
  .chat_submit::before,
@@ -221,12 +283,59 @@
221
283
 
222
284
  .chat_submit:hover,
223
285
  button.chat_upload:hover {
224
- background-color:var(--secondary-color);
225
- border-color:var(--secondary-color);
226
- color:var(--text-color);
286
+ background-color:var(--arcane-secondary, var(--secondary-color));
287
+ border-color:var(--arcane-secondary, var(--secondary-color));
288
+ color:var(--arcane-secondary-text, var(--text-color));
227
289
  box-shadow:0 .25em .6em var(--shadow-color);
228
290
  }
229
291
 
292
+ .chat_submit:focus-visible,
293
+ button.chat_upload:focus-visible {
294
+ outline: .2rem solid var(--arcane-focus, var(--focus-color));
295
+ outline-offset: .18rem;
296
+ }
297
+
298
+ #speech {
299
+ display: grid;
300
+ flex: 0 0 auto;
301
+ min-width: 0;
302
+ }
303
+
304
+ .controls {
305
+ display: flex;
306
+ justify-content: flex-end;
307
+ }
308
+
309
+ .controls select {
310
+ width: min(100%, 18rem);
311
+ min-height: 2.65rem;
312
+ padding: .55rem .75rem;
313
+ border: 1px solid var(--arcane-border, var(--border-color));
314
+ border-radius: var(--arcane-radius-medium, .7rem);
315
+ background: var(--arcane-surface-raised, var(--modal-background));
316
+ color: var(--text-color);
317
+ font: inherit;
318
+ }
319
+
320
+ .controls select:focus-visible {
321
+ outline: .2rem solid var(--arcane-focus, var(--focus-color));
322
+ outline-offset: .18rem;
323
+ }
324
+
325
+ @media (max-width: 44rem) {
326
+ .chat_area {
327
+ padding: var(--arcane-space-2, .5rem);
328
+ }
329
+
330
+ .chat_output > li {
331
+ max-width: 96%;
332
+ }
333
+
334
+ .controls select {
335
+ width: 100%;
336
+ }
337
+ }
338
+
230
339
 
231
340
  :host(.journal){
232
341
  .chat_output {
@@ -518,11 +627,13 @@
518
627
  const [
519
628
  {default:MD},
520
629
  {default:FileEntity},
521
- {ConversationSubmissionBarrier,requireConversationTimeboxDelivery}
630
+ {ConversationSubmissionBarrier,requireConversationTimeboxDelivery},
631
+ {subscribeAIRuntimeState}
522
632
  ]=await Promise.all([
523
633
  import('../modules/MD.js'),
524
634
  import('../entities/File.js'),
525
- import('../modules/ConversationTimebox.js')
635
+ import('../modules/ConversationTimebox.js'),
636
+ import('../modules/AIRuntimeState.js')
526
637
  ]);
527
638
 
528
639
  host.language=host.language||'English';
@@ -532,7 +643,10 @@
532
643
  ?speech.initialMuted
533
644
  :true;
534
645
  let initialSpeechMuteListenerInstalled=false;
646
+ let pendingSpeechAvailability=null;
647
+ let speechAvailabilityListenerInstalled=false;
535
648
  const hostSubmissionBarrier=new ConversationSubmissionBarrier();
649
+ const aiRuntimeStateAbortController=new AbortController();
536
650
  let programmaticSubmissionQueue=Promise.resolve(true);
537
651
  let conversationTimeboxController=null;
538
652
  let conversationTimeboxUnsubscribe=null;
@@ -556,8 +670,17 @@
556
670
  host.setConversationComplete=setConversationComplete;
557
671
  host.bindConversationTimebox=bindConversationTimebox;
558
672
  host.submitMessage=submitMessage;
559
- host.aiAvailability={llm:true,stt:true,tts:true};
673
+ host.destroy=destroy;
674
+ host.aiAvailability={llm:false,stt:false,tts:false};
560
675
  host.conversationComplete=false;
676
+ applyAIAvailability(host.aiAvailability);
677
+ subscribeAIRuntimeState(
678
+ synchronizeAIRuntimeState,
679
+ {
680
+ signal:aiRuntimeStateAbortController.signal
681
+ }
682
+ );
683
+ window.addEventListener('pagehide',destroy,{once:true});
561
684
 
562
685
  function setConversationComplete(complete=true){
563
686
  if(typeof complete!=='boolean'){
@@ -699,6 +822,7 @@
699
822
  }
700
823
 
701
824
  function setAIAvailability(input={}){
825
+ const explicitSpeechAvailability={};
702
826
  const availability={
703
827
  llm:Object.prototype.hasOwnProperty.call(input,'llm')
704
828
  ?Boolean(input.llm)
@@ -711,25 +835,69 @@
711
835
  :host.aiAvailability.tts
712
836
  };
713
837
 
714
- host.aiAvailability=availability;
838
+ if(Object.prototype.hasOwnProperty.call(input,'stt')){
839
+ explicitSpeechAvailability.stt=availability.stt;
840
+ }
841
+ if(Object.prototype.hasOwnProperty.call(input,'tts')){
842
+ explicitSpeechAvailability.tts=availability.tts;
843
+ }
844
+ applyAIAvailability(availability);
845
+ if(Object.keys(explicitSpeechAvailability).length){
846
+ pendingSpeechAvailability={
847
+ ...(pendingSpeechAvailability||{}),
848
+ ...explicitSpeechAvailability
849
+ };
850
+ synchronizeSpeechCompatibilityAvailability();
851
+ }
852
+ return {...availability};
853
+ }
854
+
855
+ function applyAIAvailability(availability){
856
+ host.aiAvailability={...availability};
715
857
  send.disabled=host.conversationComplete||!availability.llm;
716
858
  send.title=availability.llm
717
859
  ?'Send message'
718
860
  :'The selected language model service is unavailable.';
719
861
  send.setAttribute('aria-label',send.title);
862
+ }
720
863
 
721
- const configureSpeech=()=>speech.setAvailability?.({
722
- stt:availability.stt,
723
- tts:availability.tts
864
+ function synchronizeAIRuntimeState(snapshot){
865
+ applyAIAvailability({
866
+ llm:snapshot.roles.llm.state==='ready',
867
+ stt:snapshot.roles.stt.state==='ready',
868
+ tts:snapshot.roles.tts.state==='ready'
724
869
  });
870
+ }
725
871
 
872
+ function synchronizeSpeechCompatibilityAvailability(){
873
+ if(!pendingSpeechAvailability){
874
+ return;
875
+ }
726
876
  if(speech.componentReady){
727
- configureSpeech();
728
- }else{
729
- speech.addEventListener('speech-ready',configureSpeech,{once:true});
877
+ const availability=pendingSpeechAvailability;
878
+ pendingSpeechAvailability=null;
879
+ speech.setAvailability?.({...availability});
880
+ return;
881
+ }
882
+ if(speechAvailabilityListenerInstalled){
883
+ return;
730
884
  }
731
885
 
732
- return {...availability};
886
+ speechAvailabilityListenerInstalled=true;
887
+ speech.addEventListener(
888
+ 'speech-ready',
889
+ applyPendingSpeechAvailability,
890
+ {once:true}
891
+ );
892
+ }
893
+
894
+ function applyPendingSpeechAvailability(){
895
+ speechAvailabilityListenerInstalled=false;
896
+ if(pendingSpeechAvailability){
897
+ const availability=pendingSpeechAvailability;
898
+ pendingSpeechAvailability=null;
899
+ speech.setAvailability?.({...availability});
900
+ }
733
901
  }
734
902
 
735
903
  function formatConversationTime(milliseconds=0){
@@ -928,7 +1096,7 @@
928
1096
  }
929
1097
  )
930
1098
  );
931
- console.error('The conversation timebox message could not be sent.',error);
1099
+ console.error('The conversation timebox message could not be sent.');
932
1100
  return false;
933
1101
  }
934
1102
  );
@@ -997,10 +1165,22 @@
997
1165
  const clearThinking=thinkingHTML;
998
1166
 
999
1167
  //console.table(arguments);
1000
- if(!isThinking&&text&&host.aiAvailability.tts){
1001
- ai.streamTTS(
1002
- text
1003
- )
1168
+ if(
1169
+ !isThinking
1170
+ &&text
1171
+ &&host.aiAvailability.tts
1172
+ &&speech.muted===false
1173
+ ){
1174
+ const aiRuntime=globalThis.ai;
1175
+ if(typeof aiRuntime?.streamTTS==='function'){
1176
+ void Promise.resolve(aiRuntime.streamTTS(text)).catch(
1177
+ reportTTSError
1178
+ );
1179
+ }else{
1180
+ reportTTSError(
1181
+ new Error('The ready speech provider has no synthesis adapter.')
1182
+ );
1183
+ }
1004
1184
  }
1005
1185
 
1006
1186
  if(clearThinking){
@@ -1126,7 +1306,7 @@
1126
1306
  }
1127
1307
  )
1128
1308
  );
1129
- console.error('The chat message could not be submitted.',error);
1309
+ console.error('The chat message could not be submitted.');
1130
1310
  return false;
1131
1311
  }
1132
1312
  );
@@ -1143,6 +1323,9 @@
1143
1323
  if(host.conversationComplete){
1144
1324
  return false;
1145
1325
  }
1326
+ if(!host.aiAvailability.llm){
1327
+ return false;
1328
+ }
1146
1329
  const submissionContext=Object.freeze({
1147
1330
  source:'user',
1148
1331
  preserveDraft:false,
@@ -1154,7 +1337,6 @@
1154
1337
  return false;
1155
1338
  }
1156
1339
  const messageTime = getMilTime();
1157
- console.info("Dispatching message:", text);
1158
1340
  if(!submissionContext.preserveDraft){
1159
1341
  textArea.value='';
1160
1342
  }
@@ -1189,8 +1371,15 @@
1189
1371
  }
1190
1372
  );
1191
1373
 
1192
- ai.stopAudio();
1193
- ai.resumeAudio?.();
1374
+ if(host.aiAvailability.tts&&speech.muted===false){
1375
+ const aiRuntime=globalThis.ai;
1376
+ aiRuntime?.stopAudio?.();
1377
+ if(typeof aiRuntime?.resumeAudio==='function'){
1378
+ void Promise.resolve(aiRuntime.resumeAudio()).catch(
1379
+ reportTTSError
1380
+ );
1381
+ }
1382
+ }
1194
1383
 
1195
1384
  host.dispatchEvent(chatEvent);
1196
1385
 
@@ -1219,6 +1408,35 @@
1219
1408
  chatOutput.scrollTop = chatOutput.scrollHeight;
1220
1409
  }
1221
1410
 
1411
+ function reportTTSError(error){
1412
+ if(typeof speech.reportTTSError==='function'){
1413
+ speech.reportTTSError(error);
1414
+ return;
1415
+ }
1416
+
1417
+ const message=typeof error?.message==='string'&&error.message.trim()
1418
+ ?error.message.trim().slice(0,240)
1419
+ :'Speech synthesis failed.';
1420
+ host.dispatchEvent(
1421
+ new CustomEvent(
1422
+ 'chat-speech-synthesis-error',
1423
+ {
1424
+ bubbles:true,
1425
+ composed:true,
1426
+ detail:{error,message}
1427
+ }
1428
+ )
1429
+ );
1430
+ console.error('Speech synthesis failed.');
1431
+ }
1432
+
1433
+ function destroy(){
1434
+ aiRuntimeStateAbortController.abort();
1435
+ window.removeEventListener('pagehide',destroy);
1436
+ speech.destroy?.();
1437
+ host.ready=false;
1438
+ }
1439
+
1222
1440
  // Initialize and adjust on input
1223
1441
  resizeTextArea();
1224
1442