arcane-os 0.5.12 → 0.5.14

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 (54) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +11 -10
  3. package/browser-runtime/ai/browser-wllama-runtime.mjs +2 -1
  4. package/browser-runtime/ai/model-controller.mjs +6 -5
  5. package/browser-runtime/ai/speech-worker-client.mjs +45 -3
  6. package/browser-runtime/event-manager.mjs +2 -1
  7. package/browser-runtime/logging.mjs +58 -0
  8. package/docs/reference/README.md +8 -7
  9. package/docs/reference/ai/browser-speech.md +46 -1
  10. package/docs/reference/asset-versioning.md +69 -0
  11. package/docs/reference/inventory/package-api.json +30 -2
  12. package/docs/reference/runtime-modules.md +18 -0
  13. package/docs/reference/sdk-api.md +94 -8
  14. package/package.json +3 -2
  15. package/runtime/arcane/components/app-bar.html +3 -1
  16. package/runtime/arcane/components/calculator.html +1 -1
  17. package/runtime/arcane/components/chart.html +2 -7
  18. package/runtime/arcane/components/chat.html +22 -20
  19. package/runtime/arcane/components/dashboard-config.html +6 -9
  20. package/runtime/arcane/components/data-maintenance.html +4 -2
  21. package/runtime/arcane/components/data-view.html +3 -1
  22. package/runtime/arcane/components/directory-picker.html +3 -1
  23. package/runtime/arcane/components/file-manager.html +17 -15
  24. package/runtime/arcane/components/header.html +5 -3
  25. package/runtime/arcane/components/markdown-document.html +4 -7
  26. package/runtime/arcane/components/markdown-editor.html +6 -9
  27. package/runtime/arcane/components/media-embed.html +1 -1
  28. package/runtime/arcane/components/modal.html +3 -1
  29. package/runtime/arcane/components/screen-capture.html +4 -2
  30. package/runtime/arcane/components/speech.html +10 -8
  31. package/runtime/arcane/components/table.html +3 -1
  32. package/runtime/arcane/components/voice-transcription.html +12 -15
  33. package/runtime/arcane/entities/Chat.js +5 -4
  34. package/runtime/arcane/entities/User.js +2 -1
  35. package/runtime/arcane/modules/AI.js +442 -292
  36. package/runtime/arcane/modules/AIProviderRuntime.js +359 -82
  37. package/runtime/arcane/modules/CommunicationAppController.js +2 -1
  38. package/runtime/arcane/modules/ComponentContracts.js +3 -2
  39. package/runtime/arcane/modules/ConversationTimebox.js +2 -1
  40. package/runtime/arcane/modules/DBOPFS.js +5 -4
  41. package/runtime/arcane/modules/Errors.js +3 -14
  42. package/runtime/arcane/modules/HTMLImport.js +188 -16
  43. package/runtime/arcane/modules/LocalAIReadinessController.js +2 -1
  44. package/runtime/arcane/modules/MD.js +3 -2
  45. package/runtime/arcane/modules/MailOutbox.mjs +2 -1
  46. package/runtime/arcane/modules/PersistentAIChatSession.js +2 -1
  47. package/runtime/arcane/modules/ScreenCapture.js +2 -1
  48. package/runtime/arcane/modules/ThemeBootstrap.js +3 -2
  49. package/runtime/arcane/modules/ToolCallRouter.js +2 -1
  50. package/src/dev-server.mjs +72 -5
  51. package/src/import-map.mjs +603 -16
  52. package/src/installed-sdk-runtime.mjs +1 -0
  53. package/src/packager/core.mjs +60 -1
  54. package/src/workspace-runtime.mjs +19 -7
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import ArcaneCommunicationBridge from './ArcaneCommunicationBridge.js';
2
3
  import CommunicationHub from './CommunicationHub.js?v=2';
3
4
  import CommunicationPreferences from './CommunicationPreferences.js';
@@ -360,7 +361,7 @@ export default class CommunicationAppController{
360
361
  hub.dispose();
361
362
  }
362
363
  }catch(error){
363
- console.error('The communication hub could not be disposed cleanly.',error);
364
+ arcaneLogging.error('The communication hub could not be disposed cleanly.',error);
364
365
  }
365
366
  }
366
367
 
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {
2
3
  createArcaneEventSource,
3
4
  projectArcaneDOMEvent
@@ -713,7 +714,7 @@ function createSTTActivationController({
713
714
  try{
714
715
  onChange();
715
716
  }catch(error){
716
- console.error('Unable to render STT activation state:',error);
717
+ arcaneLogging.error('Unable to render STT activation state:',error);
717
718
  }
718
719
  }
719
720
  }
@@ -724,7 +725,7 @@ function createSTTActivationController({
724
725
  if(nextAction){
725
726
  void request(nextAction).catch(
726
727
  function reportSTTActivationRequestFailure(error){
727
- console.error('The STT activation request could not settle.',error);
728
+ arcaneLogging.error('The STT activation request could not settle.',error);
728
729
  }
729
730
  );
730
731
  }
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {createArcaneEventSource} from 'arcane-os/event-manager';
2
3
 
3
4
  const CONVERSATION_TIMEBOX_TOOL_NAME='conversation_timebox';
@@ -168,7 +169,7 @@ function conversationTimeboxSubmissionKey(context={}){
168
169
  }
169
170
 
170
171
  function reportConversationTimeboxListenerError(error){
171
- console.error('A conversation timebox listener failed.',error);
172
+ arcaneLogging.error('A conversation timebox listener failed.',error);
172
173
  }
173
174
 
174
175
  function conversationTimeboxError(message,code,reason,ErrorType=Error){
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import Is from "../../node_modules/strong-type/index.js";
2
3
  import {openApplicationDataDirectory} from './AppDataScope.js';
3
4
  import {
@@ -458,7 +459,7 @@ class DBOPFS {
458
459
  );
459
460
  }
460
461
  }catch(error){
461
- console.error(`Error writing file '${fileName}' to table '${tableName}':`,error)
462
+ arcaneLogging.error(`Error writing file '${fileName}' to table '${tableName}':`,error)
462
463
  throw error
463
464
  }
464
465
 
@@ -603,7 +604,7 @@ class DBOPFS {
603
604
  const fileName=entries[index][0]
604
605
 
605
606
  if(result.status!=='fulfilled'){
606
- console.error(`Failed to set file '${fileName}':`,result.reason)
607
+ arcaneLogging.error(`Failed to set file '${fileName}':`,result.reason)
607
608
  }
608
609
  }
609
610
  );
@@ -742,7 +743,7 @@ class DBOPFS {
742
743
  await table.removeEntry(fileName)
743
744
  }catch(error){
744
745
  if(error.name!=='NotFoundError'){
745
- console.error(error)
746
+ arcaneLogging.error(error)
746
747
  throw error
747
748
  }
748
749
  }
@@ -784,7 +785,7 @@ class DBOPFS {
784
785
  delete this.#tables[tableName]
785
786
  delete this.#tableHandles[tableName]
786
787
  }catch(error){
787
- console.error(error)
788
+ arcaneLogging.error(error)
788
789
  }
789
790
 
790
791
  return true
@@ -1,4 +1,5 @@
1
1
  import waitForComponent from './WaitForComponent.js';
2
+ import { arcaneLogging,readArcaneDeveloperMode } from 'arcane-os/logging';
2
3
  import {
3
4
  arcaneEvents,
4
5
  createArcaneEventSource
@@ -154,18 +155,6 @@ async function sendWithWindowMail(target,...args){
154
155
  return target.mail.send(...args);
155
156
  }
156
157
 
157
- function defaultDeveloperModeStatus(target){
158
- try{
159
- if(target?.user?.ready!==true){
160
- return null;
161
- }
162
-
163
- return target.user.developer===true;
164
- }catch{
165
- return false;
166
- }
167
- }
168
-
169
158
  function appendDeveloperDetail(document,list,label,value){
170
159
  if(value===null||value===undefined||value===''){
171
160
  return;
@@ -340,7 +329,7 @@ class Errors {
340
329
  this.now=options.now||Date.now;
341
330
  this.schedule=options.schedule||globalThis.setTimeout.bind(globalThis);
342
331
  this.cancel=options.cancel||globalThis.clearTimeout.bind(globalThis);
343
- this.logger=options.logger||globalThis.console;
332
+ this.logger=options.logger||arcaneLogging;
344
333
  this.storage=options.storage===undefined
345
334
  ? defaultStorage(target)
346
335
  : options.storage;
@@ -352,7 +341,7 @@ class Errors {
352
341
  || sendWithWindowMail.bind(null,target);
353
342
  this.isDeveloperMode=typeof options.isDeveloperMode==='function'
354
343
  ? options.isDeveloperMode
355
- : defaultDeveloperModeStatus.bind(null,target);
344
+ : readArcaneDeveloperMode.bind(null,target);
356
345
  this.presentDeveloperIncident=typeof options.presentDeveloperIncident==='function'
357
346
  ? options.presentDeveloperIncident
358
347
  : presentDeveloperIncidentModal.bind(null,target);
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {
2
3
  createArcaneEventSource,
3
4
  projectArcaneDOMEvent
@@ -12,6 +13,44 @@ globalThis[htmlImportHostRegistryKey]=htmlImportHostRegistry;
12
13
 
13
14
  let htmlImportScriptId=0;
14
15
 
16
+ const htmlImportModuleURL=new URL(import.meta.url);
17
+ const htmlImportAssetVersion=htmlImportModuleURL.searchParams.get('arcaneVersion');
18
+
19
+ function versionComponentResource(value,baseHref){
20
+ if(!htmlImportAssetVersion||typeof value!=='string'||!value||value.startsWith('#')){
21
+ return value;
22
+ }
23
+ let resolvedURL;
24
+ try{
25
+ resolvedURL=new URL(value,baseHref);
26
+ }catch{
27
+ return value;
28
+ }
29
+ if(resolvedURL.protocol!==htmlImportModuleURL.protocol
30
+ ||resolvedURL.origin!==htmlImportModuleURL.origin){
31
+ return value;
32
+ }
33
+
34
+ const fragmentIndex=value.indexOf('#');
35
+ const fragment=fragmentIndex<0?'':value.slice(fragmentIndex);
36
+ const resource=fragmentIndex<0?value:value.slice(0,fragmentIndex);
37
+ const queryIndex=resource.indexOf('?');
38
+ const pathname=queryIndex<0?resource:resource.slice(0,queryIndex);
39
+ const query=queryIndex<0?'':resource.slice(queryIndex+1);
40
+ const versionField=`arcaneVersion=${encodeURIComponent(htmlImportAssetVersion)}`;
41
+ let replaced=false;
42
+ const fields=query?query.split('&').map(function versionQueryField(field){
43
+ if(!new URLSearchParams(field).has('arcaneVersion'))return field;
44
+ if(replaced)return null;
45
+ replaced=true;
46
+ return versionField;
47
+ }).filter(function retainQueryField(field){
48
+ return field!==null;
49
+ }):[];
50
+ if(!replaced)fields.push(versionField);
51
+ return `${pathname}?${fields.join('&')}${fragment}`;
52
+ }
53
+
15
54
  function componentRuntimeRoot(resolvedHref){
16
55
  const componentURL=new URL(resolvedHref);
17
56
  const componentMarker='/arcane/components/';
@@ -32,28 +71,157 @@ function resolveComponentResource(value,runtimeRoot){
32
71
  }
33
72
 
34
73
  function resolveComponentStyleResources(styleText,runtimeRoot){
35
- if(!runtimeRoot)return styleText;
36
- return styleText.replace(
37
- /url\(\s*(['"]?)\.\/arcane\/([^)'"\s]+)\1\s*\)/gu,
38
- function resolveStyleURL(_match,quote,resourcePath){
39
- const resolved=new URL(resourcePath,runtimeRoot).href;
40
- return `url(${quote}${resolved}${quote})`;
74
+ if(!runtimeRoot&&!htmlImportAssetVersion)return styleText;
75
+ const parts=[];
76
+ let copiedThrough=0;
77
+ let index=0;
78
+
79
+ function afterComment(start){
80
+ const end=styleText.indexOf('*/',start+2);
81
+ return end<0?styleText.length:end+2;
82
+ }
83
+
84
+ function afterEscape(start){
85
+ let end=start+1;
86
+ if(/[\da-f]/iu.test(styleText[end]||'')){
87
+ const limit=end+6;
88
+ while(end<limit&&/[\da-f]/iu.test(styleText[end]||''))end+=1;
89
+ if(/[\t\n\f\r ]/u.test(styleText[end]||'')){
90
+ end+=styleText[end]==='\r'&&styleText[end+1]==='\n'?2:1;
91
+ }
92
+ return end;
41
93
  }
42
- );
94
+ return end+(styleText[end]==='\r'&&styleText[end+1]==='\n'?2:1);
95
+ }
96
+
97
+ function afterString(start){
98
+ const quote=styleText[start];
99
+ let end=start+1;
100
+ while(end<styleText.length){
101
+ if(styleText[end]==='\\'){
102
+ end=afterEscape(end);
103
+ }else if(styleText[end]===quote){
104
+ return end+1;
105
+ }else if(/[\n\r\f]/u.test(styleText[end])){
106
+ return end;
107
+ }else{
108
+ end+=1;
109
+ }
110
+ }
111
+ return styleText.length;
112
+ }
113
+
114
+ function afterWhitespaceAndComments(start){
115
+ let end=start;
116
+ while(end<styleText.length){
117
+ if(/[\t\n\f\r ]/u.test(styleText[end])){
118
+ end+=1;
119
+ }else if(styleText.startsWith('/*',end)){
120
+ end=afterComment(end);
121
+ }else{
122
+ break;
123
+ }
124
+ }
125
+ return end;
126
+ }
127
+
128
+ function appendResource(start,end){
129
+ const resourcePath=styleText.slice(start,end);
130
+ // CSS escapes retain their original spelling until their URL syntax is decoded.
131
+ if(resourcePath.includes('\\'))return;
132
+ const resolved=versionComponentResource(
133
+ resolveComponentResource(resourcePath,runtimeRoot),
134
+ document.baseURI
135
+ );
136
+ if(resolved===resourcePath)return;
137
+ parts.push(styleText.slice(copiedThrough,start),resolved);
138
+ copiedThrough=end;
139
+ }
140
+
141
+ while(index<styleText.length){
142
+ if(styleText.startsWith('/*',index)){
143
+ index=afterComment(index);
144
+ continue;
145
+ }
146
+ if(styleText[index]==='"'||styleText[index]==="'"){
147
+ index=afterString(index);
148
+ continue;
149
+ }
150
+
151
+ const atRule=styleText[index]==='@';
152
+ const hashToken=styleText[index]==='#';
153
+ const nameStart=atRule||hashToken?index+1:index;
154
+ let nameEnd=nameStart;
155
+ while(nameEnd<styleText.length&&/[-\w\u0080-\uFFFF\\]/u.test(styleText[nameEnd])){
156
+ nameEnd=styleText[nameEnd]==='\\'?afterEscape(nameEnd):nameEnd+1;
157
+ }
158
+ if(nameEnd===nameStart){
159
+ index+=1;
160
+ continue;
161
+ }
162
+ const name=styleText.slice(nameStart,nameEnd).toLowerCase();
163
+ index=nameEnd;
164
+ if(hashToken)continue;
165
+ if(atRule&&name==='import'){
166
+ const start=afterWhitespaceAndComments(nameEnd);
167
+ if(styleText[start]==='"'||styleText[start]==="'"){
168
+ const end=afterString(start);
169
+ if(styleText[end-1]===styleText[start])appendResource(start+1,end-1);
170
+ index=end;
171
+ }
172
+ continue;
173
+ }
174
+ if(atRule||name!=='url'||styleText[nameEnd]!=='(')continue;
175
+
176
+ let start=nameEnd+1;
177
+ while(start<styleText.length&&/[\t\n\f\r ]/u.test(styleText[start]))start+=1;
178
+ if(styleText[start]==='"'||styleText[start]==="'"){
179
+ const end=afterString(start);
180
+ const close=afterWhitespaceAndComments(end);
181
+ if(styleText[end-1]===styleText[start]&&styleText[close]===')'){
182
+ appendResource(start+1,end-1);
183
+ index=close+1;
184
+ }else{
185
+ index=end;
186
+ }
187
+ continue;
188
+ }
189
+
190
+ let end=start;
191
+ while(end<styleText.length&&!/[\t\n\f\r )'"(]/u.test(styleText[end])){
192
+ end=styleText[end]==='\\'?afterEscape(end):end+1;
193
+ }
194
+ let close=end;
195
+ while(close<styleText.length&&/[\t\n\f\r ]/u.test(styleText[close]))close+=1;
196
+ if(styleText[close]===')'){
197
+ appendResource(start,end);
198
+ index=close+1;
199
+ }else{
200
+ index=end;
201
+ }
202
+ }
203
+ if(parts.length===0)return styleText;
204
+ parts.push(styleText.slice(copiedThrough));
205
+ return parts.join('');
43
206
  }
44
207
 
45
208
  function createComponentFragment(html,resolvedHref){
46
209
  const template=document.createElement('template');
47
210
  template.innerHTML=html;
48
211
  const runtimeRoot=componentRuntimeRoot(resolvedHref);
49
- if(!runtimeRoot)return template.content;
50
212
 
51
213
  for(const element of template.content.querySelectorAll('[href],[src]')){
52
214
  for(const attribute of ['href','src']){
53
215
  if(!element.hasAttribute(attribute))continue;
54
216
  const value=element.getAttribute(attribute);
55
217
  const resolved=resolveComponentResource(value,runtimeRoot);
56
- if(resolved!==value)element.setAttribute(attribute,resolved);
218
+ const navigation=attribute==='href'
219
+ &&['a','area','base'].includes(element.localName);
220
+ const versioned=navigation?resolved:versionComponentResource(
221
+ resolved,
222
+ element.localName==='script'?resolvedHref:document.baseURI
223
+ );
224
+ if(versioned!==value)element.setAttribute(attribute,versioned);
57
225
  }
58
226
  }
59
227
  for(const style of template.content.querySelectorAll('style')){
@@ -69,7 +237,8 @@ function resolveRelativeDynamicImports(source,baseHref){
69
237
  return source.replace(
70
238
  /\bimport\s*\(\s*(['"])(\.{1,2}\/[^'"]+)\1\s*\)/gu,
71
239
  function resolveDynamicImport(_match,_quote,specifier){
72
- return `import(${JSON.stringify(new URL(specifier,baseHref).href)})`;
240
+ const resolved=versionComponentResource(new URL(specifier,baseHref).href,baseHref);
241
+ return `import(${JSON.stringify(resolved)})`;
73
242
  }
74
243
  );
75
244
  }
@@ -131,15 +300,15 @@ class HTMLImport extends HTMLElement {
131
300
  let resolvedHref='';
132
301
  const operationId=`html-import-load-${this.#events.instanceId}-${generation}`;
133
302
  if(!href){
134
- console.log('no href provided for html-import tag',this);
303
+ arcaneLogging.log('no href provided for html-import tag',this);
135
304
  this.#loadController=null;
136
305
  return;
137
306
  }
138
307
 
139
308
  try{
140
309
  const resolvedURL=new URL(href,document.baseURI);
141
- resolvedHref=resolvedURL.href;
142
- const response=await fetch(resolvedURL.href,{
310
+ resolvedHref=versionComponentResource(resolvedURL.href,document.baseURI);
311
+ const response=await fetch(resolvedHref,{
143
312
  cache:'default',
144
313
  method:'GET',
145
314
  signal:controller.signal
@@ -151,7 +320,7 @@ class HTMLImport extends HTMLElement {
151
320
  await this.#destroyImportedHost();
152
321
  return;
153
322
  }
154
- console.error('Error loading HTML component:',err);
323
+ arcaneLogging.error('Error loading HTML component:',err);
155
324
  const detail={
156
325
  code:'HTML_IMPORT_FAILED',
157
326
  href,
@@ -201,7 +370,7 @@ class HTMLImport extends HTMLElement {
201
370
  }
202
371
 
203
372
  #reportImportedDestroyError(error,{events,href,operationId}){
204
- console.error('Error destroying imported HTML component:',error);
373
+ arcaneLogging.error('Error destroying imported HTML component:',error);
205
374
  if(!events||events.disposed)return;
206
375
  const detail={
207
376
  code:'HTML_IMPORT_IMPORTED_COMPONENT_DESTROY_REJECTED',
@@ -308,7 +477,10 @@ class HTMLImport extends HTMLElement {
308
477
  let sourceBaseHref=contentBaseHref;
309
478
  const scriptSource=script.getAttribute('src');
310
479
  if(scriptSource!==null){
311
- const requestedScriptHref=new URL(scriptSource,contentBaseHref).href;
480
+ const requestedScriptHref=versionComponentResource(
481
+ new URL(scriptSource,contentBaseHref).href,
482
+ contentBaseHref
483
+ );
312
484
  const response=await fetch(requestedScriptHref,{
313
485
  cache:'default',
314
486
  method:'GET',
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {
2
3
  checkLocalAIReadiness,
3
4
  deriveLocalAIRequirements
@@ -287,7 +288,7 @@ export function createLocalAIReadinessController({
287
288
  function retry(){
288
289
  void check().catch(function ignoreDestroyedLocalAIRetry(error){
289
290
  if(error?.name!=='AbortError'){
290
- console.error('Local AI readiness retry failed.',error);
291
+ arcaneLogging.error('Local AI readiness retry failed.',error);
291
292
  }
292
293
  });
293
294
  }
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import { marked } from './Marked.min.js';
2
3
  import Is from '../../node_modules/strong-type/index.js';
3
4
 
@@ -45,7 +46,7 @@ class MD {
45
46
 
46
47
  set raw(value=''){
47
48
  if(!is.string(value)){
48
- console.trace('MD.raw must be a string.');
49
+ arcaneLogging.trace('MD.raw must be a string.');
49
50
  return this.#raw;
50
51
  }
51
52
  this.#raw = value;
@@ -55,7 +56,7 @@ class MD {
55
56
 
56
57
  append(value=''){
57
58
  if(!is.string(value)){
58
- console.trace('MD.append must be a string.');
59
+ arcaneLogging.trace('MD.append must be a string.');
59
60
  return this.#raw;
60
61
  }
61
62
  this.#raw += value;
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  export const MAIL_OUTBOX_PROTOCOL='arcane-mail-outbox/1';
2
3
  export const MAIL_OUTBOX_TABLE='mail_outbox';
3
4
  export const MAIL_OUTBOX_IDEMPOTENCY_WINDOW_MS=24*60*60*1000;
@@ -34,7 +35,7 @@ function reportMailOutboxObserverError(error){
34
35
  return;
35
36
  }
36
37
  }catch{}
37
- try{globalThis.console?.error?.(error);}catch{}
38
+ try{arcaneLogging.error?.(error);}catch{}
38
39
  }
39
40
 
40
41
  function fail(message,code='MAIL_OUTBOX_INVALID',ErrorType=Error,cause){
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import ChatEntity from '../entities/Chat.js';
2
3
  import ConfiguredAIChatSession,{
3
4
  normalizeStructuralToolCall
@@ -475,7 +476,7 @@ class PersistentAIChatSession{
475
476
  error instanceof Error?error:new Error('The saved chat history is invalid.'),
476
477
  'AI_CHAT_INCOHERENT_PERSISTENCE',
477
478
  );
478
- console.error('Arcane saved chat history is readable but not actionable.',error);
479
+ arcaneLogging.error('Arcane saved chat history is readable but not actionable.',error);
479
480
  }
480
481
  const storedSystem=storedMessages.find(message=>message.role==='system');
481
482
  const initialMessages=storedMessages
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {createArcaneEventSource} from 'arcane-os/event-manager';
2
3
  import GifEncoder from './GifEncoder.js';
3
4
 
@@ -252,7 +253,7 @@ function supportedRecorderType(Recorder){
252
253
 
253
254
  function reportDetachedError(error){
254
255
  if(typeof globalThis.reportError==='function')globalThis.reportError(error);
255
- else globalThis.console?.error?.(error);
256
+ else arcaneLogging.error?.(error);
256
257
  }
257
258
 
258
259
  function monotonicNow(){
@@ -1,3 +1,4 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  import {loadAndApplyTheme} from './ThemeManager.js';
2
3
  import {createArcaneEventSource} from 'arcane-os/event-manager';
3
4
 
@@ -43,7 +44,7 @@ function installAppearanceListener(ready){
43
44
  Promise.resolve(ready).then(function reloadTheme(result){
44
45
  return result?.manager?.load?.();
45
46
  }).catch(function reportThemeReloadFailure(error){
46
- console.warn(
47
+ arcaneLogging.warn(
47
48
  '[Arcane theme] Unable to apply the changed host appearance.',
48
49
  error
49
50
  );
@@ -80,7 +81,7 @@ export function bootstrapArcaneTheme(options={}){
80
81
  }
81
82
 
82
83
  const ready=loadAndApplyTheme(options).catch(error=>{
83
- console.warn('[Arcane theme] Unable to load the saved appearance; using the system theme.',error);
84
+ arcaneLogging.warn('[Arcane theme] Unable to load the saved appearance; using the system theme.',error);
84
85
  return {manager:null,state:null,error};
85
86
  });
86
87
 
@@ -1,10 +1,11 @@
1
+ import { arcaneLogging } from 'arcane-os/logging';
1
2
  function parseArguments(value,name=''){
2
3
  let parsed=value;
3
4
  if(typeof parsed==='string'){
4
5
  try{
5
6
  parsed=JSON.parse(parsed);
6
7
  }catch(error){
7
- console.warn(error);
8
+ arcaneLogging.warn(error);
8
9
  throw new Error(`Tool ${name} returned invalid JSON arguments.`);
9
10
  }
10
11
  }
@@ -1,14 +1,19 @@
1
1
  import {constants as FS_CONSTANTS} from 'node:fs';
2
- import {lstat,open,realpath} from 'node:fs/promises';
2
+ import {lstat,open,readFile,realpath} from 'node:fs/promises';
3
3
  import http from 'node:http';
4
4
  import path from 'node:path';
5
5
  import {resolveWorkspace} from './workspace.mjs';
6
6
  import {createEventQueue} from './event-queue.mjs';
7
+ import {
8
+ inspectImportMapHtml,MANAGED_IMPORT_MAP_ATTRIBUTE,
9
+ readWorkspaceAssetVersion,rewriteAssetReferences
10
+ } from './import-map.mjs';
7
11
 
8
12
  const MIME_TYPES=new Map([
9
13
  ['.css','text/css; charset=utf-8'],
10
14
  ['.gif','image/gif'],
11
15
  ['.html','text/html; charset=utf-8'],
16
+ ['.htm','text/html; charset=utf-8'],
12
17
  ['.ico','image/x-icon'],
13
18
  ['.jpeg','image/jpeg'],
14
19
  ['.jpg','image/jpeg'],
@@ -308,11 +313,21 @@ async function openSafeFile(root,segments){
308
313
  }
309
314
  }
310
315
 
311
- async function sendFile(response,opened,{head=false}={}){
316
+ async function sendFile(response,opened,{head=false,assetVersion,revalidate=false,onReference}={}){
312
317
  const extension=path.extname(opened.candidate).toLowerCase();
318
+ const content=assetVersion&&/\.(?:m?js|html?|css)$/iu.test(extension)
319
+ ?Buffer.from(rewriteAssetReferences(opened.content.toString('utf8'),{
320
+ filePath:opened.candidate,
321
+ version:assetVersion,
322
+ onReference
323
+ }),'utf8')
324
+ :opened.content;
325
+ // Entry documents must discover the current generated graph on navigation.
326
+ // no-cache permits storage and revalidation; ordinary asset caching is unchanged.
313
327
  response.writeHead(200,{
314
328
  'content-type':MIME_TYPES.get(extension)||'application/octet-stream',
315
- 'content-length':opened.content.byteLength
329
+ 'content-length':content.byteLength,
330
+ ...(revalidate?{'cache-control':'no-cache'}:{})
316
331
  });
317
332
  await new Promise((resolve,reject)=>{
318
333
  let settled=false;
@@ -336,7 +351,7 @@ async function sendFile(response,opened,{head=false}={}){
336
351
  response.once('error',failed);
337
352
  response.once('finish',completed);
338
353
  response.once('close',completed);
339
- response.end(head?undefined:opened.content);
354
+ response.end(head?undefined:content);
340
355
  });
341
356
  }
342
357
 
@@ -509,11 +524,23 @@ async function startOwnedDevServer({
509
524
  })
510
525
  :await packagedRoutes(releaseRoot);
511
526
  const mappings=deterministicMappings(routeSet.mappings);
527
+ async function selectedAssetVersion(){
528
+ if(mode!=='source')return undefined;
529
+ return sdkRuntimeSourceRoot===undefined
530
+ ?readWorkspaceAssetVersion(routeSet.workspaceRoot)
531
+ :JSON.parse(await readFile(
532
+ path.join(routeSet.runtime.sourceRoot,'package.json'),'utf8'
533
+ )).version;
534
+ }
535
+ let assetVersion=await selectedAssetVersion();
512
536
  for(const mapping of mappings){
513
537
  const info=await lstat(mapping.root);
514
538
  if(info.isSymbolicLink()||!info.isDirectory())fail(`Server route root must be a real directory: ${mapping.root}.`);
515
539
  mapping.root=await realpath(mapping.root);
516
540
  }
541
+ // Remember actual resource edges as their owners are served, not every
542
+ // HTML/JS/CSS file in an application's document or attachment corpus.
543
+ const resourcePaths=new Set([routeSet.startPath]);
517
544
  const requestTasks=new Set();
518
545
  const runFileWork=createFileWorkLimiter();
519
546
  const server=http.createServer((request,response)=>{
@@ -541,7 +568,47 @@ async function startOwnedDevServer({
541
568
  await runFileWork(async()=>{
542
569
  const opened=await openSafeFile(mapping.root,relative);
543
570
  if(!opened){deny(response,404,'Not found.');return;}
544
- await sendFile(response,opened,{head:request.method==='HEAD'});
571
+ const extension=path.extname(opened.candidate).toLowerCase();
572
+ const html=extension==='.html'||extension==='.htm';
573
+ let managedDocument=false;
574
+ if(html&&opened.content.includes(MANAGED_IMPORT_MAP_ATTRIBUTE)){
575
+ try{
576
+ managedDocument=inspectImportMapHtml(
577
+ opened.content.toString('utf8')
578
+ ).managedMaps.length>0;
579
+ }catch{
580
+ // An unrelated document is not a source validation surface.
581
+ }
582
+ }
583
+ const entryDocument=target.path===routeSet.startPath||managedDocument;
584
+ const managedMap=path.basename(opened.candidate)==='arcane.importmap.json';
585
+ // A live server can span an SDK upgrade. Refresh the small
586
+ // version record on navigation, not on each resource request.
587
+ if(entryDocument||managedMap)assetVersion=await selectedAssetVersion();
588
+ const runtimeResource=segments[0]==='arcane';
589
+ const browserResource=['script','style','worker','sharedworker','serviceworker']
590
+ .includes(request.headers['sec-fetch-dest']);
591
+ const rewrite=runtimeResource||entryDocument||browserResource
592
+ ||resourcePaths.has(target.path);
593
+ const onReference=({url,kind,baseHref,baseKind})=>{
594
+ if(baseKind==='document'||kind==='fetch'||(kind==='asset'&&!/\.css(?:[?#]|$)/iu.test(url))
595
+ ||(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url)))return;
596
+ try{
597
+ const documentUrl=new URL(target.path,'http://arcane.invalid');
598
+ const base=baseHref?new URL(baseHref,documentUrl):documentUrl;
599
+ const resource=new URL(url,base);
600
+ if(resource.origin===documentUrl.origin
601
+ &&/\.(?:m?js|html?|css)$/iu.test(resource.pathname)){
602
+ resourcePaths.add(decodeURIComponent(resource.pathname));
603
+ }
604
+ }catch{ /* Non-URL values remain under their existing owner. */ }
605
+ };
606
+ await sendFile(response,opened,{
607
+ head:request.method==='HEAD',
608
+ assetVersion:rewrite?assetVersion:undefined,
609
+ revalidate:entryDocument||managedMap,
610
+ onReference
611
+ });
545
612
  });
546
613
  })().catch(async error=>{
547
614
  await events.enqueue({type:'server.request.failed',message:error.message});